Restaurant Menu Core Publishing Implementation Plan
For Codex: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Let a DailyMark tenant create, edit, preview, publish, serve, and roll back an immutable static restaurant menu on a DailyMark subdomain.
Architecture: Tenant-scoped draft entities live in PostgreSQL under FORCE RLS. A pure renderer consumes an immutable snapshot, a BullMQ processor uploads the versioned bundle and atomically writes a host manifest, and a new read-only apps/menu service serves files without querying mutable menu data.
Tech Stack: NestJS 11, TypeORM/PostgreSQL, type-rls, Redis/BullMQ, S3 API, Bun, Next.js 16, React 19, Ant Design 6, Vitest/Bun test, Playwright.
Global Constraints
- Follow the umbrella spec at
docs/superpowers/specs/2026-08-09-restaurant-qr-menu-platform-design.mdand program constraints atdocs/superpowers/plans/2026-08-09-restaurant-qr-menu-program-plan.md. - New tables use explicit migration
1723300000000-RestaurantMenus.ts, FORCE RLS, UTC timestamps, and both runtime/CLI entity registration. Role.Membermay edit drafts; onlyRole.AdminandRole.Ownermay publish or roll back.- No draft write or failed build may alter the active host manifest.
- The renderer accepts only
MenuSnapshotV1; it never queries repositories. - Default public host is
${slug}.dailymark.me; custom-domain verification is implemented by the domains/QR plan. - Obtain approval before installing AWS SDK dependencies or running the migration.
File and Interface Ledger
Shared contracts
- Create
packages/menu-contracts/package.json— workspace package metadata. - Create
packages/menu-contracts/tsconfig.json— strict TypeScript config. - Create
packages/menu-contracts/src/index.ts—MenuSnapshotV1,PublicationManifestV1,RenderedFile,HostManifestV1.
export interface RenderedFile {
path: string;
contentType: string;
body: Uint8Array;
cacheControl: string;
sha256: string;
}
export interface MenuSnapshotV1 {
schemaVersion: 1;
publicationId: string;
tenantId: string;
menuId: string;
primaryLocale: 'ru' | 'en' | 'sr';
locales: Array<'ru' | 'en' | 'sr'>;
currency: string;
generatedAt: string;
sections: SnapshotSection[];
}
export interface PublicationManifestV1 {
schemaVersion: 1;
publicationId: string;
rendererVersion: string;
files: Array<{ path: string; sha256: string; contentType: string }>;
}
export interface HostManifestV1 {
schemaVersion: 1;
normalizedHost: string;
publicationId: string;
bundlePrefix: string;
activatedAt: string;
}API boundaries
- Create
apps/api/src/menu/storage/object-store.ts—ObjectStoreinterface and token. - Create
apps/api/src/menu/storage/s3-object-store.ts— production S3 implementation. - Create
apps/api/src/menu/storage/memory-object-store.ts— deterministic tests. - Create
apps/api/src/menu/publish/menu-snapshot.ts—buildMenuSnapshot(...). - Create
apps/api/src/menu/publish/menu-renderer.ts—renderMenu(snapshot): RenderedFile[]. - Create
apps/api/src/menu/publish/host-key.ts—hostManifestKey(host): stringshared algorithm. - Create
apps/api/src/menu/publish/publish.service.ts— enqueue, status, rollback. - Create
apps/api/src/menu/publish/publish.processor.ts— build/upload/verify/activate.
export interface ObjectStore {
put(key: string, body: Uint8Array, options: PutOptions): Promise<void>;
get(key: string): Promise<StoredObject | null>;
head(key: string): Promise<StoredHead | null>;
deletePrefix(prefix: string): Promise<void>;
signUpload(key: string, contentType: string, maxBytes: number): Promise<SignedUpload>;
}
export function hostManifestKey(normalizedHost: string): string;
export function buildMenuSnapshot(input: SnapshotSource): MenuSnapshotV1;
export function renderMenu(snapshot: MenuSnapshotV1): RenderedFile[];Task 1: Shared Contracts, Storage Configuration, and Object Store
Files:
- Create:
packages/menu-contracts/package.json - Create:
packages/menu-contracts/tsconfig.json - Create:
packages/menu-contracts/src/index.ts - Create:
apps/api/src/config/namespaces/menu.config.ts - Create:
apps/api/src/config/services/menu-config.service.ts - Modify:
apps/api/src/config/namespaces/index.ts - Modify:
apps/api/src/config/config.module.ts - Modify:
apps/api/package.json - Modify:
bun.lock - Create:
apps/api/src/menu/storage/object-store.ts - Create:
apps/api/src/menu/storage/memory-object-store.ts - Create:
apps/api/src/menu/storage/s3-object-store.ts - Test:
apps/api/src/config/namespaces/menu.config.spec.ts - Test:
apps/api/src/menu/storage/memory-object-store.spec.ts
Interfaces:
Consumes: existing global
LocalConfigModuleand zod environment validation.Produces:
MenuConfigService,MENU_OBJECT_STORE,ObjectStore, and package@dailymark/menu-contractsfor every later task.[ ] Step 1: Obtain dependency approval and add exact packages
Run after approval:
bun add --cwd apps/api @aws-sdk/client-s3 @aws-sdk/s3-request-presignerExpected: apps/api/package.json and bun.lock contain the two AWS SDK packages; no unrelated upgrade appears.
- [ ] Step 2: Write failing configuration and memory-store tests
it('rejects a non-https public base outside test', () => {
expect(() => menuSchema.parse({ MENU_PUBLIC_BASE_URL: 'http://bucket' }))
.toThrow();
});
it('returns immutable bytes by key', async () => {
const store = new MemoryObjectStore();
await store.put('bundles/p1/index.html', bytes('ok'), {
contentType: 'text/html', cacheControl: 'public,max-age=31536000,immutable',
});
expect(text((await store.get('bundles/p1/index.html'))!.body)).toBe('ok');
});- [ ] Step 3: Run the narrow tests and verify failure
Run:
bun test apps/api/src/config/namespaces/menu.config.spec.ts apps/api/src/menu/storage/memory-object-store.spec.tsExpected: FAIL because menuSchema and MemoryObjectStore do not exist.
- [ ] Step 4: Implement contracts and configuration
Define exact environment keys:
export const menuConfig = registerAs('menu', () => ({
bucket: process.env.MENU_S3_BUCKET ?? 'dailymark-menu-dev',
region: process.env.MENU_S3_REGION ?? 'auto',
endpoint: process.env.MENU_S3_ENDPOINT,
accessKeyId: process.env.MENU_S3_ACCESS_KEY_ID,
secretAccessKey: process.env.MENU_S3_SECRET_ACCESS_KEY,
publicBaseUrl: process.env.MENU_PUBLIC_BASE_URL ?? 'http://localhost:3010',
rootDomain: process.env.MENU_ROOT_DOMAIN ?? 'dailymark.me',
}));MenuConfigService exposes typed getters and enabled only when bucket credentials are complete. S3ObjectStore maps put/get/head/deletePrefix/signUpload to AWS SDK commands. MemoryObjectStore copies input/output byte arrays so tests cannot mutate stored values.
- [ ] Step 5: Run tests, typecheck, and format check
Run:
bun test apps/api/src/config/namespaces/menu.config.spec.ts apps/api/src/menu/storage/memory-object-store.spec.ts
bun run --cwd apps/api typecheck
bun run --cwd apps/api format:checkExpected: PASS; no environment secret appears in snapshots or logs.
- [ ] Step 6: Commit
git add packages/menu-contracts apps/api/package.json bun.lock apps/api/src/config apps/api/src/menu/storage
git commit -m "feat(menu): add publication storage contract"Task 2: Core Entities, Domain Entity, and FORCE RLS Migration
Files:
- Create:
apps/api/src/menu/entities/menu.entity.ts - Create:
apps/api/src/menu/entities/menu-location.entity.ts - Create:
apps/api/src/menu/entities/menu-section.entity.ts - Create:
apps/api/src/menu/entities/menu-item.entity.ts - Create:
apps/api/src/menu/entities/menu-item-variant.entity.ts - Create:
apps/api/src/menu/entities/menu-translation.entity.ts - Create:
apps/api/src/menu/entities/media-asset.entity.ts - Create:
apps/api/src/menu/entities/menu-domain.entity.ts - Create:
apps/api/src/menu/entities/menu-publication.entity.ts - Create:
apps/api/src/menu/entities/menu-audit-event.entity.ts - Create:
apps/api/src/db/migrations/1723300000000-RestaurantMenus.ts - Modify:
apps/api/src/db/data-source.ts - Create:
apps/api/src/menu/menu.module.ts - Modify:
apps/api/src/app.module.ts - Test:
apps/api/test/menu-schema.e2e.spec.ts
Interfaces:
Consumes:
Location,TenantContext, TypeORM, andRLSModule.forFeature.Produces: core repositories and the schema used by CRUD, renderer, domains, imports, AI, rich media, and analytics.
[ ] Step 1: Write a failing migration/RLS e2e test
it('isolates menu rows by rls.tenant_id', async () => {
await asTenant(ds, TENANT_A, () => insertMenu(ds, TENANT_A, 'a'));
const visible = await asTenant(ds, TENANT_B, () => ds.query('select id from menus'));
expect(visible).toEqual([]);
});
it('enforces globally unique normalized domains', async () => {
await insertDomain(ds, TENANT_A, 'cafe.dailymark.me');
await expect(insertDomain(ds, TENANT_B, 'cafe.dailymark.me'))
.rejects.toMatchObject({ code: '23505' });
});- [ ] Step 2: Start test DB, run existing migrations, and verify failure
Run:
bun run test:db:up
bun run test:db:migrate
bun test apps/api/test/menu-schema.e2e.spec.tsExpected: FAIL with relation menus not found.
- [ ] Step 3: Implement focused entities
Use UUID primary keys, explicit tenant_id, sort_index, timestamptz, and string unions. The critical publication fields are:
@Entity({ name: 'menu_publications' })
export class MenuPublication {
@PrimaryGeneratedColumn('uuid') id: string;
@Column({ name: 'tenant_id', type: 'uuid' }) tenantId: string;
@Column({ name: 'menu_id', type: 'uuid' }) menuId: string;
@Column({ type: 'integer' }) version: number;
@Column({ type: 'jsonb' }) snapshot: MenuSnapshotV1;
@Column({ name: 'bundle_prefix', nullable: true }) bundlePrefix: string | null;
@Column({ type: 'jsonb', nullable: true }) manifest: PublicationManifestV1 | null;
@Column({ type: 'varchar' }) state: PublicationState;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) createdAt: Date;
}- [ ] Step 4: Implement migration, indexes, constraints, and policies
Create unique constraints for (tenant_id, menu_id, version), (menu_id, sort_index) where appropriate, and global menu_domains.normalized_host. Apply this policy to every tenant business table:
ALTER TABLE menus ENABLE ROW LEVEL SECURITY;
ALTER TABLE menus FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON menus
USING (tenant_id = current_setting('rls.tenant_id', true)::uuid)
WITH CHECK (tenant_id = current_setting('rls.tenant_id', true)::uuid);The down() migration drops policies/tables in reverse dependency order.
- [ ] Step 5: Register entities in CLI and runtime modules
Add all ten entities to apps/api/src/db/data-source.ts, RLSModule.forFeature([...]) in MenuModule, import MenuModule from AppModule, and use ordinary TypeOrmModule only where an exact public identity lookup is explicitly designed.
- [ ] Step 6: Obtain migration approval, run up/down/up, and run test
Run after approval:
DATABASE_URL=postgres://app:app@localhost:54332/checklist_test bun run --cwd apps/api migration:run
bun test apps/api/test/menu-schema.e2e.spec.ts
DATABASE_URL=postgres://app:app@localhost:54332/checklist_test bun run --cwd apps/api migration:revert
DATABASE_URL=postgres://app:app@localhost:54332/checklist_test bun run --cwd apps/api migration:runExpected: test PASS and round trip completes without orphan types/tables.
- [ ] Step 7: Commit
git add apps/api/src/menu apps/api/src/db/data-source.ts apps/api/src/db/migrations/1723300000000-RestaurantMenus.ts apps/api/src/app.module.ts apps/api/test/menu-schema.e2e.spec.ts
git commit -m "feat(menu): establish tenant-isolated menu schema"Task 3: Draft CRUD, Location Assignment, and RBAC API
Files:
- Create:
apps/api/src/menu/menu.dto.ts - Create:
apps/api/src/menu/menu.service.ts - Create:
apps/api/src/menu/menu.controller.ts - Modify:
apps/api/src/menu/menu.module.ts - Test:
apps/api/src/menu/menu.service.spec.ts - Test:
apps/api/test/menu.e2e.spec.ts
Interfaces:
Consumes: core repositories,
RequestUser,roleAtLeast,Role, and existingLocationrepository.Produces: REST endpoints
/menus,/menus/:id,/menus/:id/sections,/menus/:id/items,/menus/:id/preview-sourceandMenuDraftDetail.[ ] Step 1: Write failing service tests for slug, location, ordering, and tenant scope
it('creates the menu and default host atomically', async () => {
const result = await service.create(TENANT, ACTOR, {
name: 'Dinner', slug: 'warm-corner', primaryLocale: 'ru', currency: 'RSD', locationIds: [LOCATION],
});
expect(result.defaultHost).toBe('warm-corner.dailymark.me');
});- [ ] Step 2: Run narrow test and verify failure
Run: bun test apps/api/src/menu/menu.service.spec.ts
Expected: FAIL because MenuService does not exist.
- [ ] Step 3: Implement validated DTOs and transaction-safe service methods
Use class-validator DTOs with ISO-4217 three-letter currency, locale enum, non-empty names, bounded arrays, and slug regex. Expose exact methods:
create(tenantId: string, actorId: string, dto: CreateMenuDto): Promise<MenuDraftDetail>;
get(tenantId: string, menuId: string): Promise<MenuDraftDetail>;
update(tenantId: string, actorId: string, menuId: string, dto: UpdateMenuDto): Promise<MenuDraftDetail>;
replaceSections(tenantId: string, actorId: string, menuId: string, dto: ReplaceSectionsDto): Promise<MenuDraftDetail>;
archive(tenantId: string, actorId: string, menuId: string): Promise<void>;Create menu, location joins, default MenuDomain, and audit event in one transaction. Catch unique host violation and return HTTP 409 without revealing another tenant.
- [ ] Step 4: Implement guarded controller
Apply @UseGuards(CsrfGuard, AuthGuard) to authenticated routes. Allow member draft edits. Require roleAtLeast(user.role, Role.Admin) for archive and return 403 otherwise. Obtain tenantId and actorId only from req.user.
- [ ] Step 5: Add e2e tests
Cover owner/admin/member CRUD, member archive denial, invalid location from another tenant, duplicate host conflict, CSRF rejection, and cross-tenant UUID probing returning 404.
- [ ] Step 6: Run tests and API gates
bun test apps/api/src/menu/menu.service.spec.ts
bun test apps/api/test/menu.e2e.spec.ts
bun run --cwd apps/api typecheck
bun run --cwd apps/api lintExpected: PASS and zero lint errors.
- [ ] Step 7: Commit
git add apps/api/src/menu apps/api/test/menu.e2e.spec.ts
git commit -m "feat(menu): add tenant draft management"Task 4: Deterministic Snapshot and Static Renderer
Files:
- Create:
apps/api/src/menu/publish/menu-snapshot.ts - Create:
apps/api/src/menu/publish/menu-renderer.ts - Create:
apps/api/src/menu/publish/html.ts - Create:
apps/api/src/menu/publish/host-key.ts - Test:
apps/api/src/menu/publish/menu-snapshot.spec.ts - Test:
apps/api/src/menu/publish/menu-renderer.spec.ts - Test fixture:
apps/api/src/menu/publish/__fixtures__/snapshot-v1.ts - Golden outputs:
apps/api/src/menu/publish/__fixtures__/golden/
Interfaces:
Consumes: fully loaded
SnapshotSourcefrom repositories.Produces:
MenuSnapshotV1, deterministicRenderedFile[], and host-manifest key algorithm used by gateway.[ ] Step 1: Write failing deterministic and safety tests
it('renders identical bytes for identical snapshots', () => {
expect(renderMenu(FIXTURE)).toEqual(renderMenu(structuredClone(FIXTURE)));
});
it('escapes tenant text and emits no executable tenant markup', () => {
const files = renderMenu(withItemName('<script>alert(1)</script>'));
expect(text(file(files, 'ru/index.html').body)).toContain('<script>');
});- [ ] Step 2: Run and verify failure
Run: bun test apps/api/src/menu/publish/menu-snapshot.spec.ts apps/api/src/menu/publish/menu-renderer.spec.ts
Expected: FAIL because renderer modules do not exist.
- [ ] Step 3: Implement canonical snapshot sorting and serialization
Sort sections/items/variants by sortIndex, then stable ID. Convert Date to UTC ISO strings. Never include mutable database timestamps that do not affect output. Include only reviewed translations and approved/publishable media.
- [ ] Step 4: Implement static files and cache headers
Generate ru/index.html, en/index.html, sr/index.html for enabled locales, assets/menu.css, assets/menu.js, and manifest.json. HTML references only relative versioned bundle paths. Use:
const IMMUTABLE = 'public,max-age=31536000,immutable';
const files = locales.map(locale => renderedHtml(locale, IMMUTABLE));
return files.map(file => ({ ...file, sha256: sha256(file.body) }));menu.js provides locale switch, search/filter extension points, reduced-motion behavior, and no analytics code yet.
- [ ] Step 5: Assert budgets and golden output
Tests gzip critical files and assert JavaScript ≤50 KB and HTML+CSS+JS ≤150 KB. Golden changes require an intentional renderer-version bump.
- [ ] Step 6: Run tests and typecheck
bun test apps/api/src/menu/publish
bun run --cwd apps/api typecheckExpected: PASS with stable checksums on two consecutive runs.
- [ ] Step 7: Commit
git add apps/api/src/menu/publish packages/menu-contracts
git commit -m "feat(menu): render deterministic static bundles"Task 5: Publish Queue, Verification, Activation, and Rollback
Files:
- Create:
apps/api/src/menu/publish/publish.constants.ts - Create:
apps/api/src/menu/publish/publish.service.ts - Create:
apps/api/src/menu/publish/publish.processor.ts - Create:
apps/api/src/menu/publish/publication-validator.ts - Create:
apps/api/src/menu/publish/publish.controller.ts - Modify:
apps/api/src/menu/menu.module.ts - Test:
apps/api/src/menu/publish/publication-validator.spec.ts - Test:
apps/api/src/menu/publish/publish.processor.spec.ts - Test:
apps/api/test/menu-publication.e2e.spec.ts
Interfaces:
Consumes:
ObjectStore, renderer, repositories, BullMQ, and admin-or-owner request identity.Produces:
POST /menus/:id/publications,GET /menus/:id/publications,POST /menus/:id/publications/:publicationId/rollback.[ ] Step 1: Write failing validator and failure-atomicity tests
it('does not activate when read-back checksum fails', async () => {
store.corruptNextRead('bundles/p1/ru/index.html');
await expect(processor.process(jobFor('p1'))).rejects.toThrow('checksum');
expect(await store.get(hostManifestKey('warm-corner.dailymark.me'))).toBeNull();
});- [ ] Step 2: Run and verify failure
Run: bun test apps/api/src/menu/publish/publication-validator.spec.ts apps/api/src/menu/publish/publish.processor.spec.ts
Expected: FAIL because validator and processor do not exist.
- [ ] Step 3: Implement publication readiness and immutable snapshot transaction
Block invalid price, missing primary-locale name, unreviewed factual fields, missing rich-media fallback, inactive default domain, or duplicate build for the same revision. In one transaction allocate next version, save snapshot/state building, and audit enqueue.
- [ ] Step 4: Implement queue registration and processor
Register menu-publish only when QUEUE_DISABLED !== '1'. Use job ID menu-publish:${publicationId}. Upload all files to public/bundles/${publicationId}/, upload manifest.json, read every head/checksum, then write host manifest last.
- [ ] Step 5: Implement idempotent rollback
Rollback accepts only a verified, active, or superseded publication for the same menu. It writes the prior HostManifestV1, updates database active states in a transaction, records audit, and returns the now-active version. Repeating the same rollback returns success without another state transition.
- [ ] Step 6: Run unit/e2e tests
bun test apps/api/src/menu/publish
bun test apps/api/test/menu-publication.e2e.spec.ts
bun run --cwd apps/api typecheckExpected: PASS for success, duplicate job, upload failure, checksum failure, activation retry, and rollback.
- [ ] Step 7: Commit
git add apps/api/src/menu apps/api/test/menu-publication.e2e.spec.ts
git commit -m "feat(menu): publish and roll back immutable versions"Task 6: Read-Only Public Menu Gateway
Files:
- Create:
apps/menu/package.json - Create:
apps/menu/tsconfig.json - Create:
apps/menu/Dockerfile - Create:
apps/menu/src/config.ts - Create:
apps/menu/src/host.ts - Create:
apps/menu/src/store.ts - Create:
apps/menu/src/server.ts - Create:
apps/menu/src/main.ts - Test:
apps/menu/src/host.spec.ts - Test:
apps/menu/src/server.spec.ts - Modify:
package.json - Modify:
bun.lock
Interfaces:
Consumes:
HostManifestV1,PublicationManifestV1,hostManifestKeyalgorithm, and read-only S3 credentials.Produces: HTTP
GET/HEADstatic delivery with no database dependency.[ ] Step 1: Write failing host and server tests
it('normalizes host and strips a valid port', () => {
expect(normalizeRequestHost('Warm-Corner.DailyMark.Me:443')).toBe('warm-corner.dailymark.me');
});
it('serves active index with revalidation and assets immutable', async () => {
const response = await app.request('https://warm-corner.dailymark.me/ru/');
expect(response.headers.get('cache-control')).toBe('no-cache');
});- [ ] Step 2: Run and verify failure
Run: bun test --cwd apps/menu
Expected: FAIL because gateway modules do not exist.
- [ ] Step 3: Implement strict host and path resolution
Reject missing/multiple Host, IP literals, invalid IDNA, encoded slash, dot traversal, hidden source prefixes, and unknown host manifests. Map / to /${primaryLocale}/index.html; map directory paths to index.html; support only GET/HEAD.
- [ ] Step 4: Implement cache and security headers
Index HTML: Cache-Control: no-cache, ETag from object checksum. Hashed/versioned assets: public,max-age=31536000,immutable. Always set CSP, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, and deny framing unless the preview route uses an explicit management-only origin.
- [ ] Step 5: Run gateway tests and build
bun test --cwd apps/menu
bun run --cwd apps/menu typecheck
bun run --cwd apps/menu buildExpected: PASS; build produces a standalone Bun entrypoint.
- [ ] Step 6: Commit
git add apps/menu package.json bun.lock packages/menu-contracts
git commit -m "feat(menu): serve published bundles by host"Task 7: Management UI for Menus, Editor, Preview, and Publications
Files:
- Create:
apps/web/ui/app/menus/menuClient.ts - Create:
apps/web/ui/app/menus/menuTypes.ts - Create:
apps/web/ui/app/menus/MenusScreen.tsx - Create:
apps/web/ui/app/menus/MenuEditorScreen.tsx - Create:
apps/web/ui/app/menus/MenuPreview.tsx - Create:
apps/web/ui/app/menus/PublicationsPanel.tsx - Create:
apps/web/ui/app/menus/menuView.ts - Test:
apps/web/ui/app/menus/menuView.test.ts - Story:
apps/web/ui/app/menus/MenuEditorScreen.stories.tsx - Create:
apps/web/app/[locale]/app/menus/page.tsx - Create:
apps/web/app/[locale]/app/menus/[menuId]/page.tsx - Modify:
apps/web/ui/app/AppShell.tsx - Modify:
apps/web/messages/ru.json - Modify:
apps/web/messages/en.json - Modify:
apps/web/messages/sr.json
Interfaces:
Consumes: core REST endpoints and existing
apiRequest<T>.Produces: owner/admin/member draft workflow; publication/rollback actions hidden and server-denied for member.
[ ] Step 1: Write failing pure view-state tests
it('blocks publish when primary names or prices are invalid', () => {
expect(publicationReadiness(invalidDraft).blocking.map(x => x.code))
.toEqual(['missing_primary_name', 'invalid_price']);
});- [ ] Step 2: Run and verify failure
Run: bun test --cwd apps/web ui/app/menus/menuView.test.ts
Expected: FAIL because publicationReadiness does not exist.
- [ ] Step 3: Implement typed client and pure view functions
menuClient uses apiRequest for list/get/create/update/replace/publish/rollback. menuView.ts maps backend readiness codes to translated UI keys and never duplicates authoritative validation rules as permission decisions.
- [ ] Step 4: Implement mobile-first screens
Menus list shows location, host, current version, health, and review count. Editor uses focused section/item components, sortable order, locale tabs, variants, factual warning, autosave state, and preview. Publications panel shows build state, manifest health, diff summary, and rollback confirmation.
- [ ] Step 5: Add route/nav/copy/story states
Add menus navigation and RU/EN/SR messages. Storybook covers empty, draft, blocking warnings, building, published, failed, and read-only member states with the existing Ant Design theme and mobile viewport.
- [ ] Step 6: Run web gates
bun test --cwd apps/web ui/app/menus/menuView.test.ts
bun run --cwd apps/web typecheck
bun run --cwd apps/web lint
bun run --cwd apps/web build-storybookExpected: PASS, including Storybook a11y checks for the new stories.
- [ ] Step 7: Commit
git add apps/web/app apps/web/ui/app/menus apps/web/ui/app/AppShell.tsx apps/web/messages
git commit -m "feat(menu): add draft and publication workspace"Task 8: End-to-End Acceptance, Resilience, and Operations Documentation
Files:
- Create:
apps/web/playwright.config.ts - Create:
apps/web/e2e/menu-public.spec.ts - Create:
apps/api/test/menu-rbac.e2e.spec.ts - Create:
docs-internal/menu-publishing-operations.md - Modify:
.env.dev.example - Modify:
.env.staging.example
Interfaces:
Consumes: the complete core workstream.
Produces: authoritative acceptance evidence and operator runbook.
[ ] Step 1: Write failing acceptance tests
The test creates a tenant menu, publishes version 1, edits/publishes version 2, verifies stable host content, rolls back to version 1, stops the management API fixture, and verifies the gateway still serves version 1.
expect(await page.locator('[data-menu-version]').getAttribute('data-menu-version')).toBe('1');
expect(await request.get(publicUrl)).toMatchObject({ status: 200 });- [ ] Step 2: Run and verify failure before final wiring
Run:
bun apps/web/node_modules/playwright/cli.js test apps/web/e2e/menu-public.spec.ts
bun test apps/api/test/menu-rbac.e2e.spec.tsExpected: FAIL on the first missing deployment/configuration behavior.
- [ ] Step 3: Complete environment examples and runbook
Document exact config keys, bucket policy boundaries, publication state diagnosis, dead-letter replay preconditions, active-manifest inspection, safe rollback, orphan-prefix cleanup, and recovery when DB and object manifest disagree.
- [ ] Step 4: Run full relevant verification
bun run test:db:up
bun run test:all
bun run typecheck
bun run lint
bun run format:check
bun apps/web/node_modules/playwright/cli.js test apps/web/e2e/menu-public.spec.ts
bun run test:db:downExpected: all existing and new suites pass. If the gateway requires a local object-store fixture, its setup/teardown is part of the test command and leaves no process running.
- [ ] Step 5: Capture acceptance evidence
Record test output, bundle gzip sizes, checksums, accessibility result, version 1→2→1 content proof, and management-API outage proof in the workstream issue.
- [ ] Step 6: Commit
git add apps/web/playwright.config.ts apps/web/e2e/menu-public.spec.ts apps/api/test/menu-rbac.e2e.spec.ts docs-internal/menu-publishing-operations.md .env.dev.example .env.staging.example
git commit -m "test(menu): prove static publication resilience"Workstream Acceptance Checklist
- [ ] Core schema, migration round trip, FORCE RLS, and cross-tenant tests pass.
- [ ] Member edits but cannot publish/rollback; admin/owner can.
- [ ] Same snapshot and renderer version produce identical bytes/checksums.
- [ ] Upload/build/checksum failure preserves current active host manifest.
- [ ] Version activation and rollback are retry-safe.
- [ ] Gateway serves by host without database/API dependency.
- [ ] Stable host shows version 1, then 2, then rolled-back 1.
- [ ] Management UI covers empty/loading/error/building/published/failed/read-only states in RU/EN/SR.
- [ ] Critical bundle budgets and Storybook accessibility checks pass.
- [ ] Runbook and environment contract are reviewed by operations.