From 3a4991cc8e74e6d905c711c128147a64e2d7f807 Mon Sep 17 00:00:00 2001
From: dev_agent
Date: Sun, 11 Oct 2026 16:34:02 -0400
Subject: [PATCH] Supply archive: open an unfrozen ?week= visit in the editable
Supply run (Maestro 29b629aa)
Review remediation for task 29b629aa-1ad9-48a3-be75-0380b645d4fe.
Finding 1 (medium): /plan/shopping/archive?week= forced the list
read-only whatever its status, so a saved link to a Supply run not yet
frozen showed it tagged Archived without checkboxes. Read-only, a run
not yet frozen now opens the editable Supply run screen instead
(replace), and only a frozen run is tagged Archived. The desktop test
that pinned the old read-only behaviour now expects the editable
routing, and a new test covers a past week not yet frozen.
Finding 2 (low): the card link uses the exported supplyRunScreen()
instead of a duplicate supplyRun() helper.
Co-Authored-By: Claude Opus 5.5
---
web/src/shopping/SupplyArchivePage.test.tsx | 24 ++++++++++-------
web/src/shopping/SupplyArchivePage.tsx | 13 +++------
web/src/shopping/SupplyRunPage.tsx | 30 +++++++++++----------
3 files changed, 35 insertions(+), 32 deletions(-)
diff --git a/web/src/shopping/SupplyArchivePage.test.tsx b/web/src/shopping/SupplyArchivePage.test.tsx
index ebff490..1eaad07 100644
--- a/web/src/shopping/SupplyArchivePage.test.tsx
+++ b/web/src/shopping/SupplyArchivePage.test.tsx
@@ -409,20 +409,26 @@ describe('SupplyArchivePage frozen post', () => {
expect(screen.queryByText(/Still open/)).not.toBeInTheDocument()
})
- it('on desktop opens the current week’s post read-only, and with no post open shows this week’s Supply run to change', async () => {
+ it('opens a saved link to a past week’s list not yet frozen in the editable Supply run, not read-only', async () => {
+ stubServer({ 'GET /api/v1/shopping/2026-09-28': () => jsonResponse(200, { ...FROZEN, status: 'current' }) })
+ await renderPage('/plan/shopping/archive?week=2026-09-28', )
+
+ expect(await screen.findByRole('checkbox', { name: 'Pizza dough' })).toBeEnabled()
+ expect(screen.queryByText('Archived')).not.toBeInTheDocument()
+ expect(screen.getByText("Uncheck anything you already have — it won't be counted.")).toBeInTheDocument()
+ })
+
+ it('on desktop opens a saved link to the current week’s list, not yet frozen, in the editable Supply run, as with no post open', async () => {
mockViewport(1440)
- const fetchMock = stubServer({
+ stubServer({
[`GET ${ARCHIVE('week')}`]: archive('week'),
'GET /api/v1/shopping/2026-10-05': () => jsonResponse(200, CURRENT),
})
- const view = await renderPage('/plan/shopping/archive?week=2026-10-05')
+ const view = await renderPage('/plan/shopping/archive?week=2026-10-05', )
- expect(await screen.findByText('Archived')).toBeInTheDocument()
- expect(screen.getByText('Corn tortillas')).toBeInTheDocument()
- expect(screen.queryAllByRole('checkbox')).toEqual([])
- expect(screen.queryByText('Send to Google Chat')).not.toBeInTheDocument()
- fireEvent.click(screen.getByText('Corn tortillas'))
- expect(fetchMock.mock.calls.filter(([, init]) => init?.method !== undefined && init.method !== 'GET')).toEqual([])
+ expect(await screen.findByRole('checkbox', { name: 'Corn tortillas' })).toBeEnabled()
+ expect(screen.queryByText('Archived')).not.toBeInTheDocument()
+ expect(screen.queryByRole('complementary', { name: 'Supply archive' })).not.toBeInTheDocument()
view.unmount()
await renderPage()
diff --git a/web/src/shopping/SupplyArchivePage.tsx b/web/src/shopping/SupplyArchivePage.tsx
index d3e4a80..e645c95 100644
--- a/web/src/shopping/SupplyArchivePage.tsx
+++ b/web/src/shopping/SupplyArchivePage.tsx
@@ -9,7 +9,7 @@ import { Icon } from '../ui/Icon.tsx'
import { IconButton } from '../ui/IconButton.tsx'
import { Segmented } from '../ui/Segmented.tsx'
import { Skeleton } from '../ui/Skeleton.tsx'
-import { useSupplyArchive, WEEK_PARAM, type ArchiveGroup, type ArchivePeriod, type ArchivePost } from './api.ts'
+import { supplyRunScreen, useSupplyArchive, WEEK_PARAM, type ArchiveGroup, type ArchivePeriod, type ArchivePost } from './api.ts'
import styles from './SupplyArchivePage.module.css'
import { SUPPLY_ARCHIVE, SUPPLY_RUN, SupplyRunPage } from './SupplyRunPage.tsx'
@@ -32,12 +32,6 @@ export function archivePost(weekStart: string): string {
return `${SUPPLY_ARCHIVE}?${new URLSearchParams({ [WEEK_PARAM]: weekStart })}`
}
-// supplyRun is the Supply run screen on the week starting on Monday
-// weekStart, where a list not yet frozen can still be changed.
-function supplyRun(weekStart: string): string {
- return `${SUPPLY_RUN}?${new URLSearchParams({ [WEEK_PARAM]: weekStart })}`
-}
-
function plural(n: number, one: string, many: string): string {
return `${n} ${n === 1 ? one : many}`
}
@@ -52,7 +46,7 @@ function PostCard({ post }: { post: ArchivePost }) {
const rest = post.dinners.length - CHIPS
return (
@@ -170,7 +164,8 @@ function SupplyArchive({ panel }: { panel: boolean }) {
// SupplyArchivePage is /plan/shopping/archive. A frozen post opens it on its
// week (?week=), whose list the Supply run screen shows read-only and tagged
-// Archived; a post not yet frozen opens the Supply run instead. On desktop (mock 2f) the archive is a panel at
+// Archived; a post not yet frozen opens the Supply run instead, as does a
+// ?week= visit to a list not yet frozen. On desktop (mock 2f) the archive is a panel at
// the right of that list, this week's editable Supply run when no post is
// open; on mobile and tablet it is the archive or, with a post open, the
// post's list.
diff --git a/web/src/shopping/SupplyRunPage.tsx b/web/src/shopping/SupplyRunPage.tsx
index a43545c..44d4d0b 100644
--- a/web/src/shopping/SupplyRunPage.tsx
+++ b/web/src/shopping/SupplyRunPage.tsx
@@ -1,6 +1,6 @@
import { Archive, Check, CloudUpload, MessageSquare } from 'lucide-react'
import { useState } from 'react'
-import { Link, useSearchParams } from 'react-router'
+import { Link, Navigate, useSearchParams } from 'react-router'
import { defaultWeek, isMonday, localNow, weekdayKicker, weekLabel } from '../plan/week.ts'
import { ApiError } from '../api/errors.ts'
import { formatClockTime, useHouseholdSettings } from '../settings/api.ts'
@@ -15,6 +15,7 @@ import { Skeleton } from '../ui/Skeleton.tsx'
import { Tag } from '../ui/Tag.tsx'
import {
useGenerateSupplyRun,
+ supplyRunScreen,
useSendToChat,
useSupplyRun,
useToggles,
@@ -146,10 +147,10 @@ function ArchiveLink({ layout }: { layout: Breakpoint }) {
// tablet up, where Archive and Send to Google Chat are actions in the
// header; a send that fails says why, and one whose outcome is not known
// offers no retry and leaves the status line saying so. On a current Supply run each item toggles at once (useToggles); a
-// toggle refused puts the item back and says why. An archived one, or any
-// opened from the Supply archive (readOnly), is read-only and tagged
-// Archived, and a toggle it refused is not offered again.
-function SupplyRunList({ weekStart, timezone, layout, readOnly }: WeekProps) {
+// toggle refused puts the item back and says why. An archived one is
+// read-only and tagged Archived, and a toggle it refused is not offered
+// again.
+function SupplyRunList({ weekStart, timezone, layout }: Omit) {
const toggles = useToggles(weekStart)
const send = useSendToChat(weekStart)
const run = toggles.shown
@@ -160,7 +161,7 @@ function SupplyRunList({ weekStart, timezone, layout, readOnly }: WeekProps) {
)
const sendUnconfirmed = send.error instanceof ApiError && send.error.code === 'chat_unconfirmed'
- const viewOnly = readOnly || run.status === 'archived'
+ const viewOnly = run.status === 'archived'
const wide = layout !== 'mobile'
const failed = toggles.failed
const failedItem = failed && run.groups.flatMap((group) => group.items).find((item) => item.id === failed.toggle.id)
@@ -178,11 +179,9 @@ function SupplyRunList({ weekStart, timezone, layout, readOnly }: WeekProps) {
- {run.status === 'archived'
+ {viewOnly
? 'This list is in the Supply archive, as it was when the week ended.'
- : viewOnly
- ? "This week's list as it stands, read-only in the Supply archive."
- : "Uncheck anything you already have — it won't be counted."}
+ : "Uncheck anything you already have — it won't be counted."}
{wide && (
@@ -243,7 +242,8 @@ type WeekProps = { weekStart: string; timezone: string; layout: Breakpoint; read
// WeekSupplyRun is the Supply run of the week starting on weekStart, or,
// when the week has none yet, the offer to make it from the week's plan —
-// not offered read-only.
+// not offered read-only. Read-only, a Supply run not yet frozen opens the
+// editable Supply run screen instead, where it can still change.
function WeekSupplyRun({ weekStart, timezone, layout, readOnly }: WeekProps) {
const run = useSupplyRun(weekStart)
const generate = useGenerateSupplyRun(weekStart)
@@ -270,14 +270,16 @@ function WeekSupplyRun({ weekStart, timezone, layout, readOnly }: WeekProps) {
)
}
- return
+ if (readOnly && run.data.status !== 'archived') return
+ return
}
// SupplyRunPage is the Supply run (mock 1l on mobile, 2b on tablet, 2f on
// desktop) of the week the Plan opens on — next week once this week's
// plan_draft time has come, otherwise this week — or of the week chosen by
-// ?week=. Opened from the Supply archive it is readOnly: tagged Archived,
-// with no checkbox and nothing to make or send, whatever the week. Offline,
+// ?week=. Opened from the Supply archive it is readOnly: a frozen Supply run
+// is tagged Archived, with no checkbox and nothing to make or send; one not
+// yet frozen opens the editable Supply run screen instead. Offline,
// where the settings cannot be read, it chooses the week by the schedule
// saved on this device.
export function SupplyRunPage({ readOnly = false }: { readOnly?: boolean }) {