- SUMMARY.md for Plan 02 (ServiceWithTags + 4 new server actions) - STATE.md: advance to Plan 3/4, record metrics and decision - REQUIREMENTS.md: mark OFFER-07/08/09 backend foundation complete
8.9 KiB
phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
| phase | plan | subsystem | tags | requires | provides | affects | tech-stack | key-files | key-decisions | patterns-established | requirements-completed | duration | completed | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 11-catalog-database-view-ux-legacy-consolidation | 02 | api |
|
|
|
|
|
|
|
|
|
12min | 2026-06-13 |
Phase 11 Plan 2: Catalog Query Layer + Inline-Edit/Tag/Quick-Add Server Actions Summary
getAllServices() now returns each service's assigned tag names via a left-join on the new tags table, and four new admin-gated server actions (updateServiceField, addTagToService, removeTagFromService, quickAddService) provide the inline-edit, tag-assignment, and quick-add primitives for the database-view UX.
Performance
- Duration: ~12 min
- Started: 2026-06-13T13:36:00Z
- Completed: 2026-06-13T13:40:07Z
- Tasks: 2 of 2
- Files modified: 2
Accomplishments
getAllServices()rewritten toPromise<ServiceWithTags[]>: left-joinstags(scoped toentity_type="services"viaeq+and), groups rows in-memory into{ ...service, tags: string[] }, ordered byservices.name asc, tags.name asc. Services with zero tags returntags: [](left-join nulls filtered out).getClientFullDetailandgetProjectFullDetail(and theiractiveServices: Service[]fields) verified byte-for-byte unchanged — confirmed viagit diff.- Added 4 new server actions to
src/app/admin/catalog/actions.ts, all callingrequireAdmin()first andrevalidatePath("/admin/catalog")last:updateServiceField(serviceId, fieldName, value)— generic single-field inline edit forname | description | category | unit_price | active, with a closedEDITABLE_FIELDSunion + runtime whitelist check, per-field validation (required name, non-negative price stored with 2 decimals, boolean coercion foractive).addTagToService(serviceId, tagName)/removeTagFromService(serviceId, tagName)— tag add/remove hardcoded toentity_type="services", idempotent insert viaonConflictDoNothing().quickAddService(name)— creates a newservicesrow withunit_price="0.00",active=true, from just a trimmed name (D-12).
npx tsc --noEmitpasses with zero errors across the full project.npx eslinton both modified files: 0 new warnings/errors (6 pre-existing unused-import warnings inadmin-queries.ts, unrelated to this plan's changes, confirmed present before this plan too).
Task Commits
Each task was committed atomically:
- Task 1: Extend getAllServices() with tags join (ServiceWithTags) -
f743410(feat) - Task 2: Add inline-edit, tag, and quick-add server actions -
445de85(feat)
Plan metadata: (this commit, following SUMMARY)
Files Created/Modified
src/lib/admin-queries.ts- Addedtagsto schema import; addedServiceWithTagstype (Service & { tags: string[] }); rewrotegetAllServices()to left-jointags(scopedentity_type="services") and aggregate per-service tag arrayssrc/app/admin/catalog/actions.ts- Addedtags+andimports; appendedupdateServiceField,addTagToService,removeTagFromService,quickAddServiceserver actions; existingcreateService/updateService/toggleServiceActiveunchanged
Decisions Made
onConflictDoNothing()(no explicittarget) was used as written in the plan — it compiled cleanly under the project's Drizzle version and is correct given the single unique index (tags_entity_name_unique) on thetagstable. No change to the plan's suggested fallback was needed.
Deviations from Plan
None - plan executed exactly as written.
Issues Encountered
None.
DB-state note (carried from Plan 01, non-blocking for this plan)
The tags table exists only in src/db/schema.ts (committed in Plan 01) — it has not yet been physically created in the live dev/prod Postgres database (firewalled, pending manual SSH+docker exec migration apply by the user). This plan is code-only and verified via TypeScript types (npx tsc --noEmit passes against the Drizzle-inferred tags/services schema types). No live-DB queries were attempted or required for this plan's verification — getAllServices(), addTagToService, removeTagFromService etc. will function correctly once the Plan 01 migration (scripts/push-11-tags-migration.ts) is applied to the live DB. This is the same pending checkpoint documented in 11-01-SUMMARY.md and STATE.md Blockers — not a new issue introduced here.
Known Stubs
None - no UI or data-rendering stubs introduced by this plan (query layer + server actions only, consumed by Plans 03/04).
Threat Flags
None - this plan's new surface (updateServiceField, addTagToService, removeTagFromService, quickAddService) is fully covered by the threat model already documented in 11-02-PLAN.md (T-11-06 through T-11-10), all mitigated/accepted as designed:
- T-11-06 (Elevation of Privilege): every new action calls
await requireAdmin()first. - T-11-07 (Tampering via field whitelist):
EDITABLE_FIELDSclosed union + runtime.includes()check prevents arbitrary column writes. - T-11-08 (Tampering via entity_type):
entity_typehardcoded to"services"literal in both tag actions, never client-supplied. - T-11-09 (SQL injection): all values pass through Drizzle's parameterized query builder.
- T-11-10 (DoS via duplicate tags):
onConflictDoNothing()on the(entity_type, entity_id, name)unique index makesaddTagToServiceidempotent.
No new endpoints, auth paths, or trust-boundary changes beyond what was already reviewed in the plan's threat model.
User Setup Required
None - no new environment variables or external service configuration needed. (The pending tags table migration apply is tracked from Plan 01, not new to this plan.)
Next Phase Readiness
- Query layer ready:
ServiceWithTagstype +getAllServices()are implemented and typecheck cleanly — Plan 03 (EditableCell/TagMultiSelectcomponents) and Plan 04 (ServiceTablerewrite) can importServiceWithTagsdirectly with no further query-layer changes needed. - Server actions ready:
updateServiceField,addTagToService,removeTagFromService,quickAddServiceare implemented, admin-gated, and exported — Plans 03/04 can wire these into UI components immediately. - NOT ready for live runtime verification: as with Plan 01, any runtime/integration test that queries the live
tagstable (e.g., confirmingaddTagToServiceactually persists a row) will fail until the Plan 01 migration is applied to the database. This does not block Plan 02's code-level completion (typecheck is the success bar per the DB-state note above). - Recommendation: Resolve the Plan 01 DB-execution checkpoint (apply
scripts/push-11-tags-migration.tsvia SSH+docker exec) before or in parallel with Plans 03/04, so that UI built against these actions can be verified end-to-end once ready.
Phase: 11-catalog-database-view-ux-legacy-consolidation Completed: 2026-06-13