From 2d87cd505b4494bf02f488f02a7bb9dd7216b2b7 Mon Sep 17 00:00:00 2001 From: Dave Date: Sat, 19 Sep 2026 07:52:09 +0200 Subject: [PATCH] chore(config): update .gitignore and enhance Next.js configuration Added .superpowers directory to .gitignore to prevent tracking of specific Pocketbase data. Updated Next.js configuration to enable standalone output for optimized Docker image builds, reducing the need for the full node_modules tree. --- .dockerignore | 13 + .gitignore | 2 +- .../content/combined-b-c.html | 96 ++ .../content/layout-directions.html | 123 ++ .../24862-1789651152/state/server-stopped | 1 + .../24862-1789651152/state/server.log | 4 + .../24862-1789651152/state/server.pid | 1 + Dockerfile | 56 + ...-17-phase-6-navigation-and-project-kind.md | 1200 +++++++++++++++++ next.config.ts | 4 + public/.gitkeep | 0 11 files changed, 1499 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 .superpowers/brainstorm/24862-1789651152/content/combined-b-c.html create mode 100644 .superpowers/brainstorm/24862-1789651152/content/layout-directions.html create mode 100644 .superpowers/brainstorm/24862-1789651152/state/server-stopped create mode 100644 .superpowers/brainstorm/24862-1789651152/state/server.log create mode 100644 .superpowers/brainstorm/24862-1789651152/state/server.pid create mode 100644 Dockerfile create mode 100644 docs/superpowers/plans/2026-09-17-phase-6-navigation-and-project-kind.md create mode 100644 public/.gitkeep 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 ( +
+
+ + + +
+ +