Compare commits

...
6 Commits
Author SHA1 Message Date
ginnoir 02b6ec6adb test: cover all quick-add create dialogs in e2e
CI / checks (push) Failing after 1m56s
CI / build (push) Successful in 3m42s
Add data-driven FAB tests for all ten quick-add actions plus cmd-k new event.
2026-07-04 11:53:07 -05:00
ginnoir 68a573c4d6 fix: quick add opens create ui
Add createKey to quick-add manifests and client create dialog host.

FAB and command palette open in-place create UIs instead of navigating away.
2026-07-04 11:47:35 -05:00
ginnoir 76f68548b2 docs: complete Phase 9 backlog triage (issues map, briefs, ADRs)
Map design IDs to Gitea #1-#37, add tasks 80-88, stub ADRs 0004-0006,
and point STATUS at the P1 batch order.
2026-07-04 01:23:30 -05:00
ginnoir 5dfc8b8034 chore: ignore .worktrees for agent isolation 2026-07-03 23:50:43 -05:00
ginnoir 310ba48e33 docs: add backlog triage implementation plan
Executable steps for Gitea issues, P1 task briefs 80-88,
ADR stubs, and STATUS Phase 9 updates.
2026-07-03 22:31:10 -05:00
ginnoir 72e868dda1 docs: add post-v0.1 backlog triage design
Capture approved issue map, P1 batch order, E2E plans,
and implementation-pass scope for the next work batch.
2026-07-03 22:26:35 -05:00
44 changed files with 2843 additions and 23 deletions
+3
View File
@@ -59,3 +59,6 @@ scripts/sync-prod.ps1
# Design handoff bundle (reference only, not committed)
.design-tmp/
# Git worktrees (local agent isolation)
.worktrees/
+344
View File
@@ -0,0 +1,344 @@
# famapp Architecture Diagram
## System Overview
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ EXTERNAL LAYER │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Browser │ │ Mobile PWA │ │ Share Link │ │
│ │ (Next.js) │ │ (Installed) │ │ (Public) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └────────────────────────┴────────────────────────┘ │
│ │ │
│ HTTPS (Caddy Reverse Proxy) │
│ │ │
│ fam.ginnoir.com → :3000 │
│ auth.ginnoir.com → :9000 │
└──────────────────────────────────┼───────────────────────────────────────────┘
┌──────────────────────────────────┼───────────────────────────────────────────┐
│ APPLICATION LAYER │
├──────────────────────────────────┼───────────────────────────────────────────┤
│ │ │
│ ┌────────────────────────┴────────────────────────┐ │
│ │ Next.js 15 App Router │ │
│ │ (Node 24 LTS Container) │ │
│ └────────────────────────┬────────────────────────┘ │
│ │ │
│ ┌───────────────────────────────┼───────────────────────────────┐ │
│ │ │ │ │
│ │ ┌───────────────────────────┐│ ┌───────────────────────────┐│ │
│ │ │ Middleware ││ │ Auth (NextAuth) ││ │
│ │ │ - Rate limiting ││ │ - OIDC (Authentik) ││ │
│ │ │ - Session check ││ │ - Session management ││ │
│ │ └───────────┬───────────────┘│ └───────────┬───────────────┘│ │
│ │ │ │ │ │ │
│ │ ┌───────────┴───────────────┐│ ┌───────────┴───────────────┐│ │
│ │ │ Routes (app/) ││ │ Module Registry ││ │
│ │ │ / → /d/{slug} ││ │ - Entity types ││ │
│ │ │ /d/[slug] (dashboard) ││ │ - Dashboard widgets ││ │
│ │ │ /calendar ││ │ - Quick-add actions ││ │
│ │ │ /lists ││ │ - Share handlers ││ │
│ │ │ /notes ││ │ - Search adapters ││ │
│ │ │ /garden ││ │ - Reminder handlers ││ │
│ │ │ /s/[token] (share) ││ │ - Activity renderers ││ │
│ │ │ /settings ││ │ - Toggle hooks ││ │
│ │ │ /api/* ││ └───────────┬───────────────┘│ │
│ │ └───────────┬───────────────┘ │ │ │
│ │ │ │ │ │
│ │ ┌───────────┴───────────────┬────────────────┴───────────────┐│ │
│ │ │ │ ││ │
│ │ │ ┌───────────────────────┴──────────────────────────────┐││ │
│ │ │ │ MODULE SYSTEM (src/modules/) │││ │
│ │ │ │ │││ │
│ │ │ │ ┌────────────────────────────────────────────────┐ │││ │
│ │ │ │ │ _core/ │ │││ │
│ │ │ │ │ - schema.ts (users, households, activity_log, │ │││ │
│ │ │ │ │ share_links, dashboards, reminders, push) │ │││ │
│ │ │ │ │ - registry.ts (module registration) │ │││ │
│ │ │ │ │ - share.ts (share-link service) │ │││ │
│ │ │ │ │ - push.ts (web push notifications) │ │││ │
│ │ │ │ │ - reminders.ts (reminder scheduling) │ │││ │
│ │ │ │ │ - activity.ts (activity logging) │ │││ │
│ │ │ │ │ - themes.ts (theme configuration) │ │││ │
│ │ │ │ └────────────────────────────────────────────────┘ │││ │
│ │ │ │ │││ │
│ │ │ │ ┌────────────────────────────────────────────────┐ │││ │
│ │ │ │ │ calendar/ │ │││ │
│ │ │ │ │ - schema.ts (calendars, calendar_events) │ │││ │
│ │ │ │ │ - server/ (queries, mutations, share-queries) │ │││ │
│ │ │ │ │ - components/ (shared-view, calendar views) │ │││ │
│ │ │ │ │ - manifest.tsx (widgets: upcoming, month) │ │││ │
│ │ │ │ └────────────────────────────────────────────────┘ │││ │
│ │ │ │ │││ │
│ │ │ │ ┌────────────────────────────────────────────────┐ │││ │
│ │ │ │ │ lists/ │ │││ │
│ │ │ │ │ - schema.ts (lists, list_items) │ │││ │
│ │ │ │ │ - server/ (queries, mutations, share-queries) │ │││ │
│ │ │ │ │ - components/ (shared-view, list-widget) │ │││ │
│ │ │ │ │ - manifest.tsx (widget: list items) │ │││ │
│ │ │ │ └────────────────────────────────────────────────┘ │││ │
│ │ │ │ │││ │
│ │ │ │ ┌────────────────────────────────────────────────┐ │││ │
│ │ │ │ │ notes/ │ │││ │
│ │ │ │ │ - schema.ts (notes) │ │││ │
│ │ │ │ │ - server/ (queries, mutations, share-queries) │ │││ │
│ │ │ │ │ - components/ (shared-view) │ │││ │
│ │ │ │ │ - manifest.tsx (widget: notes) │ │││ │
│ │ │ │ └────────────────────────────────────────────────┘ │││ │
│ │ │ │ │││ │
│ │ │ │ ┌────────────────────────────────────────────────┐ │││ │
│ │ │ │ │ garden/ │ │││ │
│ │ │ │ │ - schema.ts (plants, containers, care_logs) │ │││ │
│ │ │ │ │ - server/ (queries, mutations, share-queries) │ │││ │
│ │ │ │ │ - components/ (plant-widget, shared-view) │ │││ │
│ │ │ │ │ - manifest.tsx (widgets: care-due, overview) │ │││ │
│ │ │ │ └────────────────────────────────────────────────┘ │││ │
│ │ │ │ │││ │
│ │ │ │ ┌────────────────────────────────────────────────┐ │││ │
│ │ │ │ │ bangs/ │ │││ │
│ │ │ │ │ - schema.ts (bang_events) │ │││ │
│ │ │ │ │ - server/ (queries) │ │││ │
│ │ │ │ │ - components/ (bang-widget) │ │││ │
│ │ │ │ │ - manifest.tsx (widget: bang counter) │ │││ │
│ │ │ │ └────────────────────────────────────────────────┘ │││ │
│ │ │ │ │││ │
│ │ │ └────────────────────────────────────────────────────┘││ │
│ │ │ ││ │
│ │ └─────────────────────────────────────────────────────────┘│ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Shared Libraries (src/lib/) │ │
│ │ - db.ts (Drizzle client) │ │
│ │ - auth.ts (NextAuth configuration) │ │
│ │ - session.ts (session helpers) │ │
│ │ - dashboard.ts (dashboard helpers) │ │
│ │ - logger.ts (pino logging) │ │
│ │ - minio.ts (S3 client for uploads) │ │
│ │ - rate-limit.ts (rate limiting) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
└───────────────────────────────────────┬─────────────────────────────────────┘
┌───────────────────────────────────────┼─────────────────────────────────────┐
│ DATA LAYER │
├───────────────────────────────────────┼─────────────────────────────────────┤
│ │ │
│ ┌─────────────────────────────┴──────────────────────────┐ │
│ │ Postgres 16 (famapp-db) │ │
│ │ (Dedicated Container) │ │
│ └─────────────────────────────┬──────────────────────────┘ │
│ │ │
│ ┌────────────────────────────────────┼────────────────────────────┐ │
│ │ │ │ │
│ │ Core Tables │ Module Tables │ │
│ │ ┌──────────────────────────────┐ │ ┌──────────────────────┐ │ │
│ │ │ users │ │ │ calendars │ │ │
│ │ │ accounts │ │ │ calendar_events │ │ │
│ │ │ sessions │ │ │ lists │ │ │
│ │ │ verification_tokens │ │ │ list_items │ │ │
│ │ │ households │ │ │ notes │ │ │
│ │ │ household_members │ │ │ garden_plants │ │ │
│ │ │ activity_log │ │ │ garden_containers │ │ │
│ │ │ share_links │ │ │ garden_care_logs │ │ │
│ │ │ dashboards │ │ │ bang_events │ │ │
│ │ │ reminders │ │ └──────────────────────┘ │ │
│ │ │ push_subscriptions │ │ │ │
│ │ │ notifications │ │ │ │
│ │ └──────────────────────────────┘ │ │ │
│ │ │ │ │
│ └────────────────────────────────────┴────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ AUTHENTICATION LAYER │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Authentik (auth.ginnoir.com) │ │
│ │ (Separate Docker Stack) │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ authentik- │ │ authentik- │ │ authentik- │ │ │
│ │ │ server │ │ worker │ │ redis │ │ │
│ │ │ (:9000) │ │ │ │ │ │ │
│ │ └──────┬───────┘ └──────────────┘ └──────┬───────┘ │ │
│ │ │ │ │ │
│ │ └────────────────┬─────────────────┘ │ │
│ │ │ │ │
│ │ ┌───────────────────────┴──────────────────┐ │ │
│ │ │ authentik-db (Postgres 16) │ │ │
│ │ │ (Dedicated Container) │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ │ │ │
│ │ - OIDC Provider for famapp │ │
│ │ - Forward-auth for *arr apps (future) │ │
│ │ - User management │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
## Module System Architecture
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ MODULE REGISTRATION FLOW │
└─────────────────────────────────────────────────────────────────────────────┘
1. Module Registration (src/modules/index.ts)
┌─────────────────────────────────────────────────────────────────────┐
│ import { registerModule } from "./_core/registry"; │
│ import coreManifest from "./_core/manifest"; │
│ import calendarManifest from "./calendar/manifest"; │
│ import listsManifest from "./lists/manifest"; │
│ import notesManifest from "./notes/manifest"; │
│ import gardenManifest from "./garden/manifest"; │
│ import bangsManifest from "./bangs/manifest"; │
│ │
│ registerModule(coreManifest); │
│ registerModule(calendarManifest); │
│ registerModule(listsManifest); │
│ registerModule(notesManifest); │
│ registerModule(gardenManifest); │
│ registerModule(bangsManifest); │
└─────────────────────────────────────────────────────────────────────┘
2. Registry Storage (src/modules/_core/registry.ts)
┌─────────────────────────────────────────────────────────────────────┐
│ const modules = new Map<string, ModuleManifest>(); │
│ const entityTypes = new Map<string, EntityTypeRegistration>(); │
│ const widgets = new Map<string, DashboardWidget>(); │
│ │
│ registerModule() stores: │
│ - Module metadata (id, name, nav) │
│ - Entity types (with share/reminder/search handlers) │
│ - Dashboard widgets (with render functions) │
│ - Quick-add actions │
└─────────────────────────────────────────────────────────────────────┘
3. Module Manifest Structure (src/modules/_core/module.ts)
┌─────────────────────────────────────────────────────────────────────┐
│ ModuleManifest { │
│ id: string; // "calendar", "lists", etc. │
│ name: string; // "Calendar", "Lists", etc. │
│ nav?: { // Navigation entry │
│ href: string; // "/calendar" │
│ label: string; // "Calendar" │
│ icon?: string; // "calendar" │
│ }; │
│ entities: EntityTypeRegistration[]; // Entity type declarations │
│ dashboardWidgets?: DashboardWidget[]; // Widget declarations │
│ quickAdds?: QuickAddAction[]; // Quick-add actions │
│ } │
└─────────────────────────────────────────────────────────────────────┘
4. Entity Type Registration
┌─────────────────────────────────────────────────────────────────────┐
│ EntityTypeRegistration { │
│ type: string; // "calendar.event", "lists.item", etc. │
│ label: { singular, plural }; │
│ share?: { │
│ canShare: boolean; │
│ defaultCapabilities?: string[]; │
│ }; │
│ reminder?: { canRemind: boolean }; │
│ search?: { search: (query, householdId) => SearchResult[] }; │
│ resolveUrl: (id) => string; │
│ canShareEntity?: (id, ctx) => Promise<boolean>; │
│ loadForShare?: (id, ctx) => Promise<unknown>; │
│ renderSharedView?: (data, capabilities, token) => ReactNode; │
│ renderActivity?: (entry) => string; │
│ } │
└─────────────────────────────────────────────────────────────────────┘
5. Dashboard Widget Registration
┌─────────────────────────────────────────────────────────────────────┐
│ DashboardWidget { │
│ id: string; // "calendar.upcoming", "lists.list", etc│
│ title: string; │
│ description: string; │
│ category?: string; // "Calendar", "Lists", etc. │
│ defaultSize: { w, h }; │
│ minSize?: { w, h }; │
│ maxSize?: { w, h }; │
│ defaultPriority: number; // For default dashboard ordering │
│ configSchema: ZodType; // Zod schema for widget config │
│ defaultConfig: unknown; │
│ resolveConfigOptions?: (ctx) => Promise<unknown>; │
│ render: (props) => ReactNode; // Server component render │
│ } │
└─────────────────────────────────────────────────────────────────────┘
```
## Data Flow Patterns
### Share Link Flow
```
User creates share link
→ Module's canShareEntity() checks permissions
→ Core share.ts creates share_links record
→ Returns token: /s/<token>
Public viewer accesses /s/<token>
→ Middleware rate-limits by IP:token prefix
→ Share viewer loads share_links record
→ Module's loadForShare() loads entity data
→ Module's renderSharedView() renders public view
→ Share-link grants scoped read/write per capabilities
```
### Activity Logging Flow
```
Any entity mutation (server action)
→ Core activity.ts logs activity_log entry
→ Includes: entityType, entityId, actorId, action, payload
→ Dashboard activity widget queries activity_log
→ Module's renderActivity() formats entry for display
```
### Reminder Flow
```
Entity with remind_at field (note, event)
→ Module schedules reminder via core reminders.ts
→ reminders table stores: entityType, entityId, fireAt, channel
→ Background worker (planned) fires reminders
→ notify() fans out to: web push, in-app, optional ntfy
```
### Dashboard Widget Flow
```
User configures dashboard layout
→ dashboards.layout stores: [{ widgetId, config, x, y, w, h }]
→ Dashboard page loads layout
→ For each widget: calls module's render() with config
→ Widget component queries data via module's server/ queries
→ Renders widget in grid layout
```
## Key Design Principles
1. **Module Isolation**: Modules only import from `_core` and `lib/` - never from sibling modules
2. **Generic Core Services**: Sharing, reminders, activity log, search work against any registered entity type
3. **Household Scoping**: All entity data scoped to household_id; permissions gated by household membership
4. **Server Actions**: All mutations use Next.js server actions; route handlers only for webhooks/SSE/share viewer
5. **Type Safety**: TypeScript strict mode; Drizzle schema inference; Zod validation for all configs
6. **Extensibility**: Adding new features requires only creating a new module - no core changes needed
7. **PWA First**: Installable PWA with web(push) notifications; no native apps required
8. **Privacy-First Sharing**: Share links are temporary, scoped, and revocable
## Future (Phase 9)
Placeholders only — details land with tasks 8588:
- **`journal/` module** — per-user journal (task 86, Gitea #19)
- **Versioned HTTP API** (`/api/v1/…`) with token auth alongside OIDC session (task 87, Gitea #15)
- **LLM agent chat** — OpenAI-compatible tools over the API (task 88)
+13 -3
View File
@@ -42,9 +42,19 @@ Living progress tracker. Update at the end of each task. Codex and Claude Code b
## Next up
- **Ready to tag v0.1.0.** All phase 17 tasks complete and production wiring verified.
- Run `docs/tasks/09-pre-deploy-checklist.md` before pushing the tag.
- On the server: clone repo to `/srv/famapp`, copy `.env.production.example``/srv/famapp/deploy/.env`, fill in secrets (AUTH_SECRET, DB passwords, AUTHENTIK_SECRET_KEY, VAPID keys, AUTH_OIDC_CLIENT_ID/SECRET), add the Caddyfile snippet, bootstrap Authentik per `deploy/authentik/README.md`, then `docker compose -f deploy/compose.yaml up -d`.
Phase 9 — Post-v0.1 (see `docs/superpowers/specs/2026-07-03-backlog-triage-design.md`, `docs/issues-map.md`).
**Batch order:**
1. Bugs: tasks 8084 (quick-add, dashboard edit, garden count, bangs, back-nav)
2. API foundation: task 87 (+ ADR 0006)
3. Shared rich-text + notes overhaul: task 85 (+ ADR 0004); closes notes mobile overflow
4. Journal: task 86 (+ ADR 0005), including journal API endpoints
5. LLM agent chat: task 88
P2/P3 backlog is filed on Gitea only (no task briefs yet) — see `docs/issues-map.md` designs 79, 1112, 1519.
**How to resume:** Read AGENTS.md / CLAUDE.md / STATUS.md, open the next unchecked task in `docs/tasks/80``88`, stop at acceptance criteria.
## Development login/testing notes
+16
View File
@@ -0,0 +1,16 @@
# 0004 — Rich-text editor library and storage format
Date: 2026-07-03
Status: proposed
## Context
Journal (task 86) and notes overhaul (task 85) need a shared rich-text editor with emoji support. Research and choose an existing library (e.g. TipTap/ProseMirror-family) rather than building one. Gitea: [#8](https://gitea.ginnoir.com/ginnoir/famapp/issues/8) (epic [#7](https://gitea.ginnoir.com/ginnoir/famapp/issues/7)).
## Decision
To be filled when research completes.
## Consequences
To be filled when research completes.
+16
View File
@@ -0,0 +1,16 @@
# 0005 — Journal / mood tracking research
Date: 2026-07-03
Status: proposed
## Context
Journal module (task 86) needs mood multi-select, charts over time, and insights. Research existing libraries/components for mood tracking or journaling and record build-vs-adopt. Gitea: [#20](https://gitea.ginnoir.com/ginnoir/famapp/issues/20) (epic [#19](https://gitea.ginnoir.com/ginnoir/famapp/issues/19)).
## Decision
To be filled when research completes.
## Consequences
To be filled when research completes.
+16
View File
@@ -0,0 +1,16 @@
# 0006 — API auth and LLM agent architecture
Date: 2026-07-03
Status: proposed
## Context
P1 needs a documented API with token auth coexisting with OIDC session auth, plus a provider-agnostic OpenAI-compatible agent that tool-calls the API. Decide MCP vs direct tools. Gitea: [#14](https://gitea.ginnoir.com/ginnoir/famapp/issues/14) (epic [#13](https://gitea.ginnoir.com/ginnoir/famapp/issues/13)). Tasks 87 then 88.
## Decision
To be filled when research completes.
## Consequences
To be filled when research completes.
+3
View File
@@ -26,4 +26,7 @@ What this costs us, what it buys us.
- [0002 — List realtime uses Postgres NOTIFY and SSE](0002-list-sse-notify.md)
- [0003 — Release workflow (commitlint + release-it)](0003-release-workflow.md)
- [0004 — Rich-text editor library and storage format](0004-rich-text-editor.md) (proposed)
- [0005 — Journal / mood tracking research](0005-journal-research.md) (proposed)
- [0006 — API auth and LLM agent architecture](0006-api-llm-agent.md) (proposed)
<!-- END AUTO-GENERATED -->
+56
View File
@@ -0,0 +1,56 @@
# Issue map
Design IDs from `docs/superpowers/specs/2026-07-03-backlog-triage-design.md` → Gitea (`ginnoir/famapp`).
| Design | Gitea | Title | URL |
| --- | --- | --- | --- |
| 1 | 1 | Quick-add opens create UI, not module page | https://gitea.ginnoir.com/ginnoir/famapp/issues/1 |
| 2 | 2 | Dashboard edit mode renders live widgets at true size | https://gitea.ginnoir.com/ginnoir/famapp/issues/2 |
| 3 | 3 | Garden container plant count is wrong | https://gitea.ginnoir.com/ginnoir/famapp/issues/3 |
| 4 | 4 | Bangs are not editable (add edit and delete) | https://gitea.ginnoir.com/ginnoir/famapp/issues/4 |
| 5 | 5 | Notes overflow horizontally on mobile | https://gitea.ginnoir.com/ginnoir/famapp/issues/5 |
| 6 | 6 | Missing back navigation on detail pages (audit + shared affordance) | https://gitea.ginnoir.com/ginnoir/famapp/issues/6 |
| 7 | 28 | Calendar reminders overhaul | https://gitea.ginnoir.com/ginnoir/famapp/issues/28 |
| 8 | 29 | Lists index: inline task add + list property edit | https://gitea.ginnoir.com/ginnoir/famapp/issues/29 |
| 9 | 30 | Comments on lists and tasks | https://gitea.ginnoir.com/ginnoir/famapp/issues/30 |
| 10 | 7 | Notes editor overhaul (shared rich-text component) | https://gitea.ginnoir.com/ginnoir/famapp/issues/7 |
| 10.1 | 8 | Research ADR: rich-text editor library and storage format | https://gitea.ginnoir.com/ginnoir/famapp/issues/8 |
| 10.2 | 9 | Shared rich-text editor component | https://gitea.ginnoir.com/ginnoir/famapp/issues/9 |
| 10.3 | 10 | Wire shared editor into notes create/edit | https://gitea.ginnoir.com/ginnoir/famapp/issues/10 |
| 10.4 | 11 | Render note formatting on all notes surfaces | https://gitea.ginnoir.com/ginnoir/famapp/issues/11 |
| 10.5 | 12 | Fix notes mobile horizontal overflow | https://gitea.ginnoir.com/ginnoir/famapp/issues/12 |
| 11 | 31 | Bang stats dashboard widget | https://gitea.ginnoir.com/ginnoir/famapp/issues/31 |
| 12 | 35 | Appearance / theming options | https://gitea.ginnoir.com/ginnoir/famapp/issues/35 |
| 13 | 19 | Journal module | https://gitea.ginnoir.com/ginnoir/famapp/issues/19 |
| 13.1 | 20 | Research ADR: journal/mood tracking build-vs-adopt | https://gitea.ginnoir.com/ginnoir/famapp/issues/20 |
| 13.2 | 21 | Journal schema + CRUD | https://gitea.ginnoir.com/ginnoir/famapp/issues/21 |
| 13.3 | 22 | Journal index (recent, browse, entry calendar) | https://gitea.ginnoir.com/ginnoir/famapp/issues/22 |
| 13.4 | 23 | Journal entry detail and create | https://gitea.ginnoir.com/ginnoir/famapp/issues/23 |
| 13.5 | 24 | Journal mood tracker view | https://gitea.ginnoir.com/ginnoir/famapp/issues/24 |
| 13.6 | 25 | Journal insights/stats views | https://gitea.ginnoir.com/ginnoir/famapp/issues/25 |
| 13.7 | 26 | Journal E2E happy path | https://gitea.ginnoir.com/ginnoir/famapp/issues/26 |
| 13.8 | 27 | Journal API endpoints | https://gitea.ginnoir.com/ginnoir/famapp/issues/27 |
| 14 | 13 | API + LLM agent chat | https://gitea.ginnoir.com/ginnoir/famapp/issues/13 |
| 14.1 | 14 | Research ADR: API auth, surface shape, and LLM agent architecture | https://gitea.ginnoir.com/ginnoir/famapp/issues/14 |
| 14.2 | 15 | API surface + token auth for existing modules | https://gitea.ginnoir.com/ginnoir/famapp/issues/15 |
| 14.3 | 16 | LLM agent chat UI | https://gitea.ginnoir.com/ginnoir/famapp/issues/16 |
| 14.4 | 17 | Map agent tool-calling to API | https://gitea.ginnoir.com/ginnoir/famapp/issues/17 |
| 14.5 | 18 | Agent smoke tests with mock provider | https://gitea.ginnoir.com/ginnoir/famapp/issues/18 |
| 15 | 32 | Pets module | https://gitea.ginnoir.com/ginnoir/famapp/issues/32 |
| 16 | 33 | Shopping/pantry module | https://gitea.ginnoir.com/ginnoir/famapp/issues/33 |
| 17 | 34 | Backups and exports | https://gitea.ginnoir.com/ginnoir/famapp/issues/34 |
| 18 | 36 | GPS locations for calendar events | https://gitea.ginnoir.com/ginnoir/famapp/issues/36 |
| 19 | 37 | Lists and notes cohesion (research proposal) | https://gitea.ginnoir.com/ginnoir/famapp/issues/37 |
## Dependencies (P1)
| Issue | Depends on |
| --- | --- |
| #5 (notes overflow) | #7 (rich-text epic) |
| #12 (mobile overflow child) | #8 (rich-text research ADR) |
| #19 (journal epic) | #7 (rich-text epic), #15 (API surface) |
| #16 (agent UI) | #15 (API surface) |
| #17 (tool-calling) | #15 (API surface) |
| #27 (journal API) | #15 (API surface) |
Native Gitea issue dependencies API 404s on this instance; dependencies are also noted as comments on the issues above.
@@ -0,0 +1,714 @@
# Backlog Triage 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:** File the full post-v0.1 backlog on Gitea, write P1 task briefs and ADR stubs, and point STATUS at the approved batch order — no application feature code.
**Architecture:** Issue-centric tracking on `gitea.ginnoir.com/ginnoir/famapp`. Design IDs (#1#19) map to real Gitea issue numbers via `docs/issues-map.md`. P1 work is executable from `docs/tasks/80``88`; P2/P3 are issues only. Spec: `docs/superpowers/specs/2026-07-03-backlog-triage-design.md`.
**Tech Stack:** Gitea API / `user-gitea` MCP (`CreateRepoLabel`, `CreateIssue`, `AddIssueDependency`), markdown task briefs, lightweight ADRs.
---
## File map
| Path | Responsibility |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `docs/issues-map.md` | Design ID → Gitea issue number + URL |
| `docs/tasks/80-*.md``88-*.md` | P1 executable briefs |
| `docs/tasks/README.md` | Phase 9 index |
| `docs/decisions/0004-rich-text-editor.md` | ADR stub (full body later) |
| `docs/decisions/0005-journal-research.md` | ADR stub |
| `docs/decisions/0006-api-llm-agent.md` | ADR stub |
| `docs/decisions/README.md` | Index entries for 00040006 |
| `STATUS.md` | Next-up = Phase 9 order + links |
| `ARCHITECTURE.md` | Light placeholders for `/api/v1` and journal (only if already tracked; do not invent diagrams) |
| Gitea issues | Source of truth for backlog items |
**Auth:** Use Gitea MCP (`user-gitea`) or REST with token from Obsidian `claude/Credentials.md` (Gitea section). Header: `Authorization: token <tok>`. Base: `https://gitea.ginnoir.com/api/v1`. Owner/repo: `ginnoir` / `famapp`. **Never commit the token.**
**CreateIssue requirement:** Every issue `body` MUST include `<brief>one-line summary</brief>` (Gitea MCP enforces this).
**Fallback:** If Gitea is unreachable, write each body to `docs/issues-drafts/<slug>.md` and stop after Task 1 with a clear note — do not invent issue numbers.
---
### Task 1: Create labels
**Files:** none (Gitea only)
- [ ] **Step 1: Confirm repo has zero labels**
Run via MCP `gitea_read` / `ListRepoLabels` with `owner=ginnoir`, `repo=famapp`, or:
```powershell
$headers = @{ Authorization = "token <from-vault>" }
Invoke-RestMethod -Uri "https://gitea.ginnoir.com/api/v1/repos/ginnoir/famapp/labels" -Headers $headers
```
Expected: `[]` (or existing labels — skip creates for names that already exist).
- [ ] **Step 2: Create type labels**
For each row, MCP `gitea_write` / `CreateRepoLabel` (`owner=ginnoir`, `repo=famapp`):
| name | color | description |
| ------------- | -------- | -------------------------------- |
| `bug` | `d73a4a` | Something is broken |
| `enhancement` | `a2eeef` | Improvement to existing behavior |
| `feature` | `0e8a16` | New user-facing capability |
| `epic` | `5319e7` | Large multi-issue effort |
| `research` | `fbca04` | Investigation / ADR only |
- [ ] **Step 3: Create priority labels**
| name | color | description |
| ------------- | -------- | ----------- |
| `priority/p1` | `b60205` | Next batch |
| `priority/p2` | `ff9f1c` | After P1 |
| `priority/p3` | `cfd3d7` | Backlog |
- [ ] **Step 4: Record label IDs**
List labels again. Write a temporary note (or keep in shell) mapping name → `id` (int). All later `CreateIssue` calls pass `labels: [typeId, priorityId]` as **IDs**, not names.
---
### Task 2: File P1 bug issues (design #1#6)
**Files:**
- Create: `docs/issues-map.md` (start the table)
Create issues **in this order** so mapping is predictable. After each create, append a row to `docs/issues-map.md`:
```markdown
# Issue map
Design IDs from `docs/superpowers/specs/2026-07-03-backlog-triage-design.md` → Gitea.
| Design | Gitea | Title | URL |
| ------ | ----- | ------------------------------------------ | ------------------------------------------------- |
| 1 | N | Quick-add opens create UI, not module page | https://gitea.ginnoir.com/ginnoir/famapp/issues/N |
```
Labels for all six: `bug` + `priority/p1`.
- [ ] **Step 1: Create design #1 — Quick-add**
`CreateIssue` title: `Quick-add opens create UI, not module page`
Body:
```markdown
<brief>Quick-add actions must open create dialogs/sheets, not only navigate to module pages.</brief>
## Background / current behavior
Every quick-add entry (FAB sheet and cmd-k palette) currently navigates to its module page (e.g. "New event" → `/calendar`). Task 21 intentionally used `url` navigation as a placeholder; inline create was deferred.
Relevant code: `src/modules/_core/module.ts` (`QuickAddAction`), module manifests (`quickAdds`), `QuickAddSheet` / `CommandPalette` under `src/components/`.
## Expected behavior
Each quick-add opens the **creation UI** for that entity (dialog/sheet) from wherever the button lives, without requiring a full page navigation first. Navigation-only is not acceptable.
## Acceptance criteria
- [ ] "New event" opens the event create dialog/sheet with create controls visible.
- [ ] List/note/garden (and other registered) quick-adds open their create UI the same way.
- [ ] Cmd-k and FAB sheet behave identically for the same action.
- [ ] Playwright: extend `tests/e2e/dashboard.spec.ts` and/or add `tests/e2e/quick-add.spec.ts` — open FAB → action opens dialog/sheet, not only a route change.
## Priority
P1 (batch step 1). Task brief: `docs/tasks/80-quick-add-create-ui.md`.
```
- [ ] **Step 2: Create design #2 — Dashboard edit live widgets**
Title: `Dashboard edit mode renders live widgets at true size`
Body:
```markdown
<brief>Dashboard edit mode must render real widget content at final size, not empty placeholders.</brief>
## Background / current behavior
In edit mode (`?edit=1`), widgets render as generic placeholders at the dragged size. On save, widgets like Recent Activity expand to fit content and the dashboard reflows. What you arrange is not what you get.
Relevant: `DashboardEditor`, react-grid-layout usage, dashboard page under `src/app/d/`.
## Expected behavior
Edit mode live-loads each widget's real data and renders at true final size so layout matches the saved view.
## Acceptance criteria
- [ ] Edit mode shows real widget content (not empty shells) for registered widgets.
- [ ] Saving layout does not cause a large content-driven reflow for content-heavy widgets (e.g. Recent activity).
- [ ] Playwright: extend `tests/e2e/dashboard.spec.ts` — enter edit mode, assert real content visible (e.g. Recent activity). Prefer height-stability assertion only if non-flaky.
## Priority
P1 (batch step 2). Task brief: `docs/tasks/81-dashboard-edit-live-widgets.md`.
```
- [ ] **Step 3: Create design #3 — Garden plant count**
Title: `Garden container plant count is wrong`
Body:
```markdown
<brief>Container shows "0 plants" despite containing a plant; fix count query and audit siblings.</brief>
## Background / current behavior
A garden container (e.g. "plantie cabinet") shows "0 plants" when it contains at least one plant. Likely a query/join or container-association bug.
Relevant: `src/modules/garden/server/queries.ts`, container list/detail components.
## Expected behavior
Container plant counts match the number of plants associated with that container. Any other count displays sharing the same bug are fixed too.
## Acceptance criteria
- [ ] Container with N plants displays N (not 0).
- [ ] Audit other garden counts (lists, widgets) for the same pattern; fix or file follow-ups.
- [ ] Playwright: extend `tests/e2e/garden.spec.ts` — plant in container → count ≥ 1.
## Priority
P1 (batch step 3). Task brief: `docs/tasks/82-garden-plant-count.md`.
```
- [ ] **Step 4: Create design #4 — Bang edit/delete**
Title: `Bangs are not editable (add edit and delete)`
Body:
```markdown
<brief>Bang entries cannot be corrected after creation; add edit and delete.</brief>
## Background / current behavior
Bang events can be recorded but not corrected (e.g. wrong date). Only create exists in `src/modules/bangs/server/actions.ts` and the widget UI.
## Expected behavior
Users can edit (at least date/fields present today) and delete bang entries.
## Acceptance criteria
- [ ] Edit a bang (e.g. change date) and see the update in the widget/list.
- [ ] Delete a bang and it no longer appears.
- [ ] Activity log entries for update/delete if other modules log similarly.
- [ ] Playwright: new `tests/e2e/bangs.spec.ts` — record → edit date → assert → delete → assert gone.
## Priority
P1 (batch step 4). Task brief: `docs/tasks/83-bang-edit-delete.md`. Related (not blocking): bang stats widget (design #11).
```
- [ ] **Step 5: Create design #5 — Notes mobile overflow**
Title: `Notes overflow horizontally on mobile`
Body:
```markdown
<brief>Note content scrolls sideways on small viewports instead of wrapping.</brief>
## Background / current behavior
On mobile, note content overflows horizontally. Fix belongs with the shared rich-text / notes overhaul (design #10) so formatting and overflow are solved together.
## Expected behavior
Note content wraps; no horizontal page scroll from note body on small viewports.
## Acceptance criteria
- [ ] Long / wide note content at 375px width does not cause horizontal overflow.
- [ ] Playwright: extend `tests/e2e/notes.spec.ts``scrollWidth` within viewport tolerance (same pattern as dashboard mobile check).
- [ ] Closed together with design #10 / task `85-rich-text-notes.md`.
## Priority
P1 (batch step 7, with #10). **Blocked by** design #10 (rich-text notes overhaul).
```
- [ ] **Step 6: Create design #6 — Back navigation**
Title: `Missing back navigation on detail pages (audit + shared affordance)`
Body:
```markdown
<brief>Audit detail routes lacking back affordances; add a shared header/back pattern.</brief>
## Background / current behavior
Individual plant pages (and other detail routes) have no in-page "back" except global nav. Affects many detail pages under `src/app/`.
## Expected behavior
Every detail page has a consistent back control to its parent list/index. Prefer a shared header/back component.
## Acceptance criteria
- [ ] Audit all routes in `src/app/` and list detail pages missing back in the PR description.
- [ ] Shared back affordance applied consistently.
- [ ] Playwright: `tests/e2e/navigation.spec.ts` or per-module — open plant/note/list (and other) detail routes → Back visible and returns to parent.
## Priority
P1 (batch step 5). Task brief: `docs/tasks/84-back-navigation.md`.
```
- [ ] **Step 7: Update `docs/issues-map.md` with all six rows**
---
### Task 3: File P1 epic issues (#10, #13, #14 and children)
Labels: parent epics use `epic` + `priority/p1`. Children use `enhancement` or `feature` or `research` as noted + `priority/p1`.
- [ ] **Step 1: Create design #10 parent — Notes editor / shared rich-text**
Title: `Notes editor overhaul (shared rich-text component)`
Labels: `epic`, `priority/p1`
Body: include `<brief>…</brief>`, checklist of children (research ADR, shared editor, notes wiring, render everywhere, mobile overflow), links to task `85` and ADR `0004`, note that #5 closes with this epic.
- [ ] **Step 2: Create #10 children** (five issues), titles:
1. `Research ADR: rich-text editor library and storage format` — labels `research`, `priority/p1`
2. `Shared rich-text editor component``enhancement`, `priority/p1`
3. `Wire shared editor into notes create/edit``enhancement`, `priority/p1`
4. `Render note formatting on all notes surfaces``enhancement`, `priority/p1`
5. `Fix notes mobile horizontal overflow``bug`, `priority/p1` (can reference design #5; may close #5 when done)
Each body: brief, parent epic link (fill Gitea # after parent exists), acceptance criteria one-liners, task `85` / ADR `0004`.
- [ ] **Step 3: Create design #14 parent — API + LLM agent chat**
Title: `API + LLM agent chat`
Labels: `epic`, `priority/p1`
Body: checklist — ADR → API surface for existing modules → (pause for editor/journal) → agent UI → tool-calling → smoke tests. Task briefs `87`, `88`. ADR `0006`. API early, agent late.
- [ ] **Step 4: Create #14 children**
1. `Research ADR: API auth, surface shape, and LLM agent architecture``research`, `priority/p1`
2. `API surface + token auth for existing modules``feature`, `priority/p1`
3. `LLM agent chat UI``feature`, `priority/p1`
4. `Map agent tool-calling to API``feature`, `priority/p1`
5. `Agent smoke tests with mock provider``enhancement`, `priority/p1`
- [ ] **Step 5: Create design #13 parent — Journal module**
Title: `Journal module`
Labels: `epic`, `priority/p1`
Body: requirements from design (per-user journal, recent entries, browse, calendar of entry days, entry fields, mood tracker, insights, research first). Depends on #10 and API foundation (#14 API child). Task `86`, ADR `0005`.
- [ ] **Step 6: Create #13 children**
1. `Research ADR: journal/mood tracking build-vs-adopt``research`, `priority/p1`
2. `Journal schema + CRUD``feature`, `priority/p1`
3. `Journal index (recent, browse, entry calendar)``feature`, `priority/p1`
4. `Journal entry detail and create``feature`, `priority/p1`
5. `Journal mood tracker view``feature`, `priority/p1`
6. `Journal insights/stats views``feature`, `priority/p1`
7. `Journal E2E happy path``enhancement`, `priority/p1`
8. `Journal API endpoints``feature`, `priority/p1`
- [ ] **Step 7: Wire dependencies** (`AddIssueDependency`)
| Issue (index) | Depends on (depends_on_id) |
| ---------------------------------- | ---------------------------------------- |
| Design #5 (notes overflow) | Design #10 parent |
| Design #10 child "mobile overflow" | Design #10 research ADR child (optional) |
| Design #13 parent | Design #10 parent |
| Design #13 parent | Design #14 API surface child |
| Design #14 agent UI child | Design #14 API surface child |
| Design #14 tool-calling child | Design #14 API surface child |
| Design #13 journal API child | Design #14 API surface child |
Use real Gitea indexes from `docs/issues-map.md`.
- [ ] **Step 8: Update `docs/issues-map.md`** with all epic + child rows (use design IDs like `10`, `10.1`, `13.2`, `14.1`).
---
### Task 4: File P2 and P3 issues
- [ ] **Step 1: P2 enhancements/features** (labels as in design)
| Design | Title | Labels |
| ------ | ------------------------------------------------- | ---------------------------- |
| 7 | Calendar reminders overhaul | `enhancement`, `priority/p2` |
| 8 | Lists index: inline task add + list property edit | `enhancement`, `priority/p2` |
| 9 | Comments on lists and tasks | `feature`, `priority/p2` |
| 11 | Bang stats dashboard widget | `enhancement`, `priority/p2` |
Each body: brief, background, expected, acceptance criteria (high level), note "full `docs/tasks/` when P2 batch starts", related links (#7 → task 41; #11 related to #4; #9 related to #19).
- [ ] **Step 2: P2 epics with light checklists only** (no child issues)
| Design | Title |
| ------ | ---------------------- |
| 15 | Pets module |
| 16 | Shopping/pantry module |
| 17 | Backups & exports |
Labels: `epic`, `priority/p2`. Body includes a markdown checklist of future sub-work (not filed as issues yet). #17 references `docs/tasks/60-backups.md`.
- [ ] **Step 3: P3**
| Design | Title | Labels |
| ------ | ------------------------------------------ | ------------------------- |
| 12 | Appearance / theming options | `epic`, `priority/p3` |
| 18 | GPS locations for calendar events | `research`, `priority/p3` |
| 19 | Lists ↔ notes cohesion (research proposal) | `research`, `priority/p3` |
- [ ] **Step 4: Finish `docs/issues-map.md`** for designs 79, 1112, 1519.
- [ ] **Step 5: Commit map**
```bash
git add docs/issues-map.md
git commit -m "docs: map design backlog IDs to Gitea issues"
```
If only drafts were written under `docs/issues-drafts/`, commit those instead and stop — do not invent Gitea numbers.
---
### Task 5: Write bug task briefs (8084)
**Files:**
- Create: `docs/tasks/80-quick-add-create-ui.md`
- Create: `docs/tasks/81-dashboard-edit-live-widgets.md`
- Create: `docs/tasks/82-garden-plant-count.md`
- Create: `docs/tasks/83-bang-edit-delete.md`
- Create: `docs/tasks/84-back-navigation.md`
Each file uses the standard sections: Goal, Why, Depends on, Scope, Out of scope, Acceptance criteria, Notes.
- [ ] **Step 1: Write `80-quick-add-create-ui.md`**
```markdown
# 80 — Quick-add opens create UI
## Goal
Make every quick-add action open the entity's creation dialog/sheet instead of only navigating to a module page.
## Why
Task 21 shipped URL navigation as a placeholder. Users expect "New event" to start creating an event immediately.
## Depends on
- 21 (quick-add registry)
## Scope
- Extend `QuickAddAction` / client handling so actions can open create UI (dialog/sheet) in-place.
- Wire calendar, lists, notes, garden, bangs (and any other registered) quick-adds to their create UIs.
- Keep FAB sheet and cmd-k behavior identical per action.
- Playwright coverage per design #1 / Gitea issue (see `docs/issues-map.md`).
## Out of scope
- Fuzzy search across entities.
- New entity types.
## Acceptance criteria
- [ ] Each registered quick-add opens create UI with create controls visible.
- [ ] No quick-add is navigation-only unless explicitly documented as view-only (none in v1 set).
- [ ] E2E: FAB → action opens dialog/sheet (not only route change).
## Notes
- Gitea: design #1 (URL in `docs/issues-map.md`).
- Prefer reusing existing create dialogs/sheets over new ones.
```
Fill Gitea issue number into Notes after map exists (replace "design #1" with `ginnoir/famapp#N`).
- [ ] **Step 2: Write `81-dashboard-edit-live-widgets.md`**
Same structure: Goal = edit mode renders live widgets at true size; Depends on 26; Scope = DashboardEditor loads real widget components/data; Acceptance = real content in edit mode + E2E; Notes → design #2.
- [ ] **Step 3: Write `82-garden-plant-count.md`**
Goal = fix container plant count; Depends on 71/72; Scope = find and fix query/join, audit sibling counts; E2E garden.spec; Notes → design #3.
- [ ] **Step 4: Write `83-bang-edit-delete.md`**
Goal = edit + delete bangs; Depends on bangs module (no prior task number — note "bangs module in tree"); new `bangs.spec.ts`; Notes → design #4.
- [ ] **Step 5: Write `84-back-navigation.md`**
Goal = shared back affordance on all detail pages; Scope = audit `src/app/`, shared component, apply consistently; E2E navigation; Notes → design #6.
- [ ] **Step 6: Commit**
```bash
git add docs/tasks/80-quick-add-create-ui.md docs/tasks/81-dashboard-edit-live-widgets.md docs/tasks/82-garden-plant-count.md docs/tasks/83-bang-edit-delete.md docs/tasks/84-back-navigation.md
git commit -m "docs: add P1 bug task briefs 80-84"
```
---
### Task 6: Write feature task briefs (8588)
**Files:**
- Create: `docs/tasks/85-rich-text-notes.md`
- Create: `docs/tasks/86-journal-module.md`
- Create: `docs/tasks/87-api-surface.md`
- Create: `docs/tasks/88-llm-agent-chat.md`
- [ ] **Step 1: Write `85-rich-text-notes.md`**
Include: Goal = shared rich-text editor + full notes overhaul + mobile overflow; Depends on 12, 8084 not required but batch order is bugs first; Scope = ADR 0004, shared component, notes create/edit, render on index/widgets/share, fix overflow (closes design #5); Out of scope = journal (86); Acceptance includes E2E notes formatting + mobile overflow; Notes link design #10 and #5.
- [ ] **Step 2: Write `86-journal-module.md`**
Include full journal requirements from the design spec (recent entries, browse, calendar, fields, mood multi-select, pills boolean, mood tracker, insights, research ADR 0005, journal API endpoints). Depends on 85 and 87. E2E `journal.spec.ts`.
- [ ] **Step 3: Write `87-api-surface.md`**
Goal = documented API + token auth for existing modules (calendar, lists, notes, garden, bangs). Depends on ADR 0006 (research child of #14). Coexistence with OIDC session. Additive versioning. Vitest for auth/handlers. Batch step 6 (after bugs).
- [ ] **Step 4: Write `88-llm-agent-chat.md`**
Goal = provider-agnostic OpenAI-compatible agent chat with tool-calling to API. Depends on 87. Mock provider in CI. No live LLM in CI.
- [ ] **Step 5: Commit**
```bash
git add docs/tasks/85-rich-text-notes.md docs/tasks/86-journal-module.md docs/tasks/87-api-surface.md docs/tasks/88-llm-agent-chat.md
git commit -m "docs: add P1 feature task briefs 85-88"
```
---
### Task 7: Update `docs/tasks/README.md`
**Files:**
- Modify: `docs/tasks/README.md`
- [ ] **Step 1: Append Phase 9 section** after Phase 8:
```markdown
### Phase 9 — Post-v0.1
Batch order (bugs → API → editor/notes → journal → agent). Design: [`docs/superpowers/specs/2026-07-03-backlog-triage-design.md`](../superpowers/specs/2026-07-03-backlog-triage-design.md). Issue map: [`docs/issues-map.md`](../issues-map.md).
- [80 — Quick-add create UI](80-quick-add-create-ui.md)
- [81 — Dashboard edit live widgets](81-dashboard-edit-live-widgets.md)
- [82 — Garden plant count](82-garden-plant-count.md)
- [83 — Bang edit/delete](83-bang-edit-delete.md)
- [84 — Back navigation](84-back-navigation.md)
- [85 — Rich-text notes overhaul](85-rich-text-notes.md)
- [86 — Journal module](86-journal-module.md)
- [87 — API surface](87-api-surface.md)
- [88 — LLM agent chat](88-llm-agent-chat.md)
```
- [ ] **Step 2: Commit**
```bash
git add docs/tasks/README.md
git commit -m "docs: index Phase 9 post-v0.1 task briefs"
```
---
### Task 8: Stub ADRs 00040006
**Files:**
- Create: `docs/decisions/0004-rich-text-editor.md`
- Create: `docs/decisions/0005-journal-research.md`
- Create: `docs/decisions/0006-api-llm-agent.md`
- Modify: `docs/decisions/README.md` (index entries)
Each stub uses ADR format with `Status: proposed` and a short Context that points at the Gitea research issue and task brief. Decision/Consequences sections state: "To be filled when research completes."
Example for 0004:
```markdown
# 0004 — Rich-text editor library and storage format
Date: 2026-07-03
Status: proposed
## Context
Journal (task 86) and notes overhaul (task 85) need a shared rich-text editor with emoji support. Research and choose an existing library (e.g. TipTap/ProseMirror-family) rather than building one. Gitea: design #10 research child (see `docs/issues-map.md`).
## Decision
To be filled when research completes.
## Consequences
To be filled when research completes.
```
Mirror for 0005 (journal build-vs-adopt) and 0006 (API auth + LLM agent architecture).
- [ ] **Step 1: Write three stubs**
- [ ] **Step 2: Add index lines** in `docs/decisions/README.md` under the AUTO-GENERATED block (or after it if the generator would wipe them — prefer manual entries matching existing style):
```markdown
- [0004 — Rich-text editor library and storage format](0004-rich-text-editor.md) (proposed)
- [0005 — Journal / mood tracking research](0005-journal-research.md) (proposed)
- [0006 — API auth and LLM agent architecture](0006-api-llm-agent.md) (proposed)
```
- [ ] **Step 3: Commit**
```bash
git add docs/decisions/0004-rich-text-editor.md docs/decisions/0005-journal-research.md docs/decisions/0006-api-llm-agent.md docs/decisions/README.md
git commit -m "docs: stub ADRs 0004-0006 for P1 cross-cutting choices"
```
---
### Task 9: Update STATUS.md
**Files:**
- Modify: `STATUS.md`
- [ ] **Step 1: Replace "Next up" section** with:
```markdown
## Next up
Phase 9 — Post-v0.1 (see `docs/superpowers/specs/2026-07-03-backlog-triage-design.md`, `docs/issues-map.md`).
**Batch order:**
1. Bugs: tasks 8084 (quick-add, dashboard edit, garden count, bangs, back-nav)
2. API foundation: task 87 (+ ADR 0006)
3. Shared rich-text + notes overhaul: task 85 (+ ADR 0004); closes notes mobile overflow
4. Journal: task 86 (+ ADR 0005), including journal API endpoints
5. LLM agent chat: task 88
P2/P3 backlog is filed on Gitea only (no task briefs yet).
**How to resume:** Read AGENTS.md / CLAUDE.md / STATUS.md, open the next unchecked task in `docs/tasks/80``88`, stop at acceptance criteria.
```
- [ ] **Step 2: Commit**
```bash
git add STATUS.md
git commit -m "docs: point STATUS at Phase 9 backlog batch order"
```
---
### Task 10: Light ARCHITECTURE.md touch
**Files:**
- Modify: `ARCHITECTURE.md` only if it is already part of the repo tracking story (it may be untracked — if untracked, add a minimal "Future" note under module list, or skip if the file is incomplete/unrelated). Prefer: add two bullets under module system / routes:
- Future: `journal/` module (task 86)
- Future: versioned HTTP API (`/api/v1/…`) with token auth alongside OIDC session (task 87)
Do **not** invent detailed diagrams.
- [ ] **Step 1: Apply minimal placeholders or skip with a note in the commit message if ARCHITECTURE.md is not ready**
- [ ] **Step 2: Commit if changed**
```bash
git add ARCHITECTURE.md
git commit -m "docs: note future journal module and versioned API in architecture"
```
---
### Task 11: Vault write-back
**Files:**
- Modify: `C:\Users\MattC\Documents\Obsidian Vault\Famapp\_Claude.md`
- [ ] **Step 1: Append a session block**
```markdown
## 2026-07-03 Backlog triage
- Design: `docs/superpowers/specs/2026-07-03-backlog-triage-design.md`
- Plan: `docs/superpowers/plans/2026-07-03-backlog-triage.md`
- Issue map: `docs/issues-map.md` (Gitea `ginnoir/famapp`)
- P1 order: bugs 8084 → API 87 → rich-text notes 85 → journal 86 → agent 88
- P2/P3: Gitea issues only until their batch
```
- [ ] **Step 2: Do not commit vault files into famapp git**
---
### Task 12: Final verification
- [ ] **Step 1: Verify Gitea**
Open `https://gitea.ginnoir.com/ginnoir/famapp/issues` — confirm open issues exist for all design IDs in `docs/issues-map.md`, labels present, dependencies on #5/#13/#14 children look correct.
- [ ] **Step 2: Verify repo files**
```bash
git status
ls docs/tasks/8*.md
ls docs/decisions/0004*.md docs/decisions/0005*.md docs/decisions/0006*.md
```
Expected: `80``88` present, ADR stubs present, clean working tree (except unrelated untracked files).
- [ ] **Step 3: Cross-link pass**
In each of `80``88`, Notes section contains the real Gitea issue URL or `#N` from `docs/issues-map.md`. Fix and amend only if this commit is still local and was created by this session; otherwise new commit `docs: link task briefs to Gitea issue numbers`.
---
## Self-review (plan vs spec)
| Spec requirement | Plan task |
| ------------------------------- | ---------------------------------------------------- |
| Issues for all 19 items | Tasks 24 |
| Epic children for #10, #13, #14 | Task 3 |
| Light checklists P2/P3 epics | Task 4 |
| Labels + priority | Task 1 |
| P1 task briefs 8088 | Tasks 56 |
| Phase 9 README | Task 7 |
| ADR stubs 00040006 | Task 8 |
| STATUS update | Task 9 |
| ARCHITECTURE light touch | Task 10 |
| E2E plans in issues/briefs | Embedded in issue bodies + brief acceptance criteria |
| No feature code | Entire plan is docs/issues only |
| Fallback drafts | Task 1/4 fallback |
| Vault write-back | Task 11 |
| API early order in STATUS | Task 9 |
No TBD placeholders remain in executable steps. Issue bodies for epic children are specified by title + required sections; executor fills `<brief>` and Gitea parent links using the map.
@@ -0,0 +1,197 @@
# Backlog triage, issues, E2E coverage, and next-batch planning
Date: 2026-07-03
Status: approved (design); implementation pass not started
## Goal
Turn the post-v0.1 backlog into an actionable state: one Gitea issue per item (epics with sub-issues where needed), Playwright regression plans for every bug, P1 `docs/tasks/` specs, ADR placeholders for cross-cutting choices, STATUS/ARCHITECTURE updates, and a prioritized next-batch plan.
## Decisions (interview)
| Topic | Choice |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| Rich text for journal | Pull shared editor ADR + component into **P1**; full notes overhaul in the same batch (editor, render formatting everywhere, mobile overflow). |
| Batch ordering | **Bugs first**, then features. |
| Spec depth | **Full backlog issues now**; full `docs/tasks/` only for P1; P2/P3 get issues + light epic checklists. |
| Organization model | **Issue-centric**: Gitea is source of truth for work items; task briefs are the agent execution format for P1. |
| API timing | **API early**: after bugs, API foundation for existing modules; journal and notes extend it; LLM agent last. |
## Approaches considered
1. **Issue-centric (chosen).** Gitea issues own tracking; P1 task briefs link from issues; ADRs gate cross-cutting work; E2E criteria live on issues and land with PRs.
2. **Task-doc-centric.** `docs/tasks/` primary; Gitea thin stubs. Weaker epic/sub-issue tracking.
3. **Monolithic batch plan.** One mega-issue per priority tier. Fights one-issue-per-item and partial progress.
## Issue map and labels
**Labels:** `bug` | `enhancement` | `feature` | `epic` | `research`
**Priority labels:** `priority/p1` | `priority/p2` | `priority/p3`
| # | Title | Label | Priority | Notes |
| --- | ----------------------------------------------------- | --------------------------- | -------- | ------------------------------------------- |
| 1 | Quick-add opens create UI, not module page | bug | P1 | |
| 2 | Dashboard edit mode renders live widgets at true size | bug | P1 | |
| 3 | Garden container plant count wrong | bug | P1 | Audit sibling counts |
| 4 | Bangs not editable (add edit + delete) | bug | P1 | |
| 5 | Notes overflow horizontally on mobile | bug | P1 | Closed with / blocked by #10 |
| 6 | Missing back navigation — audit + shared affordance | bug | P1 | Sweep all detail routes |
| 7 | Calendar reminders overhaul | enhancement | P2 | Builds on task 41 |
| 8 | Lists index: inline task add + list property edit | enhancement | P2 | |
| 9 | Comments on lists and tasks | feature | P2 | Consider generic comment component |
| 10 | Notes editor overhaul (shared rich-text) | enhancement / epic children | P1 | Pulled into P1; ADR 0004 |
| 11 | Bang stats dashboard widget | enhancement | P2 | Related to #4; not blocking |
| 12 | Appearance / theming options | epic | P3 | Feasibility first |
| 13 | Journal module | epic | P1 | Sub-issues; depends on #10 + API foundation |
| 14 | API + LLM agent chat | epic | P1 | API children early; agent children last |
| 15 | Pets module | epic | P2 | Garden-shaped; light checklist |
| 16 | Shopping/pantry module | epic | P2 | Design before build; light checklist |
| 17 | Backups & exports | epic | P2 | Extends task 60; light checklist |
| 18 | GPS locations for calendar events | research | P3 | Issue only |
| 19 | Lists ↔ notes cohesion | research | P3 | Proposal/ADR later; not code |
### Epic children (file as separate issues)
**#10 Shared rich-text / notes overhaul**
1. Research ADR (library choice, storage format, shared component contract)
2. Shared editor component
3. Notes create/edit wiring
4. Render formatting on all notes surfaces (index, widgets, share view)
5. Mobile overflow fix (closes #5)
**#13 Journal**
1. Research ADR (mood/journal libs; build-vs-adopt)
2. Schema + CRUD
3. Index (recent entries, browse all, entry-day calendar)
4. Entry detail + create (date/time required; all else optional)
5. Mood tracker view
6. Insights/stats views
7. E2E happy path
8. Journal API endpoints (additive on API foundation)
**#14 API + LLM**
1. Architecture ADR (token auth vs OIDC session; API shape; OpenAI-compatible provider; MCP yes/no)
2. API surface + token auth + docs for **existing** modules (calendar, lists, notes, garden, bangs; dashboards as needed)
3. Agent chat UI
4. Tool-calling mapped to API
5. Smoke tests (mock provider in CI)
P2/P3 epics (#12, #1517): epic body holds a light checklist only — no child issues until that batch.
### Issue body template
Every issue includes:
- Background / current behavior
- Expected behavior or feature description
- Acceptance criteria (including E2E where applicable)
- Priority label
- Links: `docs/tasks/…` when present, blocked-by / related issues
## P1 batch order
Strict sequence:
| Step | Work | Issues | Task brief(s) |
| ---- | -------------------------------------------------------------------------------------- | -------------------- | -------------------------------------------------------- |
| 1 | Quick-add opens create dialogs/sheets | #1 | `80-quick-add-create-ui.md` |
| 2 | Dashboard edit mode live widgets | #2 | `81-dashboard-edit-live-widgets.md` |
| 3 | Garden plant count | #3 | `82-garden-plant-count.md` |
| 4 | Bang edit + delete | #4 | `83-bang-edit-delete.md` |
| 5 | Back-nav audit + shared header | #6 | `84-back-navigation.md` |
| 6 | API ADR + token auth + endpoints for existing modules | #14 (API children) | ADR `0006-api-llm-agent.md` + `87-api-surface.md` |
| 7 | Shared rich-text + full notes overhaul (+ mobile overflow); extend notes API if needed | #10, #5 | ADR `0004-rich-text-editor.md` + `85-rich-text-notes.md` |
| 8 | Journal module (UI + journal API in same epic) | #13 (+ children) | ADR `0005-journal-research.md` + `86-journal-module.md` |
| 9 | LLM agent chat | #14 (agent children) | `88-llm-agent-chat.md` |
**Rules**
- API is **additive**; no freeze that blocks journal. New modules document/register endpoints as they ship.
- #14 epic stays open across steps 6 and 9 (API early, agent late).
- #13 depends on #10 (editor) and on API foundation existing.
- #5 closes with #10.
- Task numbering continues after 76 (80+). Do not renumber existing tasks.
- `docs/tasks/README.md` gains **Phase 9 — Post-v0.1**.
- P2/P3: Gitea issues only until their batch; STATUS may list them under Backlog.
## E2E plan
Follow `tests/e2e/*.spec.ts` patterns (dev-login storage state, happy-path assertions). Regression tests land **with each bug/feature PR**, not as a separate upfront suite.
### Bugs
| Bug | Spec strategy |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| Quick-add create UI | Extend `dashboard.spec.ts` and/or `quick-add.spec.ts`: FAB action opens dialog/sheet with create controls, not only navigation. |
| Dashboard edit live size | Extend `dashboard.spec.ts`: `?edit=1` shows real widget content (e.g. Recent activity); prefer stable height assertion only if non-flaky. |
| Garden plant count | Extend `garden.spec.ts`: plant in container → count ≥ 1. |
| Bang edit/delete | New `bangs.spec.ts`: record → edit date → assert → delete → assert gone. |
| Notes mobile overflow | Extend `notes.spec.ts` at 375px: no horizontal overflow; lands with #10. |
| Back navigation | `navigation.spec.ts` or per-module: detail routes expose Back and return to parent. |
### P1 features
| Feature | Coverage |
| ----------------- | --------------------------------------------------------------------------------- |
| API surface | Vitest for auth + route handlers; optional Playwright smoke only if useful. |
| Rich-text / notes | `notes.spec.ts`: formatted note visible on detail + other surfaces. |
| Journal | New `journal.spec.ts`: minimal entry → recent list → detail → mood tracker loads. |
| LLM agent | Smoke with **mock/stub** provider in test env; no live LLM in CI. |
## Docs and ADRs
### P1 task briefs (implementation pass)
| File | Covers |
| ----------------------- | --------------------------------------------------------------------------- |
| `80``84` | One brief per bug (quick-add, dashboard edit, plant count, bangs, back-nav) |
| `85-rich-text-notes.md` | Shared editor + full notes overhaul + mobile overflow |
| `86-journal-module.md` | Journal epic (split later only if execution needs it) |
| `87-api-surface.md` | Token auth + API for existing modules |
| `88-llm-agent-chat.md` | Agent UI + OpenAI-compatible tools |
Format: Goal / Why / Depends on / Scope / Out of scope / Acceptance criteria / Notes. Each brief links its Gitea issue(s).
### ADRs
| ADR | Topic | Gates |
| -------------------------- | --------------------------------------------------------------------- | ------------------- |
| `0004-rich-text-editor.md` | Library (e.g. TipTap), storage format, shared component contract | `85`, journal body |
| `0005-journal-research.md` | Build-vs-adopt for mood/journal UI and charts | `86` implementation |
| `0006-api-llm-agent.md` | Token auth vs OIDC; API shape; OpenAI-compatible client; MCP decision | `87` then `88` |
Triage pass stubs titles in `docs/decisions/README.md` (and stub files pointing at epic issues). Full ADR bodies when that step runs.
P2/P3 research (#18, #19, appearance) remain issue-only until their batch.
### STATUS / ARCHITECTURE
- **STATUS.md:** replace stale “Ready to tag v0.1.0” next-up with Phase 9 batch order, link to this design doc and Gitea issues.
- **ARCHITECTURE.md:** light placeholders only (`/api/v1`, journal module) — no invented detail. Full diagrams when modules land.
## Implementation pass (next execution session)
No application feature code in this pass.
1. Commit this design doc (done when design is accepted).
2. File Gitea issues on `gitea.ginnoir.com/ginnoir/famapp` for all 19 items; full children for epics #10, #13, #14; light checklists on P2/P3 epics #12, #1517. Labels + priority as above. On auth failure: write `docs/issues-drafts/<slug>.md` and report.
3. Write P1 task briefs `80``88` and update `docs/tasks/README.md` Phase 9.
4. Stub ADRs 00040006 (title + pointer to epic issues).
5. Update STATUS.md Next-up; light ARCHITECTURE touch if needed.
6. Vault write-back to `Famapp/_Claude.md`.
**Out of scope for implementation pass:** Playwright code, schema changes, module implementation, full ADR research bodies.
**Success criteria:** every backlog item has a Gitea URL (or draft file); P1 has executable task briefs; STATUS points agents at step 1 (bugs).
## Out of scope (this design)
- Implementing any bug fix or feature
- Renumbering or rewriting existing `docs/tasks/01``76`
- P2/P3 full task briefs or ADRs
- Live LLM integration in CI
- Forward-auth for non-famapp services
- Multi-household UI
+36
View File
@@ -0,0 +1,36 @@
# 80 — Quick-add opens create UI
## Goal
Make every quick-add action open the entity's creation dialog/sheet instead of only navigating to a module page.
## Why
Task 21 shipped URL navigation as a placeholder. Users expect "New event" to start creating an event immediately.
## Depends on
- 21 (quick-add registry)
## Scope
- Extend `QuickAddAction` / client handling so actions can open create UI (dialog/sheet) in-place.
- Wire calendar, lists, notes, garden, bangs (and any other registered) quick-adds to their create UIs.
- Keep FAB sheet and cmd-k behavior identical per action.
- Playwright: extend `tests/e2e/dashboard.spec.ts` and/or add `tests/e2e/quick-add.spec.ts`.
## Out of scope
- Fuzzy search across entities.
- New entity types.
## Acceptance criteria
- [ ] Each registered quick-add opens create UI with create controls visible.
- [ ] No quick-add is navigation-only unless explicitly documented as view-only (none in v1 set).
- [ ] E2E: FAB → action opens dialog/sheet (not only route change).
## Notes
- Gitea: [ginnoir/famapp#1](https://gitea.ginnoir.com/ginnoir/famapp/issues/1)
- Prefer reusing existing create dialogs/sheets over new ones.
@@ -0,0 +1,34 @@
# 81 — Dashboard edit mode live widgets
## Goal
In dashboard edit mode, render each widget's real data at its true final size so layout matches the saved view.
## Why
Edit mode currently shows empty placeholders; on save, content-heavy widgets (e.g. Recent activity) expand and the grid reflows.
## Depends on
- 26 (customizable layout + widget configuration)
## Scope
- `DashboardEditor` / edit mode loads real widget components and data (not empty shells).
- Widgets size to content consistently between edit and view modes.
- Playwright: extend `tests/e2e/dashboard.spec.ts` — enter `?edit=1`, assert real content visible. Prefer height-stability assertion only if non-flaky.
## Out of scope
- Redesigning the widget picker or config schemas.
- New widget types.
## Acceptance criteria
- [ ] Edit mode shows real widget content for registered widgets.
- [ ] Saving layout does not cause a large content-driven reflow for content-heavy widgets.
- [ ] E2E covers real content visible in edit mode.
## Notes
- Gitea: [ginnoir/famapp#2](https://gitea.ginnoir.com/ginnoir/famapp/issues/2)
+33
View File
@@ -0,0 +1,33 @@
# 82 — Garden container plant count
## Goal
Fix container plant counts so a container with N plants displays N, and audit sibling counts for the same bug.
## Why
Containers (e.g. "plantie cabinet") show "0 plants" despite containing plants — likely a query/join or association bug.
## Depends on
- 71 (garden containers), 72 (garden plants)
## Scope
- Find and fix the count query in `src/modules/garden/server/queries.ts` (and related UI).
- Audit other garden counts (lists, widgets) for the same pattern; fix or file follow-ups.
- Playwright: extend `tests/e2e/garden.spec.ts` — plant in container → count ≥ 1.
## Out of scope
- New garden features.
## Acceptance criteria
- [ ] Container with N plants displays N (not 0).
- [ ] Sibling counts audited; fixed or follow-ups filed.
- [ ] E2E covers plant-in-container count.
## Notes
- Gitea: [ginnoir/famapp#3](https://gitea.ginnoir.com/ginnoir/famapp/issues/3)
+35
View File
@@ -0,0 +1,35 @@
# 83 — Bang edit and delete
## Goal
Allow editing and deleting bang entries after creation (e.g. wrong date).
## Why
Bangs can be recorded but not corrected. Only create exists today.
## Depends on
- Bangs module (in tree; no prior task number)
## Scope
- Server actions for update and delete.
- UI on the bang widget (and any list surface) for edit/delete.
- Activity log for update/delete if other modules log similarly.
- Playwright: new `tests/e2e/bangs.spec.ts` — record → edit date → assert → delete → assert gone.
## Out of scope
- Bang stats widget (design #11 / Gitea #31) — separate P2 issue.
## Acceptance criteria
- [ ] Edit a bang (e.g. change date) and see the update.
- [ ] Delete a bang and it no longer appears.
- [ ] E2E covers edit and delete.
## Notes
- Gitea: [ginnoir/famapp#4](https://gitea.ginnoir.com/ginnoir/famapp/issues/4)
- Related (not blocking): #31 bang stats widget.
+34
View File
@@ -0,0 +1,34 @@
# 84 — Back navigation on detail pages
## Goal
Audit detail routes lacking back affordances and add a shared header/back pattern consistently.
## Why
Individual plant pages (and other detail routes) have no in-page "back" except global nav.
## Depends on
- Existing app routes under `src/app/`
## Scope
- Audit all routes in `src/app/` for detail pages missing back; list them in the PR description.
- Shared back affordance (header/back component).
- Apply consistently to plant, note, list, and other detail pages.
- Playwright: `tests/e2e/navigation.spec.ts` or per-module — Back visible and returns to parent.
## Out of scope
- Redesigning global nav or dashboard tabs.
## Acceptance criteria
- [ ] Audit list included in PR.
- [ ] Shared back control on detail pages.
- [ ] E2E covers representative detail routes.
## Notes
- Gitea: [ginnoir/famapp#6](https://gitea.ginnoir.com/ginnoir/famapp/issues/6)
+42
View File
@@ -0,0 +1,42 @@
# 85 — Rich-text notes overhaul
## Goal
Land a shared rich-text editor (ADR 0004), wire it through notes create/edit and all display surfaces, and fix mobile horizontal overflow.
## Why
Journal (86) and notes need modern formatting and emoji. Notes currently overflow sideways on mobile. P1 pulls the full notes overhaul into this batch.
## Depends on
- 12 (notes module)
- ADR `0004-rich-text-editor.md` (research child: Gitea #8)
- Batch order: after bugs 8084 and API foundation 87 is preferred for notes API extension, but editor work can proceed once ADR is accepted
## Scope
- Research and accept ADR 0004 (library e.g. TipTap, storage format, shared component contract).
- Shared editor component used by notes (and later journal).
- Notes create/edit use the shared editor.
- Render formatting on index, widgets, and share view.
- Fix mobile overflow (closes Gitea #5 / design #5).
- Playwright: extend `tests/e2e/notes.spec.ts` — formatted note visible; 375px no horizontal overflow.
## Out of scope
- Journal module (86) — consumes the shared editor after this lands.
- Appearance/theming epic.
## Acceptance criteria
- [ ] ADR 0004 accepted with library and storage format chosen.
- [ ] Shared editor component in tree.
- [ ] Notes create/edit use it.
- [ ] Formatting renders on all notes surfaces.
- [ ] Mobile overflow fixed; E2E green.
## Notes
- Gitea epic: [#7](https://gitea.ginnoir.com/ginnoir/famapp/issues/7); children #8#12; overflow bug [#5](https://gitea.ginnoir.com/ginnoir/famapp/issues/5)
- Design: `docs/superpowers/specs/2026-07-03-backlog-triage-design.md`
+45
View File
@@ -0,0 +1,45 @@
# 86 — Journal module
## Goal
Ship a per-user journal module with entries, mood tracking, insights, and journal API endpoints on the existing API foundation.
## Why
P1 personal journaling with mood/stress/pills tracking and stats, without blocking on P2 work.
## Depends on
- 85 (shared rich-text editor)
- 87 (API surface for existing modules — journal adds endpoints the same way)
- ADR `0005-journal-research.md` (Gitea #20)
## Scope
- Research ADR 0005 (mood/journal libs; build-vs-adopt).
- Schema + CRUD (per-user entries).
- Index: last 510 entries compact, browse all, small calendar of days with entries.
- Entry detail and create: **date (default today) and time (default now) required**; everything else optional.
- Fields: rich note text (shared editor); numerical stress; mood with color/emoji options and **multi-select**; took-my-pills-today boolean.
- Module-level mood-tracker view over time.
- Per-user insights/stats (trends, streaks, correlations — propose specifics in ADR/implementation).
- Journal API endpoints (additive on 87).
- Playwright: new `tests/e2e/journal.spec.ts` — minimal entry → recent list → detail → mood tracker loads.
## Out of scope
- Household-shared journals (per-user only).
- LLM agent tools for journal (88 can add later).
## Acceptance criteria
- [ ] ADR 0005 accepted.
- [ ] Create entry with only date/time.
- [ ] Index, detail, mood tracker, and insights views work.
- [ ] Journal API endpoints documented and callable with token auth.
- [ ] E2E happy path green.
## Notes
- Gitea epic: [#19](https://gitea.ginnoir.com/ginnoir/famapp/issues/19); children #20#27
- Depends on #7 (rich-text) and #15 (API surface)
+39
View File
@@ -0,0 +1,39 @@
# 87 — API surface + token auth
## Goal
Documented HTTP API for existing modules (calendar, lists, notes, garden, bangs) with token auth suitable for non-browser clients, coexisting with OIDC session auth.
## Why
API-early batch order: foundation lands after bugs so journal and notes can extend it; LLM agent (88) maps tools onto it.
## Depends on
- ADR `0006-api-llm-agent.md` research half (Gitea #14) — auth model and API shape decided before implementation
## Scope
- Architecture ADR 0006: token auth vs OIDC session coexistence; API shape; additive versioning (no freeze that blocks journal).
- Token auth for non-browser clients.
- Endpoints for existing entities: events, lists/items, notes, garden, bangs (dashboards as needed).
- API docs (OpenAPI or equivalent living doc).
- Vitest for auth + route handlers; optional Playwright smoke only if useful.
## Out of scope
- LLM agent chat UI and tool-calling (88).
- Journal endpoints (86 adds them).
## Acceptance criteria
- [ ] ADR 0006 documents auth coexistence and API shape.
- [ ] Token-authenticated client can read/write covered entities.
- [ ] Session (OIDC) auth still works for the web app.
- [ ] Docs list endpoints and auth.
- [ ] Unit/integration tests for auth and handlers.
## Notes
- Gitea: epic [#13](https://gitea.ginnoir.com/ginnoir/famapp/issues/13); research [#14](https://gitea.ginnoir.com/ginnoir/famapp/issues/14); implementation [#15](https://gitea.ginnoir.com/ginnoir/famapp/issues/15)
- Batch step 6 (after bugs 8084)
+39
View File
@@ -0,0 +1,39 @@
# 88 — LLM agent chat
## Goal
Natural-language chat that acts on the app via tool-calling against the API, using a provider-agnostic OpenAI-compatible endpoint (self-hosted on the homelab).
## Why
Users should say "add milk to the shopping list" or "what's on the calendar Friday?" and have the agent use the API.
## Depends on
- 87 (API surface)
- ADR `0006-api-llm-agent.md` agent half (MCP yes/no, tool mapping)
## Scope
- Agent chat UI in the app.
- OpenAI-compatible client (provider-agnostic).
- Tool-calling mapped to API endpoints from 87 (and journal if present).
- Decide MCP vs direct tools in ADR 0006.
- Smoke tests with **mock/stub** provider in CI — no live LLM in CI.
## Out of scope
- Training or fine-tuning models.
- Forward-auth for other homelab apps.
## Acceptance criteria
- [ ] Chat UI sends prompts and shows responses.
- [ ] Tools call the API successfully in dev against the homelab endpoint.
- [ ] CI uses mock provider only.
- [ ] ADR 0006 records provider and MCP decisions.
## Notes
- Gitea: [#16](https://gitea.ginnoir.com/ginnoir/famapp/issues/16), [#17](https://gitea.ginnoir.com/ginnoir/famapp/issues/17), [#18](https://gitea.ginnoir.com/ginnoir/famapp/issues/18)
- Depends on [#15](https://gitea.ginnoir.com/ginnoir/famapp/issues/15)
+14
View File
@@ -79,3 +79,17 @@ Every task file has these sections:
- [73 — Garden care tracking](73-garden-care-tracking.md)
- [74 — Garden integrations](74-garden-integrations.md)
- [75 — Garden dashboard](75-garden-dashboard.md)
### Phase 9 — Post-v0.1
Batch order (bugs → API → editor/notes → journal → agent). Design: [`docs/superpowers/specs/2026-07-03-backlog-triage-design.md`](../superpowers/specs/2026-07-03-backlog-triage-design.md). Issue map: [`docs/issues-map.md`](../issues-map.md).
- [80 — Quick-add create UI](80-quick-add-create-ui.md)
- [81 — Dashboard edit live widgets](81-dashboard-edit-live-widgets.md)
- [82 — Garden plant count](82-garden-plant-count.md)
- [83 — Bang edit/delete](83-bang-edit-delete.md)
- [84 — Back navigation](84-back-navigation.md)
- [85 — Rich-text notes overhaul](85-rich-text-notes.md)
- [86 — Journal module](86-journal-module.md)
- [87 — API surface](87-api-surface.md)
- [88 — LLM agent chat](88-llm-agent-chat.md)
+2
View File
@@ -11,6 +11,7 @@ import type { DashboardMeta } from "@/app/d/actions";
import { getQuickAdds } from "@/modules/_core";
import { QuickAddProvider } from "@/components/quick-add-provider";
import { QuickAddSheet } from "@/components/quick-add-sheet";
import { QuickAddCreateHost } from "@/components/quick-add/create-host";
import { CommandPalette } from "@/components/command-palette";
import { PwaRegister } from "@/components/pwa-register";
import { InstallPrompt } from "@/components/install-prompt";
@@ -167,6 +168,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
{children}
</AppShell>
<QuickAddSheet />
<QuickAddCreateHost />
<CommandPalette />
<InstallPrompt />
<PwaRegister />
+12 -4
View File
@@ -6,7 +6,7 @@ import { useRouter } from "next/navigation";
import { useQuickAdd } from "./quick-add-provider";
export function CommandPalette() {
const { paletteOpen, closePalette, actions } = useQuickAdd();
const { paletteOpen, closePalette, openCreate, actions } = useQuickAdd();
const router = useRouter();
const inputRef = useRef<HTMLInputElement>(null);
@@ -19,9 +19,13 @@ export function CommandPalette() {
if (!paletteOpen) return null;
function handleSelect(url: string) {
function handleSelect(action: import("@/modules/_core").SerializedQuickAddItem) {
closePalette();
router.push(url);
if (action.createKey) {
openCreate(action.createKey);
return;
}
router.push(action.url);
}
return (
@@ -64,7 +68,7 @@ export function CommandPalette() {
<Command.Item
key={action.id}
value={`${action.moduleName} ${action.label}`}
onSelect={() => handleSelect(action.url)}
onSelect={() => handleSelect(action)}
className="flex cursor-pointer items-center gap-3 rounded-lg px-3 py-2 text-sm aria-selected:bg-accent aria-selected:text-accent-foreground"
>
<span className="text-base leading-none">{iconEmoji(action.icon)}</span>
@@ -113,6 +117,10 @@ function iconEmoji(icon?: string): string {
"list-checks": "✅",
"list-plus": "📋",
"file-plus": "📝",
leaf: "🌿",
box: "📦",
droplets: "💧",
sparkles: "💥",
};
return icon ? (map[icon] ?? "") : "";
}
+18 -1
View File
@@ -6,10 +6,13 @@ import type { SerializedQuickAddItem } from "@/modules/_core";
type QuickAddState = {
sheetOpen: boolean;
paletteOpen: boolean;
activeCreateKey: string | null;
openSheet: () => void;
closeSheet: () => void;
openPalette: () => void;
closePalette: () => void;
openCreate: (key: string) => void;
closeCreate: () => void;
actions: SerializedQuickAddItem[];
};
@@ -30,11 +33,14 @@ export function QuickAddProvider({
}) {
const [sheetOpen, setSheetOpen] = useState(false);
const [paletteOpen, setPaletteOpen] = useState(false);
const [activeCreateKey, setActiveCreateKey] = useState<string | null>(null);
const openSheet = useCallback(() => setSheetOpen(true), []);
const closeSheet = useCallback(() => setSheetOpen(false), []);
const openPalette = useCallback(() => setPaletteOpen(true), []);
const closePalette = useCallback(() => setPaletteOpen(false), []);
const openCreate = useCallback((key: string) => setActiveCreateKey(key), []);
const closeCreate = useCallback(() => setActiveCreateKey(null), []);
useEffect(() => {
function onKeyDown(e: KeyboardEvent) {
@@ -49,7 +55,18 @@ export function QuickAddProvider({
return (
<QuickAddContext.Provider
value={{ sheetOpen, paletteOpen, openSheet, closeSheet, openPalette, closePalette, actions }}
value={{
sheetOpen,
paletteOpen,
activeCreateKey,
openSheet,
closeSheet,
openPalette,
closePalette,
openCreate,
closeCreate,
actions,
}}
>
{children}
</QuickAddContext.Provider>
+12 -4
View File
@@ -27,6 +27,10 @@ const QUICK_ADD_ICON: Record<string, string> = {
"list-checks": "check-square",
"list-plus": "list",
"file-plus": "note",
leaf: "sprout",
box: "box",
droplets: "droplets",
sparkles: "sparkles",
};
function useShortcutLabel() {
@@ -35,7 +39,7 @@ function useShortcutLabel() {
}
export function QuickAddSheet() {
const { sheetOpen, closeSheet, actions } = useQuickAdd();
const { sheetOpen, closeSheet, openCreate, actions } = useQuickAdd();
const shortcut = useShortcutLabel();
const router = useRouter();
const backdropRef = useRef<HTMLDivElement>(null);
@@ -53,9 +57,13 @@ export function QuickAddSheet() {
const groups = groupByModule(actions);
function handleAction(url: string) {
function handleAction(action: SerializedQuickAddItem) {
closeSheet();
router.push(url);
if (action.createKey) {
openCreate(action.createKey);
return;
}
router.push(action.url);
}
return (
@@ -129,7 +137,7 @@ export function QuickAddSheet() {
{group.items.map((action) => (
<button
key={action.id}
onClick={() => handleAction(action.url)}
onClick={() => handleAction(action)}
className="btn btn-sm justify-start"
>
<NavIcon
+55
View File
@@ -0,0 +1,55 @@
"use client";
import { useQuickAdd } from "@/components/quick-add-provider";
import { BangRecordDialog } from "@/components/quick-add/dialogs/bang-record-dialog";
import { CalendarCreateDialog } from "@/components/quick-add/dialogs/calendar-create-dialog";
import { CalendarEventCreateDialog } from "@/components/quick-add/dialogs/calendar-event-create-dialog";
import { ContainerCreateDialog } from "@/components/quick-add/dialogs/container-create-dialog";
import { GardenCareLogDialog } from "@/components/quick-add/dialogs/garden-care-log-dialog";
import { ListCreateDialog } from "@/components/quick-add/dialogs/list-create-dialog";
import { ListItemCreateDialog } from "@/components/quick-add/dialogs/list-item-create-dialog";
import { NoteCreateDialog } from "@/components/quick-add/dialogs/note-create-dialog";
import { PlantCreateDialog } from "@/components/quick-add/dialogs/plant-create-dialog";
export function QuickAddCreateHost() {
const { activeCreateKey, closeCreate } = useQuickAdd();
const onOpenChange = (next: boolean) => {
if (!next) closeCreate();
};
return (
<>
<CalendarEventCreateDialog
open={activeCreateKey === "calendar.event"}
onOpenChange={onOpenChange}
/>
<CalendarCreateDialog
open={activeCreateKey === "calendar.calendar"}
onOpenChange={onOpenChange}
/>
<NoteCreateDialog open={activeCreateKey === "notes.note"} onOpenChange={onOpenChange} />
<ListItemCreateDialog
open={activeCreateKey === "lists.add-shopping"}
onOpenChange={onOpenChange}
listType="shopping"
/>
<ListItemCreateDialog
open={activeCreateKey === "lists.add-task"}
onOpenChange={onOpenChange}
listType="task"
/>
<ListCreateDialog open={activeCreateKey === "lists.new-list"} onOpenChange={onOpenChange} />
<PlantCreateDialog open={activeCreateKey === "garden.plant"} onOpenChange={onOpenChange} />
<ContainerCreateDialog
open={activeCreateKey === "garden.container"}
onOpenChange={onOpenChange}
/>
<GardenCareLogDialog
open={activeCreateKey === "garden.log-care"}
onOpenChange={onOpenChange}
/>
<BangRecordDialog open={activeCreateKey === "bangs.record"} onOpenChange={onOpenChange} />
</>
);
}
@@ -0,0 +1,87 @@
"use client";
import { useState, useTransition } from "react";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { addBang } from "@/modules/bangs/server/actions";
type Props = {
open: boolean;
onOpenChange: (open: boolean) => void;
};
export function BangRecordDialog({ open, onOpenChange }: Props) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
{open ? <BangRecordForm onDone={() => onOpenChange(false)} /> : null}
</DialogContent>
</Dialog>
);
}
function BangRecordForm({ onDone }: { onDone: () => void }) {
const [dateValue, setDateValue] = useState(todayValue());
const [error, setError] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError(null);
startTransition(async () => {
try {
await addBang({ occurredOn: dateValue });
onDone();
} catch {
setError("Something went wrong. Try again.");
}
});
}
return (
<>
<DialogHeader>
<DialogTitle>Record a bang</DialogTitle>
</DialogHeader>
<form id="quick-add-bang-form" onSubmit={handleSubmit} className="flex flex-col gap-3">
<div className="flex flex-col gap-1">
<label htmlFor="qa-bang-date" className="text-sm font-medium">
Date
</label>
<input
id="qa-bang-date"
type="date"
value={dateValue}
onChange={(e) => setDateValue(e.target.value)}
className="input input-sm"
required
/>
<p className="text-xs text-[var(--ink-mute)]">
Change if you&apos;re documenting a bang from a previous day.
</p>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
</form>
<DialogFooter showCloseButton>
<button
type="submit"
form="quick-add-bang-form"
disabled={isPending}
className="btn btn-primary btn-sm"
>
{isPending ? "Recording…" : "Record it"}
</button>
</DialogFooter>
</>
);
}
function todayValue(): string {
const d = new Date();
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}
@@ -0,0 +1,109 @@
"use client";
import { useState, useTransition } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { createCalendar } from "@/modules/calendar/server/actions";
const DEFAULT_COLOR = "#B85C3C";
const VISIBILITY_ITEMS = [
{ label: "Household", value: "household" },
{ label: "Private", value: "private" },
];
type Props = {
open: boolean;
onOpenChange: (open: boolean) => void;
};
export function CalendarCreateDialog({ open, onOpenChange }: Props) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
{open ? <CalendarCreateForm onDone={() => onOpenChange(false)} /> : null}
</DialogContent>
</Dialog>
);
}
function CalendarCreateForm({ onDone }: { onDone: () => void }) {
const [name, setName] = useState("");
const [color, setColor] = useState(DEFAULT_COLOR);
const [visibility, setVisibility] = useState<"private" | "household">("household");
const [isPending, startTransition] = useTransition();
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!name.trim()) return;
startTransition(async () => {
await createCalendar({ name: name.trim(), color, visibility });
onDone();
});
}
return (
<>
<DialogHeader>
<DialogTitle>New calendar</DialogTitle>
</DialogHeader>
<form id="quick-add-calendar-form" onSubmit={handleSubmit} className="grid gap-3">
<div className="space-y-1.5">
<Label htmlFor="qa-calendar-name">Name</Label>
<Input
id="qa-calendar-name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</div>
<div className="grid grid-cols-[1fr_1fr] gap-2">
<Input
aria-label="Calendar color"
type="color"
value={color}
onChange={(e) => setColor(e.target.value)}
/>
<Select
items={VISIBILITY_ITEMS}
value={visibility}
onValueChange={(value) => setVisibility(value as "private" | "household")}
>
<SelectTrigger aria-label="Calendar visibility">
<SelectValue>{visibility === "household" ? "Household" : "Private"}</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
{VISIBILITY_ITEMS.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
</form>
<DialogFooter>
<Button type="submit" form="quick-add-calendar-form" disabled={!name.trim() || isPending}>
Create calendar
</Button>
</DialogFooter>
</>
);
}
@@ -0,0 +1,181 @@
"use client";
import { useEffect, useMemo, useState, useTransition } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { createEvent } from "@/modules/calendar/server/actions";
import { listCalendars, type CalendarDto } from "@/modules/calendar/server/queries";
type Props = {
open: boolean;
onOpenChange: (open: boolean) => void;
};
export function CalendarEventCreateDialog({ open, onOpenChange }: Props) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
{open ? <CalendarEventCreateForm onDone={() => onOpenChange(false)} /> : null}
</DialogContent>
</Dialog>
);
}
function CalendarEventCreateForm({ onDone }: { onDone: () => void }) {
const [calendars, setCalendars] = useState<CalendarDto[]>([]);
const [calendarId, setCalendarId] = useState("");
const [title, setTitle] = useState("");
const [startAt, setStartAt] = useState(() => toInputDateTime(new Date()));
const [endAt, setEndAt] = useState(() => toInputDateTime(new Date(Date.now() + 60 * 60 * 1000)));
const [location, setLocation] = useState("");
const [notes, setNotes] = useState("");
const [remind, setRemind] = useState(true);
const [isPending, startTransition] = useTransition();
useEffect(() => {
listCalendars().then((rows) => {
setCalendars(rows);
setCalendarId(rows[0]?.id ?? "");
});
}, []);
const calendarItems = useMemo(
() => calendars.map((c) => ({ label: c.name, value: c.id })),
[calendars],
);
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!calendarId || !title.trim()) return;
startTransition(async () => {
await createEvent({
calendarId,
title: title.trim(),
startAt: new Date(startAt),
endAt: new Date(endAt),
allDay: false,
location: location || null,
notes: notes || null,
remindMinutesBefore: remind ? 30 : null,
});
onDone();
});
}
return (
<>
<DialogHeader>
<DialogTitle>New event</DialogTitle>
</DialogHeader>
<form id="quick-add-event-form" onSubmit={handleSubmit} className="grid gap-3">
<div className="space-y-1.5">
<Label htmlFor="qa-event-title">Title</Label>
<Input
id="qa-event-title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="qa-event-calendar">Calendar</Label>
<Select
items={calendarItems}
value={calendarId}
onValueChange={(value) => setCalendarId(value ?? "")}
>
<SelectTrigger id="qa-event-calendar">
<SelectValue>
{calendars.find((c) => c.id === calendarId)?.name ?? "Select a calendar"}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
{calendarItems.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor="qa-event-start">Start</Label>
<Input
id="qa-event-start"
type="datetime-local"
value={startAt}
onChange={(e) => setStartAt(e.target.value)}
required
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="qa-event-end">End</Label>
<Input
id="qa-event-end"
type="datetime-local"
value={endAt}
onChange={(e) => setEndAt(e.target.value)}
required
/>
</div>
</div>
<div className="space-y-1.5">
<Label htmlFor="qa-event-location">Location</Label>
<Input
id="qa-event-location"
value={location}
onChange={(e) => setLocation(e.target.value)}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="qa-event-notes">Notes</Label>
<textarea
id="qa-event-notes"
className="min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-sm outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50"
value={notes}
onChange={(e) => setNotes(e.target.value)}
/>
</div>
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
className="size-4 cursor-pointer"
checked={remind}
onChange={(e) => setRemind(e.target.checked)}
/>
Remind me 30 min before
</label>
</form>
<DialogFooter>
<Button type="submit" form="quick-add-event-form" disabled={!title.trim() || isPending}>
Save event
</Button>
</DialogFooter>
</>
);
}
function toInputDateTime(date: Date) {
const offset = date.getTimezoneOffset();
const local = new Date(date.getTime() - offset * 60 * 1000);
return local.toISOString().slice(0, 16);
}
@@ -0,0 +1,29 @@
"use client";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { ContainerForm } from "@/modules/garden/components/container-form";
type Props = {
open: boolean;
onOpenChange: (open: boolean) => void;
};
export function ContainerCreateDialog({ open, onOpenChange }: Props) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md max-h-[90dvh] overflow-y-auto">
{open ? (
<>
<DialogHeader>
<DialogTitle>Add container</DialogTitle>
</DialogHeader>
<ContainerForm
onSuccess={() => onOpenChange(false)}
onCancel={() => onOpenChange(false)}
/>
</>
) : null}
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,86 @@
"use client";
import { useEffect, useState } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { CareLogForm } from "@/modules/garden/components/care-log-form";
import { listPlants, type PlantListItemDto } from "@/modules/garden/server/queries";
type Props = {
open: boolean;
onOpenChange: (open: boolean) => void;
};
export function GardenCareLogDialog({ open, onOpenChange }: Props) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
{open ? <GardenCareLogForm onDone={() => onOpenChange(false)} /> : null}
</DialogContent>
</Dialog>
);
}
function GardenCareLogForm({ onDone }: { onDone: () => void }) {
const [plants, setPlants] = useState<PlantListItemDto[]>([]);
const [plantId, setPlantId] = useState("");
const [loadError, setLoadError] = useState<string | null>(null);
useEffect(() => {
listPlants()
.then((rows) => {
setPlants(rows);
setPlantId(rows[0]?.id ?? "");
if (rows.length === 0) setLoadError("No plants yet — add a plant first.");
})
.catch(() => setLoadError("Could not load plants."));
}, []);
const plantItems = plants.map((p) => ({ label: p.name, value: p.id }));
return (
<>
<DialogHeader>
<DialogTitle>Log plant care</DialogTitle>
</DialogHeader>
{loadError ? (
<p className="text-sm text-red-500">{loadError}</p>
) : (
<div className="grid gap-3">
<div className="space-y-1.5">
<Label htmlFor="qa-care-plant">Plant</Label>
<Select
items={plantItems}
value={plantId}
onValueChange={(value) => setPlantId(value ?? "")}
>
<SelectTrigger id="qa-care-plant">
<SelectValue>
{plants.find((p) => p.id === plantId)?.name ?? "Select a plant"}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
{plantItems.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
{plantId ? <CareLogForm plantId={plantId} onSuccess={onDone} /> : null}
</div>
)}
</>
);
}
@@ -0,0 +1,81 @@
"use client";
import { useState, useTransition } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { createList } from "@/modules/lists/server/actions";
type Props = {
open: boolean;
onOpenChange: (open: boolean) => void;
};
export function ListCreateDialog({ open, onOpenChange }: Props) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
{open ? <ListCreateForm onDone={() => onOpenChange(false)} /> : null}
</DialogContent>
</Dialog>
);
}
function ListCreateForm({ onDone }: { onDone: () => void }) {
const [type, setType] = useState("shopping");
const [name, setName] = useState("");
const [isPending, startTransition] = useTransition();
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!type.trim() || !name.trim()) return;
startTransition(async () => {
await createList({ type: type.trim(), name: name.trim() });
onDone();
});
}
return (
<>
<DialogHeader>
<DialogTitle>New list</DialogTitle>
</DialogHeader>
<form id="quick-add-new-list-form" onSubmit={handleSubmit} className="grid gap-3">
<div className="space-y-1.5">
<Label htmlFor="qa-new-list-type">Type</Label>
<Input
id="qa-new-list-type"
value={type}
onChange={(e) => setType(e.target.value)}
required
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="qa-new-list-name">Name</Label>
<Input
id="qa-new-list-name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</div>
</form>
<DialogFooter>
<Button
type="submit"
form="quick-add-new-list-form"
disabled={!type.trim() || !name.trim() || isPending}
>
Create list
</Button>
</DialogFooter>
</>
);
}
@@ -0,0 +1,113 @@
"use client";
import { useEffect, useState, useTransition } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { addItem } from "@/modules/lists/server/actions";
import { listLists } from "@/modules/lists/server/queries";
type Props = {
open: boolean;
onOpenChange: (open: boolean) => void;
listType: "shopping" | "task";
};
const TITLES: Record<Props["listType"], string> = {
shopping: "Add to shopping",
task: "Add to tasks",
};
export function ListItemCreateDialog({ open, onOpenChange, listType }: Props) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
{open ? (
<ListItemCreateForm listType={listType} onDone={() => onOpenChange(false)} />
) : null}
</DialogContent>
</Dialog>
);
}
function ListItemCreateForm({
listType,
onDone,
}: {
listType: "shopping" | "task";
onDone: () => void;
}) {
const [listId, setListId] = useState<string | null>(null);
const [listName, setListName] = useState("");
const [text, setText] = useState("");
const [loadError, setLoadError] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();
useEffect(() => {
listLists({ type: listType })
.then((rows) => {
const list = rows[0];
if (!list) {
setLoadError(`No ${listType} list found.`);
return;
}
setListId(list.id);
setListName(list.name);
})
.catch(() => setLoadError("Could not load list."));
}, [listType]);
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const trimmed = text.trim();
if (!listId || !trimmed) return;
startTransition(async () => {
await addItem({ listId, text: trimmed });
onDone();
});
}
return (
<>
<DialogHeader>
<DialogTitle>{TITLES[listType]}</DialogTitle>
</DialogHeader>
{loadError ? (
<p className="text-sm text-red-500">{loadError}</p>
) : (
<form id={`quick-add-list-item-${listType}`} onSubmit={handleSubmit} className="grid gap-3">
{listName ? <p className="text-sm text-muted-foreground">Adding to {listName}</p> : null}
<div className="space-y-1.5">
<Label htmlFor={`qa-list-item-${listType}`}>
{listType === "shopping" ? "Item" : "Task"}
</Label>
<Input
id={`qa-list-item-${listType}`}
value={text}
onChange={(e) => setText(e.target.value)}
placeholder={listType === "shopping" ? "Add to list…" : "Add a task…"}
required
autoFocus
/>
</div>
</form>
)}
<DialogFooter>
<Button
type="submit"
form={`quick-add-list-item-${listType}`}
disabled={!listId || !text.trim() || isPending}
>
Add
</Button>
</DialogFooter>
</>
);
}
@@ -0,0 +1,92 @@
"use client";
import { useState, useTransition } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { createNote } from "@/modules/notes/server/actions";
type Props = {
open: boolean;
onOpenChange: (open: boolean) => void;
};
export function NoteCreateDialog({ open, onOpenChange }: Props) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
{open ? <NoteCreateForm onDone={() => onOpenChange(false)} /> : null}
</DialogContent>
</Dialog>
);
}
function NoteCreateForm({ onDone }: { onDone: () => void }) {
const [title, setTitle] = useState("");
const [body, setBody] = useState("");
const [remindAt, setRemindAt] = useState("");
const [isPending, startTransition] = useTransition();
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!title.trim()) return;
startTransition(async () => {
await createNote({
title: title.trim(),
body,
remindAt: remindAt ? new Date(remindAt) : null,
});
onDone();
});
}
return (
<>
<DialogHeader>
<DialogTitle>New note</DialogTitle>
</DialogHeader>
<form id="quick-add-note-form" onSubmit={handleSubmit} className="grid gap-3">
<div className="space-y-1.5">
<Label htmlFor="qa-note-title">Title</Label>
<Input
id="qa-note-title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="qa-note-body">Body</Label>
<textarea
id="qa-note-body"
aria-label="Body"
className="min-h-32 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-sm outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50"
value={body}
onChange={(e) => setBody(e.target.value)}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="qa-note-reminder">Reminder</Label>
<Input
id="qa-note-reminder"
type="datetime-local"
value={remindAt}
onChange={(e) => setRemindAt(e.target.value)}
/>
</div>
</form>
<DialogFooter>
<Button type="submit" form="quick-add-note-form" disabled={!title.trim() || isPending}>
Save note
</Button>
</DialogFooter>
</>
);
}
@@ -0,0 +1,40 @@
"use client";
import { useEffect, useState } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { PlantForm } from "@/modules/garden/components/plant-form";
import { listContainers, type ContainerDto } from "@/modules/garden/server/queries";
type Props = {
open: boolean;
onOpenChange: (open: boolean) => void;
};
export function PlantCreateDialog({ open, onOpenChange }: Props) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-lg max-h-[90dvh] overflow-y-auto">
{open ? <PlantCreateForm onDone={() => onOpenChange(false)} /> : null}
</DialogContent>
</Dialog>
);
}
function PlantCreateForm({ onDone }: { onDone: () => void }) {
const [containers, setContainers] = useState<ContainerDto[]>([]);
useEffect(() => {
listContainers()
.then(setContainers)
.catch(() => setContainers([]));
}, []);
return (
<>
<DialogHeader>
<DialogTitle>Add plant</DialogTitle>
</DialogHeader>
<PlantForm containers={containers} onSuccess={() => onDone()} onCancel={onDone} />
</>
);
}
+2
View File
@@ -67,6 +67,8 @@ export type QuickAddAction = {
icon?: string;
/** Navigation target used by the FAB sheet and command palette. */
url: string;
/** Client-side create dialog key (see quick-add create registry). */
createKey?: string;
action?: (ctx: WidgetContext) => void | Promise<void>;
};
+3 -1
View File
@@ -43,6 +43,7 @@ export type SerializedQuickAddItem = {
label: string;
icon?: string;
url: string;
createKey?: string;
moduleId: string;
moduleName: string;
};
@@ -95,11 +96,12 @@ export async function fireItemToggleHooks(payload: ItemTogglePayload): Promise<v
export function getQuickAdds(): SerializedQuickAddItem[] {
return [...modules.values()].flatMap((manifest) =>
(manifest.quickAdds ?? []).map(({ id, label, icon, url }) => ({
(manifest.quickAdds ?? []).map(({ id, label, icon, url, createKey }) => ({
id,
label,
icon,
url,
createKey,
moduleId: manifest.id,
moduleName: manifest.name,
})),
+9
View File
@@ -41,6 +41,15 @@ const bangsManifest: ModuleManifest = {
render: (props) => <BangWidgetServer {...props} />,
},
],
quickAdds: [
{
id: "bangs.record",
label: "Record bang",
icon: "sparkles",
url: "/",
createKey: "bangs.record",
},
],
};
export default bangsManifest;
+2
View File
@@ -226,12 +226,14 @@ const manifest: ModuleManifest = {
label: "New event",
icon: "calendar-plus",
url: "/calendar",
createKey: "calendar.event",
},
{
id: "calendar.new-calendar",
label: "New calendar",
icon: "calendar-days",
url: "/calendar",
createKey: "calendar.calendar",
},
],
};
+32 -10
View File
@@ -42,9 +42,17 @@ type Props = {
existingPlant?: PlantDetailDto;
defaultContainerId?: string | null;
containers: ContainerDto[];
onSuccess?: (plantId: string) => void;
onCancel?: () => void;
};
export function PlantForm({ existingPlant, defaultContainerId, containers }: Props) {
export function PlantForm({
existingPlant,
defaultContainerId,
containers,
onSuccess,
onCancel,
}: Props) {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();
@@ -121,11 +129,19 @@ export function PlantForm({ existingPlant, defaultContainerId, containers }: Pro
try {
if (existingPlant) {
await updatePlant({ id: existingPlant.id, ...input });
router.push(`/garden/plants/${existingPlant.id}`);
router.refresh();
if (onSuccess) {
onSuccess(existingPlant.id);
} else {
router.push(`/garden/plants/${existingPlant.id}`);
router.refresh();
}
} else {
const plant = await createPlant(input);
router.push(`/garden/plants/${plant.id}`);
if (onSuccess) {
onSuccess(plant.id);
} else {
router.push(`/garden/plants/${plant.id}`);
}
}
} catch {
setError("Something went wrong. Please try again.");
@@ -352,12 +368,18 @@ export function PlantForm({ existingPlant, defaultContainerId, containers }: Pro
{error && <p className="text-sm text-red-500">{error}</p>}
<div className="flex gap-2 justify-end">
<a
href={existingPlant ? `/garden/plants/${existingPlant.id}` : "/garden"}
className="btn btn-ghost"
>
Cancel
</a>
{onCancel ? (
<button type="button" className="btn btn-ghost" onClick={onCancel}>
Cancel
</button>
) : (
<a
href={existingPlant ? `/garden/plants/${existingPlant.id}` : "/garden"}
className="btn btn-ghost"
>
Cancel
</a>
)}
<button type="submit" className="btn btn-primary" disabled={isPending || uploading}>
{isPending ? "Saving…" : existingPlant ? "Save" : "Create"}
</button>
+3
View File
@@ -203,18 +203,21 @@ const gardenManifest: ModuleManifest = {
label: "Add plant",
icon: "leaf",
url: "/garden/plants/new",
createKey: "garden.plant",
},
{
id: "garden.add-container",
label: "Add container",
icon: "box",
url: "/garden/containers/new",
createKey: "garden.container",
},
{
id: "garden.log-care",
label: "Log plant care",
icon: "droplets",
url: "/garden?logCare=1",
createKey: "garden.log-care",
},
],
};
+2
View File
@@ -1,3 +1,5 @@
"use server";
import { and, desc, eq, lte, sql } from "drizzle-orm";
import { db } from "@/lib/db";
import { getCurrentSession } from "@/lib/session";
+3
View File
@@ -93,18 +93,21 @@ const manifest: ModuleManifest = {
label: "Add to shopping",
icon: "shopping-cart",
url: "/lists",
createKey: "lists.add-shopping",
},
{
id: "lists.add-task",
label: "Add to tasks",
icon: "list-checks",
url: "/lists",
createKey: "lists.add-task",
},
{
id: "lists.new-list",
label: "New list",
icon: "list-plus",
url: "/lists",
createKey: "lists.new-list",
},
],
};
+1
View File
@@ -106,6 +106,7 @@ const manifest: ModuleManifest = {
label: "New note",
icon: "file-plus",
url: "/notes/new",
createKey: "notes.note",
},
],
};
+140
View File
@@ -0,0 +1,140 @@
import { expect, test, type Page } from "@playwright/test";
async function ensureSignedIn(page: Page) {
await page.goto("/");
const devLogin = page.getByRole("button", { name: "Dev login" });
if (await devLogin.isVisible().catch(() => false)) {
await devLogin.click();
await page.waitForURL((url) => url.pathname === "/");
}
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
}
async function openFabAction(page: Page, actionLabel: string) {
await page.getByRole("button", { name: "Quick add" }).click();
await expect(page.getByRole("dialog", { name: "Quick add" })).toBeVisible();
await page.getByRole("button", { name: actionLabel, exact: true }).click();
await expect(page).toHaveURL("/");
}
type FabQuickAddCase = {
action: string;
dialog: string;
assertCreateControls: (page: Page) => Promise<void>;
};
const fabQuickAddCases: FabQuickAddCase[] = [
{
action: "New event",
dialog: "New event",
assertCreateControls: async (page) => {
await expect(page.getByLabel("Title")).toBeVisible();
await expect(page.getByRole("button", { name: "Save event" })).toBeVisible();
},
},
{
action: "New calendar",
dialog: "New calendar",
assertCreateControls: async (page) => {
await expect(page.getByLabel("Name")).toBeVisible();
await expect(page.getByRole("button", { name: "Create calendar" })).toBeVisible();
},
},
{
action: "Add to shopping",
dialog: "Add to shopping",
assertCreateControls: async (page) => {
await expect(page.getByLabel("Item")).toBeVisible();
await expect(page.getByRole("button", { name: "Add" })).toBeVisible();
},
},
{
action: "Add to tasks",
dialog: "Add to tasks",
assertCreateControls: async (page) => {
await expect(page.getByLabel("Task")).toBeVisible();
await expect(page.getByRole("button", { name: "Add" })).toBeVisible();
},
},
{
action: "New list",
dialog: "New list",
assertCreateControls: async (page) => {
await expect(page.getByLabel("Type")).toBeVisible();
await expect(page.getByLabel("Name")).toBeVisible();
await expect(page.getByRole("button", { name: "Create list" })).toBeVisible();
},
},
{
action: "New note",
dialog: "New note",
assertCreateControls: async (page) => {
await expect(page.getByLabel("Title")).toBeVisible();
await expect(page.getByLabel("Body")).toBeVisible();
await expect(page.getByRole("button", { name: "Save note" })).toBeVisible();
},
},
{
action: "Add plant",
dialog: "Add plant",
assertCreateControls: async (page) => {
await expect(page.getByLabel("Name")).toBeVisible();
await expect(page.getByRole("button", { name: "Create" })).toBeVisible();
},
},
{
action: "Add container",
dialog: "Add container",
assertCreateControls: async (page) => {
await expect(page.getByLabel("Name")).toBeVisible();
await expect(page.getByLabel("Type")).toBeVisible();
await expect(page.getByRole("button", { name: "Create" })).toBeVisible();
},
},
{
action: "Log plant care",
dialog: "Log plant care",
assertCreateControls: async (page) => {
const plantField = page.getByLabel("Plant");
const noPlants = page.getByText("No plants yet");
await expect(plantField.or(noPlants)).toBeVisible();
if (await plantField.isVisible()) {
await expect(page.getByRole("button", { name: "Log care" })).toBeVisible();
}
},
},
{
action: "Record bang",
dialog: "Record a bang",
assertCreateControls: async (page) => {
await expect(page.getByLabel("Date")).toBeVisible();
await expect(page.getByRole("button", { name: "Record it" })).toBeVisible();
},
},
];
test.describe("quick-add create UI", () => {
for (const { action, dialog, assertCreateControls } of fabQuickAddCases) {
test(`FAB opens ${action} create dialog on dashboard`, async ({ page }) => {
await ensureSignedIn(page);
await openFabAction(page, action);
await expect(page.getByRole("dialog", { name: dialog })).toBeVisible();
await assertCreateControls(page);
});
}
test("command palette opens new event dialog on dashboard", async ({ page }) => {
await ensureSignedIn(page);
const isMac = await page.evaluate(() => /Mac|iPhone|iPad|iPod/.test(navigator.platform));
await page.keyboard.press(isMac ? "Meta+k" : "Control+k");
await expect(page.getByRole("dialog", { name: "Command palette" })).toBeVisible();
await page.getByRole("option", { name: /New event/i }).click();
await expect(page).toHaveURL("/");
await expect(page.getByRole("dialog", { name: "New event" })).toBeVisible();
await expect(page.getByLabel("Title")).toBeVisible();
await expect(page.getByRole("button", { name: "Save event" })).toBeVisible();
});
});