From cf563f355033113cf479e181980595565294092e Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 2 Aug 2026 14:54:19 +0200 Subject: [PATCH] docs: plan manual recent furni resync --- .../2026-08-02-manual-recent-furni-resync.md | 357 ++++++++++++++++++ 1 file changed, 357 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-02-manual-recent-furni-resync.md diff --git a/docs/superpowers/plans/2026-08-02-manual-recent-furni-resync.md b/docs/superpowers/plans/2026-08-02-manual-recent-furni-resync.md new file mode 100644 index 00000000..7afaa363 --- /dev/null +++ b/docs/superpowers/plans/2026-08-02-manual-recent-furni-resync.md @@ -0,0 +1,357 @@ +# Manual Recent Furni Resync Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a guarded Tools command to Admin Import Furni that resyncs furniture imported during the last seven days and displays the operation result. + +**Architecture:** Keep the existing permission-protected `/api/admin/import/furni/resync` route unchanged. Add a client-safe request/normalization helper with an injected fetcher so its exact URL, HTTP method, success payload, partial failures, and transport failures are testable without rendering the large Import Furni client. The existing client component owns the confirmation, loading, and result UI state. + +**Tech Stack:** TypeScript, React 19, Next.js 16 App Router, Vitest, shadcn/Radix UI, Sonner, Biome. + +## Global Constraints + +- The operation targets only `furni_import` audit entries from the last seven days. +- Use the existing `ASSETS_IMPORT`-protected endpoint and `adminFetch` CSRF flow. +- Do not expose `all=1`, delete database rows, or regenerate `.nitro`, SWF, or icon files. +- Display examined, resynced, failed, RCON status, and returned per-item errors. +- Prevent duplicate submissions while a request is active. + +--- + +### Task 1: Tested recent-resync client contract + +**Files:** +- Create: `src/lib/admin/recent-furni-resync.ts` +- Test: `src/lib/admin/recent-furni-resync.test.ts` + +**Interfaces:** +- Consumes: a fetcher matching `(input: RequestInfo | URL, init?: RequestInit) => Promise`. +- Produces: `RECENT_FURNI_RESYNC_URL`, `RecentFurniResyncError`, `RecentFurniResyncResult`, and `requestRecentFurniResync(fetcher): Promise`. + +- [ ] **Step 1: Write the failing request-contract tests** + +Create `src/lib/admin/recent-furni-resync.test.ts`: + +```ts +import { describe, expect, it, vi } from "vitest"; +import { + RECENT_FURNI_RESYNC_URL, + requestRecentFurniResync, +} from "./recent-furni-resync"; + +describe("requestRecentFurniResync", () => { + it("posts to the fixed seven-day resync endpoint and normalizes the result", async () => { + const fetcher = vi.fn(async () => + Response.json({ + ok: true, + mode: "days", + days: 7, + examined: 4, + resynced: 3, + failed: 1, + rconOk: false, + errors: [{ classname: "chair", message: "invalid entry" }], + }), + ); + + await expect(requestRecentFurniResync(fetcher)).resolves.toEqual({ + examined: 4, + resynced: 3, + failed: 1, + rconOk: false, + errors: [{ classname: "chair", message: "invalid entry" }], + }); + expect(RECENT_FURNI_RESYNC_URL).toBe( + "/api/admin/import/furni/resync?days=7", + ); + expect(fetcher).toHaveBeenCalledWith(RECENT_FURNI_RESYNC_URL, { + method: "POST", + }); + }); + + it("throws the API error when the request fails", async () => { + const fetcher = vi.fn(async () => + Response.json({ error: "Forbidden" }, { status: 403 }), + ); + + await expect(requestRecentFurniResync(fetcher)).rejects.toThrow( + "Forbidden", + ); + }); +}); +``` + +- [ ] **Step 2: Run the tests and verify RED** + +Run: + +```powershell +pnpm exec vitest run --coverage=false src/lib/admin/recent-furni-resync.test.ts +``` + +Expected: FAIL because `recent-furni-resync.ts` does not exist. + +- [ ] **Step 3: Implement the minimal typed request helper** + +Create `src/lib/admin/recent-furni-resync.ts`: + +```ts +export const RECENT_FURNI_RESYNC_URL = + "/api/admin/import/furni/resync?days=7"; + +export interface RecentFurniResyncError { + classname: string; + message: string; +} + +export interface RecentFurniResyncResult { + examined: number; + resynced: number; + failed: number; + rconOk: boolean; + errors: RecentFurniResyncError[]; +} + +type AdminFetcher = ( + input: RequestInfo | URL, + init?: RequestInit, +) => Promise; + +export async function requestRecentFurniResync( + fetcher: AdminFetcher, +): Promise { + const response = await fetcher(RECENT_FURNI_RESYNC_URL, { method: "POST" }); + const payload = (await response.json()) as Record; + if (!response.ok) { + throw new Error( + typeof payload.error === "string" ? payload.error : "Furni resync failed", + ); + } + + const errors = Array.isArray(payload.errors) + ? payload.errors.filter( + (value): value is RecentFurniResyncError => + typeof value === "object" && + value !== null && + typeof (value as RecentFurniResyncError).classname === "string" && + typeof (value as RecentFurniResyncError).message === "string", + ) + : []; + + return { + examined: Number(payload.examined) || 0, + resynced: Number(payload.resynced) || 0, + failed: Number(payload.failed) || 0, + rconOk: payload.rconOk === true, + errors, + }; +} +``` + +- [ ] **Step 4: Run focused tests and verify GREEN** + +Run: + +```powershell +pnpm exec vitest run --coverage=false src/lib/admin/recent-furni-resync.test.ts +pnpm exec biome check --write src/lib/admin/recent-furni-resync.ts src/lib/admin/recent-furni-resync.test.ts +``` + +Expected: 2 tests pass and Biome reports no remaining errors. + +- [ ] **Step 5: Commit the tested contract** + +```powershell +git add -- src/lib/admin/recent-furni-resync.ts src/lib/admin/recent-furni-resync.test.ts +git commit -m "feat: add recent furni resync client contract" +``` + +### Task 2: Import Furni Tools action and result UI + +**Files:** +- Modify: `src/app/admin/import/furni/import-furni-client.tsx` +- Consume: `src/lib/admin/recent-furni-resync.ts` + +**Interfaces:** +- Consumes: `requestRecentFurniResync(adminFetch): Promise`. +- Produces: a `Tools → Update imported furni` action, confirmation dialog, loading state, and dismissible result panel. + +- [ ] **Step 1: Add state and the guarded request handler** + +Import `DatabaseZap`, `RecentFurniResyncResult`, and +`requestRecentFurniResync`. Add state alongside the existing reorganization +state: + +```ts +const [confirmRecentResync, setConfirmRecentResync] = useState(false); +const [recentResyncing, setRecentResyncing] = useState(false); +const [recentResyncResult, setRecentResyncResult] = + useState(null); +``` + +Add the handler near `startReorganize`: + +```ts +async function resyncRecentImports() { + setConfirmRecentResync(false); + setRecentResyncing(true); + setRecentResyncResult(null); + try { + const result = await requestRecentFurniResync(adminFetch); + setRecentResyncResult(result); + if (result.examined === 0) { + toast.info("No imported furni found in the last 7 days"); + } else if (result.failed > 0 || !result.rconOk) { + toast.warning("Furni resync completed with warnings"); + } else { + toast.success(`Updated ${result.resynced} imported furni`); + } + fetchStats(); + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Furni resync failed", + ); + } finally { + setRecentResyncing(false); + } +} +``` + +- [ ] **Step 2: Add the Tools entry and duplicate-submit guard** + +Insert before the Tools separator: + +```tsx + setConfirmRecentResync(true)} + disabled={recentResyncing} +> + {recentResyncing ? ( + + ) : ( + + )} + {recentResyncing ? "Updating imported furni..." : "Update imported furni"} + +``` + +- [ ] **Step 3: Add the confirmation dialog** + +Add a controlled dialog beside the existing batch confirmation dialogs: + +```tsx + + + + Update imported furni? + + This updates FurnitureData for furni imported during the last 7 days, + then refreshes the emulator catalog and item caches. No database rows or + asset files are deleted. + + + + + + + + +``` + +- [ ] **Step 4: Render a dismissible result panel** + +Place the panel after the existing error banner and before batch summaries: + +```tsx +{recentResyncResult && ( +
+
+

Imported Furni Update

+ +
+
+ {recentResyncResult.examined} examined + + {recentResyncResult.resynced} updated + + + {recentResyncResult.failed} failed + + + RCON {recentResyncResult.rconOk ? "refreshed" : "not refreshed"} + +
+ {recentResyncResult.errors.length > 0 && ( +
+ + View errors + +
+ {recentResyncResult.errors.map((error) => ( +

+ {error.classname}: {error.message} +

+ ))} +
+
+ )} +
+)} +``` + +- [ ] **Step 5: Run focused and static verification** + +```powershell +pnpm exec biome check --write src/app/admin/import/furni/import-furni-client.tsx src/lib/admin/recent-furni-resync.ts src/lib/admin/recent-furni-resync.test.ts +pnpm exec vitest run --coverage=false src/lib/admin/recent-furni-resync.test.ts src/lib/services/furni-data-paths.test.ts +pnpm typecheck +``` + +Expected: Biome clean, focused tests pass, and TypeScript exits 0. + +- [ ] **Step 6: Run complete regression verification** + +```powershell +pnpm test +$env:NODE_ENV='production' +$env:DATABASE_URL='mysql://test:test@localhost:3306/test?charset=utf8mb4' +$env:AUTH_SECRET='ci-test-secret-key-that-is-long-enough' +pnpm build +``` + +Expected: all tests pass and the production build exits 0. Redis/database +availability warnings during static generation are acceptable in the local test +environment; compilation or route-generation errors are not. + +- [ ] **Step 7: Commit the UI integration** + +```powershell +git add -- src/app/admin/import/furni/import-furni-client.tsx +git commit -m "feat: add manual recent furni resync action" +``` + +- [ ] **Step 8: Verify the final repository state** + +```powershell +git status --short +git log --oneline origin/main..HEAD +``` + +Expected: clean worktree and the design, plan, tested helper, and UI commits are +ahead of `origin/main`, ready for an explicit push request.