Repository: IgnisDa/ryot (language: rust)

Issue:
[Bug] Saved workouts are not visible in 8.2.2
Saved workouts are not visible at all in ryot 8.2.2.

## Steps to reproduce
1. Start ryot using this compose file (similar to https://docs.ryot.io/), with an empty database i.e. the postgres volume is not present:
```yaml
services:
  ryot-db:
    image: postgres:17-alpine # at-least version 15 is required
    restart: unless-stopped
    volumes:
      - postgres_storage:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_USER=postgres
      - POSTGRES_DB=postgres
      - TZ=Europe/Amsterdam
    container_name: ryot-db

  ryot:
    image: ghcr.io/ignisda/ryot:v8.2.2
    environment:
      - DATABASE_URL=postgres://postgres:postgres@ryot-db:5432/postgres
      - TZ=Europe/Amsterdam
      - SERVER_ADMIN_ACCESS_TOKEN=28ebb3ae554fa9867ba0
    ports:
      - "8000:8000"
    pull_policy: always
    container_name: ryot

volumes:
  postgres_storage:
```
2. Open `http://localhost:8000` in a browser
3. Create new user, log in as a new user
4. Go to workouts, start new workout
5. Add two exercises, complete each set.
6. Click on "Finish". Workout took 22 seconds. Summary is displayed.
7. Go to workouts tab

## Expected result

One workout is shown, the one just created.

## Actual results

No workout is created. There are multiple 404 errors in the logs. See: [compose.log](https://github.com/user-attachments/files/18885012/compose.log)


Discussion / hints:
click on the refresh button.

<img width="524" alt="Image" src="https://github.com/user-attachments/assets/3b9bfe43-26b5-450e-b2a8-930715aaa9c0" />

this applies to pretty much all list pages.
After refreshing the page nothing happens - workout is still not there. There's no refresh button in 8.2.2 you mentioned:

![Image](https://github.com/user-attachments/assets/0ca87572-eb05-414b-8d6b-200a03bc9095)


Workouts are present in the database: 
```
postgres=# select id, start_time, end_time from workout;p
        id        |         start_time         |          end_time          
------------------+----------------------------+----------------------------
 wor_e9NK7B1yBeG_ | 2025-02-24 13:49:51.079+01 | 2025-02-24 13:50:16.389+01
(1 row)
```

but never shown in the UI.


The issue is reproducible on latest develop image: `sha-0f6c412`
The cache should expire after an hour and should be visible there.
Can I flush the cache manually? That's annoying to have to wait for an hour.
actually this is a bug on ryot side, so i will re-open the issue.

One question: are your workouts usually less than an hour? 
Sometimes, yes.
Same problem on the latest version (though this is my first install). Database registers the information, but UI not showing workouts. Also appears to be a problem for books and movie additions, I haven't tested all categories.

The media and the workouts do show up in the "Collections" part of the UI.

Fix patch:
diff --git a/apps/frontend/app/components/common.tsx b/apps/frontend/app/components/common.tsx
index 4385497eb7..226092dac4 100644
--- a/apps/frontend/app/components/common.tsx
+++ b/apps/frontend/app/components/common.tsx
@@ -1413,7 +1413,7 @@ export const DisplayListDetailsAndRefresh = (props: {
 				<Text display="inline" fw="bold">
 					{props.total}
 				</Text>{" "}
-				items found
+				item{props.total === 1 ? "" : "s"} found
 				{props.rightSection}
 			</Box>
 			<Form
diff --git a/apps/frontend/app/routes/_dashboard.fitness.$entity.list.tsx b/apps/frontend/app/routes/_dashboard.fitness.$entity.list.tsx
index 4d99613267..af85754797 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.$entity.list.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.$entity.list.tsx
@@ -224,23 +224,23 @@ export default function Page() {
 						<FiltersModalForm />
 					</FiltersModal>
 				</Group>
-				{loaderData.displayData.items.length > 0 ? (
-					<Stack gap="xs">
-						<DisplayListDetailsAndRefresh
-							cacheId={loaderData.displayData.cacheId}
-							total={loaderData.displayData.details.total}
-						/>
-						{loaderData.displayData.items.map((entityId, index) => (
+				<Stack gap="xs">
+					<DisplayListDetailsAndRefresh
+						cacheId={loaderData.displayData.cacheId}
+						total={loaderData.displayData.details.total}
+					/>
+					{loaderData.displayData.items.length > 0 ? (
+						loaderData.displayData.items.map((entityId, index) => (
 							<DisplayFitnessEntity
 								index={index}
 								key={entityId}
 								entityId={entityId}
 							/>
-						))}
-					</Stack>
-				) : (
-					<Text>No {loaderData.entity} found</Text>
-				)}
+						))
+					) : (
+						<Text>No {loaderData.entity} found</Text>
+					)}
+				</Stack>
 				<Center>
 					<Pagination
 						size="sm"
diff --git a/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx b/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx
index 489200dec0..e5fb03f5a0 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx
@@ -130,17 +130,17 @@ export const loader = async ({ request }: Route.LoaderArgs) => {
 			UserExercisesListDocument,
 			{
 				input: {
+					sortBy: query.sortBy,
 					search: { page: query[pageQueryParam], query: query.query },
 					filter: {
-						equipment: query.equipment,
-						force: query.force,
+						type: query.type,
 						level: query.level,
-						mechanic: query.mechanic,
+						force: query.force,
 						muscle: query.muscle,
-						type: query.type,
+						mechanic: query.mechanic,
+						equipment: query.equipment,
 						collection: query.collection,
 					},
-					sortBy: query.sortBy,
 				},
 			},
 		),
@@ -198,6 +198,10 @@ export default function Page() {
 		{ open: openFiltersModal, close: closeFiltersModal },
 	] = useDisclosure(false);
 
+	const replacingExerciseId =
+		currentWorkout?.replacingExerciseIdx &&
+		currentWorkout.exercises[currentWorkout.replacingExerciseIdx].exerciseId;
+
 	const isFilterChanged = Object.keys(defaultFiltersValue)
 		.filter((k) => k !== pageQueryParam && k !== "query")
 		.some(
@@ -205,6 +209,11 @@ export default function Page() {
 			(k) => (loaderData.query as any)[k] !== (defaultFiltersValue as any)[k],
 		);
 
+	const { data: replacingExercise } = useQuery({
+		enabled: !!replacingExerciseId,
+		...getExerciseDetailsQuery(replacingExerciseId || ""),
+	});
+
 	const allowAddingExerciseToWorkout =
 		currentWorkout &&
 		isFitnessActionActive &&
@@ -246,11 +255,7 @@ export default function Page() {
 				</Group>
 				{currentWorkout?.replacingExerciseIdx ? (
 					<Alert icon={<IconAlertCircle />}>
-						You are replacing exercise:{" "}
-						{
-							currentWorkout.exercises[currentWorkout.replacingExerciseIdx]
-								.exerciseId
-						}
+						You are replacing exercise: {replacingExercise?.name}
 					</Alert>
 				) : null}
 				{mergingExercise ? (
diff --git a/crates/services/cache/src/lib.rs b/crates/services/cache/src/lib.rs
index 84d0a436c0..d716b8e7da 100644
--- a/crates/services/cache/src/lib.rs
+++ b/crates/services/cache/src/lib.rs
@@ -68,6 +68,9 @@ impl CacheService {
         &self,
         items: Vec<(ApplicationCacheKey, ApplicationCacheValue)>,
     ) -> Result<HashMap<ApplicationCacheKey, Uuid>> {
+        if items.is_empty() {
+            return Ok(HashMap::new());
+        }
         let now = Utc::now();
         let mut response = HashMap::new();
         for (key, value) in items {


Test patch: