This is the direction
Proceed to a full design write-up
diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..aabacf1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +node_modules +.next +.git +.env* +!.env.example +docs +tests +.superpowers +pocketbase +*.md +.vscode +.DS_Store +npm-debug.log* diff --git a/.gitignore b/.gitignore index 29ae9a8..8f95688 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ node_modules/ next-env.d.ts *.tsbuildinfo pocketbase/pocketbase -pocketbase/pb_data/ \ No newline at end of file +pocketbase/pb_data/.superpowers/ diff --git a/.superpowers/brainstorm/24862-1789651152/content/combined-b-c.html b/.superpowers/brainstorm/24862-1789651152/content/combined-b-c.html new file mode 100644 index 0000000..c440dc7 --- /dev/null +++ b/.superpowers/brainstorm/24862-1789651152/content/combined-b-c.html @@ -0,0 +1,96 @@ +
Two things had to be nailed down before this could be real rather than a mockup: what the banner colour does with an arbitrary client hex, and what the progress bar / status line actually measure.
+ + + +Same real data (WF, RJBULK, Mega Infrastructure) — three different visual treatments. Click the one that's closest to what you want, even if it's not perfect yet.
+ + + +Client colour moves to a left-edge stripe on the tile itself, instead of a separate dot on both the header and the tile. Less border noise, more whitespace, one clean line of status text instead of pills.
+Each tile becomes a tiny dashboard preview: a progress bar and a status line ("2 due this week", "No activity yet") instead of a bare number. More useful at a glance, more visually busy.
+Each client gets a solid-colour header bar instead of a small dot + label. Strongest visual separation between clients at a glance across a long list; tiles underneath stay flat and simple since the banner already carries the identity.
+These are rough — spacing, exact colours and card size are easy to tune once we pick a direction. You can also mix ideas from more than one (e.g. banners from C with the progress bar from B).
\ No newline at end of file diff --git a/.superpowers/brainstorm/24862-1789651152/state/server-stopped b/.superpowers/brainstorm/24862-1789651152/state/server-stopped new file mode 100644 index 0000000..b989ea2 --- /dev/null +++ b/.superpowers/brainstorm/24862-1789651152/state/server-stopped @@ -0,0 +1 @@ +{"reason":"idle timeout","timestamp":1789653732910} diff --git a/.superpowers/brainstorm/24862-1789651152/state/server.log b/.superpowers/brainstorm/24862-1789651152/state/server.log new file mode 100644 index 0000000..f0b8e41 --- /dev/null +++ b/.superpowers/brainstorm/24862-1789651152/state/server.log @@ -0,0 +1,4 @@ +{"type":"server-started","port":50672,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:50672","screen_dir":"/Volumes/EXT/DEVELOPMENT-FORGEJO/KANBANBOARD-DEV/.superpowers/brainstorm/24862-1789651152/content","state_dir":"/Volumes/EXT/DEVELOPMENT-FORGEJO/KANBANBOARD-DEV/.superpowers/brainstorm/24862-1789651152/state"} +{"type":"screen-added","file":"/Volumes/EXT/DEVELOPMENT-FORGEJO/KANBANBOARD-DEV/.superpowers/brainstorm/24862-1789651152/content/layout-directions.html"} +{"type":"screen-added","file":"/Volumes/EXT/DEVELOPMENT-FORGEJO/KANBANBOARD-DEV/.superpowers/brainstorm/24862-1789651152/content/combined-b-c.html"} +{"type":"server-stopped","reason":"idle timeout"} diff --git a/.superpowers/brainstorm/24862-1789651152/state/server.pid b/.superpowers/brainstorm/24862-1789651152/state/server.pid new file mode 100644 index 0000000..5d6a4d8 --- /dev/null +++ b/.superpowers/brainstorm/24862-1789651152/state/server.pid @@ -0,0 +1 @@ +24871 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f8997bb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,56 @@ +# Client Board — production image for Coolify. +# +# This container is the Next.js app ONLY. PocketBase is a separate, already- +# hosted service (see docs/superpowers memory: "Coolify-only PocketBase") — +# this Dockerfile does not run or bundle it. +# +# NEXT_PUBLIC_APP_URL and NEXT_PUBLIC_PB_URL are inlined into the client +# JavaScript bundle at BUILD time, not read at container start. Coolify must +# be told to pass them as build arguments (its "Build Variables" section), +# not just as runtime environment variables — a value only set at runtime +# will be missing from the browser bundle entirely. + +# ---- deps: install once, reused unless package*.json changes ---- +FROM node:22-alpine AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci + +# ---- builder: compile with the build-time public vars baked in ---- +FROM node:22-alpine AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +ARG NEXT_PUBLIC_APP_URL +ARG NEXT_PUBLIC_PB_URL +ENV NEXT_PUBLIC_APP_URL=$NEXT_PUBLIC_APP_URL +ENV NEXT_PUBLIC_PB_URL=$NEXT_PUBLIC_PB_URL +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN npm run build + +# ---- runner: the small image Coolify actually deploys ---- +FROM node:22-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +# Standalone's server only binds every interface when told to — left at the +# default it binds localhost-only, which is unreachable from outside the +# container. +ENV HOSTNAME=0.0.0.0 +ENV PORT=3000 + +RUN addgroup --system --gid 1001 nodejs \ + && adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +CMD ["node", "server.js"] diff --git a/docs/superpowers/plans/2026-09-17-phase-6-navigation-and-project-kind.md b/docs/superpowers/plans/2026-09-17-phase-6-navigation-and-project-kind.md new file mode 100644 index 0000000..00f437d --- /dev/null +++ b/docs/superpowers/plans/2026-09-17-phase-6-navigation-and-project-kind.md @@ -0,0 +1,1200 @@ +# Phase 6 — Navigation and Project Kind 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:** Group projects by client on the home page and in a new sidebar "Kanban" flyout that jumps straight into a project's board; make the dashboard the click target for a project tile; add a `kind` field so a project can be tagged `prototype` — full board, full dashboard, same permissions, just visually distinguished. + +**Architecture:** One new select field on `projects` (`kind: "project" | "prototype"`, backfilled to `"project"`). All new browsing surfaces (home page grouping, sidebar flyout) read with the caller's own PocketBase token, exactly like the existing home page and `/plan` — the Phase 1 read rules do the scoping, not a UI filter or a role branch anyone could forget. `AppShell` becomes an async server component that loads the sidebar's project tree once and passes it down, so no existing page needs to change to get the new nav. + +**Tech Stack:** Next.js 16 App Router · PocketBase 0.30 · Tailwind 4 · vitest + +**Spec:** `docs/superpowers/specs/2026-09-17-phase-6-navigation-and-project-kind-design.md` + +**Prerequisite:** Phase 1 complete (clients/projects/read-rules exist). No other phase is required. + +--- + +## What already exists — do not rebuild this + +The dashboard already has a "Back to board" link (`src/app/p/[clientSlug]/[projectSlug]/dashboard/page.tsx:66-73`), and the board's `TopBar` already links to "Dashboard" (`src/app/p/[clientSlug]/[projectSlug]/page.tsx`). **The dashboard ↔ board shortcut from the spec already works.** Task 5 verifies this directly instead of writing new code for it — if it turns out broken, fix it there, but do not add a second link. + +--- + +## File structure + +``` +scripts/ +└── setup-remote.ts # + ensureProjectKindField +src/ +├── lib/ +│ ├── pb/types.ts # + ProjectKind, kind on ProjectRecord +│ └── data/ +│ └── board.ts # + loadSidebarProjects, SidebarProjectGroup +├── app/ +│ ├── actions/ +│ │ └── projects.ts # createProject accepts kind +│ ├── page.tsx # grouped by client, prototype tag, links to dashboard +│ └── admin/projects/page.tsx # Kind column +└── components/ + ├── admin/ + │ └── project-form.tsx # Kind select + └── shell/ + ├── app-shell.tsx # async — loads the sidebar tree + ├── sidebar.tsx # renders KanbanNav + └── kanban-nav.tsx # NEW — the flyout +tests/ +└── integration/ + └── project-kind-and-nav.test.ts # NEW +``` + +--- + +## Task 1: The `kind` field + +**Files:** +- Modify: `scripts/setup-remote.ts` +- Modify: `src/lib/pb/types.ts` + +- [ ] **Step 1: Add the type** + +In `src/lib/pb/types.ts`, add beside the other project-related types (near `ProjectStatus`, line 5): + +```ts +export type ProjectKind = "project" | "prototype"; +``` + +Then add `kind` to `ProjectRecord`: + +```ts +export interface ProjectRecord extends BaseRecord { + client: string; + name: string; + slug: string; + description: string; + status: ProjectStatus; + kind: ProjectKind; + color: string; + repo_owner: string; + repo_name: string; + repo_url: string; + /** Server-only. Must never appear in a browser payload. */ + webhook_secret: string; + last_ingest_at: string; + session_gap_minutes: number; + pre_session_minutes: number; + post_session_minutes: number; +} +``` + +- [ ] **Step 2: Add the migration helper** + +In `scripts/setup-remote.ts`, add a new function beside `ensureProjectIngestFields` (after it, around line 183): + +```ts +/** + * Add the `kind` field to projects, for Phase 6's project/prototype + * distinction — a prototype is a full project (board, dashboard, same + * permissions), just tagged as not yet a committed piece of work. + * + * Backfilled to "project" for every existing row. Nothing already in + * production should read as a prototype just because the field is new. + * + * Re-runs are safe: the field is not duplicated, and backfill only touches + * rows where kind is still unset. + */ +async function ensureProjectKindField(pb: PocketBase): PromiseCreate a client first.
+ ); + } + + function submit(event: React.FormEvent) { + event.preventDefault(); + if (!name.trim() || !clientId) return; + setError(null); + + startTransition(async () => { + try { + await createProject({ + clientId, + name, + description, + repoOwner, + repoName, + color: DEFAULT_BRAND_COLOR, + kind, + }); + setName(""); + setDescription(""); + setRepoOwner(""); + setRepoName(""); + setKind("project"); + setOpen(false); + } catch (e) { + setError(e instanceof Error ? e.message : "Could not create that project"); + } + }); + } + + if (!open) { + return ( + + ); + } + + const field = + "w-full rounded-lg border border-border-subtle bg-panel px-3 py-2 text-sm text-text-primary outline-none focus:border-brand"; + + return ( + + ); +} +``` + +- [ ] **Step 3: Add a Kind column to the admin table** + +In `src/app/admin/projects/page.tsx`, add "Kind" to the headers and a cell: + +```tsx +
+ {project.repo_owner}/{project.repo_name}
+
+ ) : (
+ Not linked
+ )}
+ + {session.role === "owner" + ? "Create one from Manage, or run npm run seed for demo data." + : "Nothing has been shared with you yet."} +
++ {project.name} +
++ {client.name} +
+No projects yet.
+ ) : ( + groups.map((group) => ( ++ {group.client.name} +
+ )} + {group.projects.map((project) => ( + setOpen(false)} + className="flex items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-xs text-text-secondary transition-colors hover:bg-surface-raised hover:text-text-primary" + > + {project.name} + {project.kind === "prototype" && ( + + Prototype + + )} + + ))} +