release: v1.32.0 — hardening and scalability (#580)

* release: prepare v1.32.0

* test: stabilize water capture re-entry
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-07-22 04:14:15 +02:00
1 parent b01822be48
commit 62eb431d66
372 files changed
+25178 -8561

No files matched your search

@@ -145,7 +145,10 @@ describe("MoodList — rated factor editing", () => {
expect(listSrc).toContain(
"const [editRatedFactors, setEditRatedFactors] = useState<",
);
expect(listSrc).toContain("setEditRatedFactors(entry.ratedFactors ?? [])");
expect(listSrc).toContain(
"ratedFactors: [...(entry.ratedFactors ?? [])].sort",
);
expect(listSrc).toContain("setEditRatedFactors(seed.ratedFactors)");
expect(listSrc).toMatch(
/<MoodTagPicker[\s\S]*ratedFactors=\{editRatedFactors\}[\s\S]*onRateFactor=\{rateEditFactor\}/,
);
+20 -24
View File
@@ -42,7 +42,8 @@ import {
moodDependentKeys,
refetchInactiveDailyReads,
} from "@/lib/query-keys";
import { ApiError, apiPost } from "@/lib/api/api-fetch";
import { apiPost } from "@/lib/api/api-fetch";
import { localizedApiError } from "@/lib/api/localized-error";
import { SheetSection, SheetSectionCount } from "@/components/ui/sheet-section";
import { MoodTagPicker, type RatedFactor } from "./mood-tag-picker";
import { MoodQuickTags } from "./mood-quick-tags";
@@ -224,11 +225,7 @@ export function MoodForm({ onSuccess, onCancel, footerSlot }: MoodFormProps) {
toast.success(t("common.saved"));
onSuccess?.();
} catch (err) {
setError(
err instanceof ApiError && err.message
? err.message
: t("mood.saveError"),
);
setError(localizedApiError(err, t, "mood.saveError"));
} finally {
setLoading(false);
}
@@ -287,8 +284,7 @@ export function MoodForm({ onSuccess, onCancel, footerSlot }: MoodFormProps) {
// v1.17.0 — the "Note & details" badge counts the free-text tags entered
// plus the note (when present), so a collapsed section communicates what
// it holds. The auto-populated timestamp is excluded (it always carries a
// default, so it would never read as "empty").
// it holds.
const freeTextTagCount = tagsInput
.split(",")
.map((s) => s.trim())
@@ -365,6 +361,20 @@ export function MoodForm({ onSuccess, onCancel, footerSlot }: MoodFormProps) {
onExpand={() => setMoreTagsOpen(true)}
/>
<FieldGroup htmlFor="mood-logged-at" label={t("mood.timestamp")}>
<DateTimeField
id="mood-logged-at"
value={moodLoggedAt}
onChange={setMoodLoggedAt}
// v1.17 W1b — match the server bound: no future instant.
max={getDefaultMoodLoggedAtValue()}
required
aria-required="true"
aria-invalid={!!error || undefined}
aria-describedby={errorDescriptor}
/>
</FieldGroup>
{/* v1.17.0 — "More tags": the full structured-tag taxonomy
(binary tiles + custom). Controlled so the Quick row's "+" can
open it; the badge reports how many tags are selected. */}
@@ -399,8 +409,8 @@ export function MoodForm({ onSuccess, onCancel, footerSlot }: MoodFormProps) {
</SheetSection>
{/* v1.17.0 — "Note & details": free-text tags, GLP-1 quick-tags,
the note field, and the timestamp. The badge counts whichever
of these carry input. */}
and the note field. The badge counts whichever of these carry
input. */}
<SheetSection
title={t("mood.sectionNoteDetails")}
icon={<StickyNote />}
@@ -518,20 +528,6 @@ export function MoodForm({ onSuccess, onCancel, footerSlot }: MoodFormProps) {
rows={3}
/>
</FieldGroup>
<FieldGroup htmlFor="mood-logged-at" label={t("mood.timestamp")}>
<DateTimeField
id="mood-logged-at"
value={moodLoggedAt}
onChange={setMoodLoggedAt}
// v1.17 W1b — match the server bound: no future instant.
max={getDefaultMoodLoggedAtValue()}
required
aria-required="true"
aria-invalid={!!error || undefined}
aria-describedby={errorDescriptor}
/>
</FieldGroup>
</SheetSection>
</div>
)}
+74 -12
View File
@@ -23,6 +23,10 @@ import {
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import {
SeededFormDiscardDialog,
useSeededFormDismissal,
} from "@/components/forms/use-seeded-form-dismissal";
import {
DropdownMenu,
DropdownMenuContent,
@@ -83,6 +87,7 @@ import {
import { useRovingRadioGroup } from "@/hooks/use-roving-radio-group";
import { MoodTagPicker, type RatedFactor } from "./mood-tag-picker";
import { apiDelete, apiGet, apiPost, apiPut } from "@/lib/api/api-fetch";
import { localizedApiError } from "@/lib/api/localized-error";
// Re-export the score map under the legacy local name to keep the
// rest of this file unchanged. v1.4.27 B6 / BL-P6-11 — the single
@@ -178,6 +183,21 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
const [editRatedFactors, setEditRatedFactors] = useState<RatedFactor[]>([]);
const [editNote, setEditNote] = useState("");
const [editMoodLoggedAt, setEditMoodLoggedAt] = useState("");
const [editSeed, setEditSeed] = useState<{
mood: string;
tagsInput: string;
tagKeys: string[];
ratedFactors: RatedFactor[];
note: string;
moodLoggedAt: string;
}>({
mood: "",
tagsInput: "",
tagKeys: [],
ratedFactors: [],
note: "",
moodLoggedAt: "",
});
const [editError, setEditError] = useState<string | null>(null);
function toggleEditTagKey(key: string) {
@@ -390,41 +410,78 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
onSuccess: async () => {
await invalidateKeys(queryClient, moodDependentKeys);
await refetchInactiveDailyReads(queryClient);
setEditing(null);
setEditError(null);
setEditSeed({
mood: editMood,
tagsInput: editTagsInput,
tagKeys: [...editTagKeys].sort(),
ratedFactors: [...editRatedFactors].sort((a, b) =>
a.key.localeCompare(b.key),
),
note: editNote,
moodLoggedAt: editMoodLoggedAt,
});
dismissEdit();
},
onError: (err) => {
setEditError(err instanceof Error ? err.message : t("mood.saveError"));
setEditError(localizedApiError(err, t, "mood.saveError"));
},
});
const editDismissal = useSeededFormDismissal({
seed: editSeed,
value: {
mood: editMood,
tagsInput: editTagsInput,
tagKeys: [...editTagKeys].sort(),
ratedFactors: [...editRatedFactors].sort((a, b) =>
a.key.localeCompare(b.key),
),
note: editNote,
moodLoggedAt: editMoodLoggedAt,
},
blocked: updateMutation.isPending || deleteMutation.isPending,
});
const totalPages = data ? Math.ceil(data.meta.total / PAGE_SIZE) : 0;
function startEdit(entry: MoodEntry) {
const seed = {
mood: entry.mood,
tagsInput: entry.tags.join(", "),
tagKeys: [...(entry.tagKeys ?? [])].sort(),
ratedFactors: [...(entry.ratedFactors ?? [])].sort((a, b) =>
a.key.localeCompare(b.key),
),
note: entry.note ?? "",
moodLoggedAt: toDateTimeLocalValue(entry.moodLoggedAt),
};
setEditing(entry);
setEditMood(entry.mood);
setEditTagsInput(entry.tags.join(", "));
setEditTagKeys(entry.tagKeys ?? []);
setEditRatedFactors(entry.ratedFactors ?? []);
setEditNote(entry.note ?? "");
setEditMoodLoggedAt(toDateTimeLocalValue(entry.moodLoggedAt));
setEditMood(seed.mood);
setEditTagsInput(seed.tagsInput);
setEditTagKeys(seed.tagKeys);
setEditRatedFactors(seed.ratedFactors);
setEditNote(seed.note);
setEditMoodLoggedAt(seed.moodLoggedAt);
setEditSeed(seed);
setEditError(null);
}
function closeEdit() {
if (updateMutation.isPending || deleteMutation.isPending) return;
function dismissEdit() {
setEditing(null);
setEditError(null);
setEditDeleteDialogOpen(false);
}
function closeEdit() {
editDismissal.requestClose(dismissEdit);
}
async function deleteEditingEntry() {
if (!editing) return;
try {
setEditError(null);
await deleteMutation.mutateAsync(editing.id);
closeEdit();
dismissEdit();
} catch {
setEditError(t("mood.deleteError"));
}
@@ -1044,6 +1101,11 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
</form>
)}
</ResponsiveSheet>
<SeededFormDiscardDialog
open={editDismissal.discardDialogOpen}
onConfirm={editDismissal.confirmDiscard}
onCancel={editDismissal.cancelDiscard}
/>
</>
);
}