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 @@ +

B + C combined, with real metrics instead of placeholders

+

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.

+ + + +
+
Preview: Projects, grouped with banners + activity cards
+
+ +
+
+ WF +
+
+
+
+
WFDash Web App
+
6 open · 14 done
+
+
Active 2 days ago
+
+
+
WFDASH Driver App
+
0 open · 0 done
+
+
No activity yet
+
+
+
+
+ +
+
+ RJBULK +
+
+
+
+
RJ Bulk App
+
0 open · 0 done
+
+
No activity yet
+
+
+
Diesel Depot AppPrototype
+
0 open · 0 done
+
+
No activity yet
+
+
+
+
+ +
+
+ +
+

What changed from the rough mockup, and why

+
+
+

Banner

+
    +
  • Soft tint (~14% of the client's colour) with the colour itself used for the text, not a solid fill — a client's arbitrary hex (say bright yellow, or near-white) would make banner text unreadable at full strength. This is a legibility fix, not a taste change.
  • +
+
+
+

Progress bar + status line

+
    +
  • Bar = done cards ÷ total cards on that project's board. Real, already computable, matches the "one honest metric" pattern used on the roadmap page's checklist %.
  • +
  • Status line = time since the project's most recent commit ("Active 2 days ago" / "No activity yet") rather than "2 due this week" from the rough mockup — target dates are sparse (most cards have none), but commit activity is the one thing this app already tracks reliably for every ingested project.
  • +
+
+
+
+ +
+
+
✓
+

This is the direction

Proceed to a full design write-up

+
+
+
✗
+

Close, but adjust something

Tell me what in the terminal

+
+
\ No newline at end of file diff --git a/.superpowers/brainstorm/24862-1789651152/content/layout-directions.html b/.superpowers/brainstorm/24862-1789651152/content/layout-directions.html new file mode 100644 index 0000000..15ef079 --- /dev/null +++ b/.superpowers/brainstorm/24862-1789651152/content/layout-directions.html @@ -0,0 +1,123 @@ +

Which direction for the Projects screen?

+

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.

+ + + +
+ +
+
+
+
WF
+
+
+
WFDash Web App
+
6 open cards
+
+
+
WFDASH Driver App
+
0 open cards
+
+
+
+
+
RJBULK
+
+
+
RJ Bulk App
+
0 open cards
+
+
+
Diesel Depot App
+
Prototype · 0 open
+
+
+
+
+
+

A — Minimal & Spacious

+

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.

+
+
+ +
+
+
+
WF
+
+
+
WFDash Web App
+
6 open · 2 due this week
+
+
+
+
WFDASH Driver App
+
No activity yet
+
+
+
+
+
+
RJBULK
+
+
+
RJ Bulk App Prototype
+
No activity yet
+
+
+
+
+
+
+

B — Richer Data Cards

+

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.

+
+
+ +
+
+
WF
+
+
+
WFDash Web App
+
6 open cards
+
+
+
WFDASH Driver App
+
0 open cards
+
+
+
RJBULK
+
+
+
RJ Bulk App
+
0 open cards
+
+
+
Diesel Depot App Prototype
+
0 open cards
+
+
+
+
+

C — Coloured Client Banners

+

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): Promise { + const projects = await pb.collections.getOne("projects"); + const have = new Set(projects.fields.map((f: { name: string }) => f.name)); + + if (!have.has("kind")) { + const fields = [...(projects.fields as unknown[])]; + fields.push({ + name: "kind", + type: "select", + required: false, + maxSelect: 1, + values: ["project", "prototype"], + presentable: false, + hidden: false, + system: false, + }); + await pb.collections.update(projects.id, { fields }); + console.log(" + projects field: kind"); + } else { + console.log(" = projects.kind already present"); + } + + const unset = await pb.collection("projects").getFullList({ + filter: 'kind = ""', + fields: "id", + }); + for (const project of unset) { + await pb.collection("projects").update(project.id, { kind: "project" }); + } + console.log(` = project kind backfilled (${unset.length} row(s))`); +} +``` + +- [ ] **Step 3: Call it from `main()`** + +In `main()`, directly after the existing `await ensureProjectIngestFields(pb);` call (around line 898): + +```ts + // Add ingest-related fields to projects (only if missing — the projects + // collection already exists from earlier migrations). + await ensureProjectIngestFields(pb); + + // Add the project/prototype kind field for Phase 6. + await ensureProjectKindField(pb); +``` + +- [ ] **Step 4: Run it against the live PocketBase** + +```bash +set -a && source .env.local && set +a && npm run setup-remote +``` + +Expected: prints `+ projects field: kind` (or `= projects.kind already present` on +a re-run) followed by `= project kind backfilled (N row(s))`. + +- [ ] **Step 5: Verify the build** + +```bash +npm run build +``` + +Expected: build succeeds. + +- [ ] **Step 6: Commit** + +```bash +git add scripts/setup-remote.ts src/lib/pb/types.ts +git commit -m "feat(projects): add kind field (project | prototype)" +``` + +--- + +## Task 2: Creating and displaying a project's kind + +**Files:** +- Modify: `src/app/actions/projects.ts` +- Modify: `src/components/admin/project-form.tsx` +- Modify: `src/app/admin/projects/page.tsx` +- Modify: `src/app/page.tsx` + +- [ ] **Step 1: Accept `kind` in `createProject`** + +In `src/app/actions/projects.ts`, change the `createProject` signature and the +record it creates: + +```ts +export async function createProject(input: { + clientId: string; + name: string; + description: string; + repoOwner: string; + repoName: string; + color: string; + kind?: ProjectRecord["kind"]; +}): Promise<{ id: string }> { + await requireOwner(); + const pb = await getServerPb(); + + const name = input.name.trim().slice(0, 120); + if (!name) throw new Error("A name is required"); + + const siblings = await pb.collection("projects").getFullList({ + filter: `client = "${input.clientId}"`, + fields: "slug", + }); + + const project = await pb.collection("projects").create({ + client: input.clientId, + name, + slug: uniqueSlug(slugify(name), siblings.map((p) => p.slug)), + description: input.description.slice(0, 20000), + status: "active", + kind: input.kind ?? "project", + color: input.color || "#2f6dad", + repo_owner: input.repoOwner.trim(), + repo_name: input.repoName.trim(), + repo_url: "", + }); +``` + +The rest of the function (column seeding, error handling, `revalidatePath`) is +unchanged. + +- [ ] **Step 2: Add the Kind toggle to the create form** + +In `src/components/admin/project-form.tsx`, add state and a select: + +```tsx +"use client"; + +import { useState, useTransition } from "react"; +import { Plus } from "lucide-react"; + +import { createProject } from "@/app/actions/projects"; +import { DEFAULT_BRAND_COLOR } from "@/lib/brand-default"; +import type { ClientRecord, ProjectRecord } from "@/lib/pb/types"; + +export function NewProjectForm({ clients }: { clients: ClientRecord[] }) { + const [open, setOpen] = useState(false); + const [clientId, setClientId] = useState(clients[0]?.id ?? ""); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [repoOwner, setRepoOwner] = useState(""); + const [repoName, setRepoName] = useState(""); + const [kind, setKind] = useState("project"); + const [pending, startTransition] = useTransition(); + const [error, setError] = useState(null); + + if (clients.length === 0) { + return ( +

Create 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 ( +
+
+ + + +
+ +