From 4663a45b45338751b9b041831a9f40244f2281bc Mon Sep 17 00:00:00 2001 From: Hammer Date: Thu, 29 Jan 2026 07:07:45 +0000 Subject: [PATCH] memory: log task worker session - due dates, subtasks, task page --- AGENTS.md | 7 + HEARTBEAT.md | 18 +++ TOOLS.md | 13 ++ donovan-portfolio | 1 + hammer-queue | 1 + hook-proxy.log | 1 + hook-proxy.ts | 21 +++ memory/2026-01-28.md | 137 ++++++++++++++++++ memory/2026-01-29.md | 51 +++++++ memory/heartbeat-state.json | 12 +- network-app-api | 1 + network-app-mobile | 1 + network-app-web | 1 + notes | 2 +- skills/app-builder.skill | Bin 0 -> 13870 bytes skills/app-builder/SKILL.md | 132 +++++++++++++++++ .../app-builder/assets/template/.env.example | 20 +++ skills/app-builder/assets/template/.gitignore | 5 + .../assets/template/api/Dockerfile | 7 + .../assets/template/api/drizzle.config.ts | 10 ++ .../assets/template/api/package.json | 23 +++ .../assets/template/api/src/db/index.ts | 6 + .../assets/template/api/src/db/schema.ts | 54 +++++++ .../assets/template/api/src/index.ts | 14 ++ .../assets/template/api/src/lib/auth.ts | 17 +++ .../assets/template/api/src/routes/hammer.ts | 38 +++++ .../template/docker-compose.dokploy.yml | 27 ++++ .../assets/template/docker-compose.yml | 40 +++++ .../assets/template/web/Dockerfile | 12 ++ .../assets/template/web/nginx.conf | 17 +++ .../assets/template/web/package.json | 28 ++++ skills/app-builder/references/deploy.md | 66 +++++++++ .../app-builder/references/legal-payments.md | 47 ++++++ skills/app-builder/references/migrations.md | 49 +++++++ skills/app-builder/references/rollback.md | 43 ++++++ skills/app-builder/scripts/scaffold.sh | 55 +++++++ skills/debugging/SKILL.md | 68 +++++++++ todo-app | 1 + todo-app-web | 1 + 39 files changed, 1044 insertions(+), 3 deletions(-) create mode 160000 donovan-portfolio create mode 160000 hammer-queue create mode 100644 hook-proxy.log create mode 100644 hook-proxy.ts create mode 100644 memory/2026-01-28.md create mode 100644 memory/2026-01-29.md create mode 160000 network-app-api create mode 160000 network-app-mobile create mode 160000 network-app-web create mode 100644 skills/app-builder.skill create mode 100644 skills/app-builder/SKILL.md create mode 100644 skills/app-builder/assets/template/.env.example create mode 100644 skills/app-builder/assets/template/.gitignore create mode 100644 skills/app-builder/assets/template/api/Dockerfile create mode 100644 skills/app-builder/assets/template/api/drizzle.config.ts create mode 100644 skills/app-builder/assets/template/api/package.json create mode 100644 skills/app-builder/assets/template/api/src/db/index.ts create mode 100644 skills/app-builder/assets/template/api/src/db/schema.ts create mode 100644 skills/app-builder/assets/template/api/src/index.ts create mode 100644 skills/app-builder/assets/template/api/src/lib/auth.ts create mode 100644 skills/app-builder/assets/template/api/src/routes/hammer.ts create mode 100644 skills/app-builder/assets/template/docker-compose.dokploy.yml create mode 100644 skills/app-builder/assets/template/docker-compose.yml create mode 100644 skills/app-builder/assets/template/web/Dockerfile create mode 100644 skills/app-builder/assets/template/web/nginx.conf create mode 100644 skills/app-builder/assets/template/web/package.json create mode 100644 skills/app-builder/references/deploy.md create mode 100644 skills/app-builder/references/legal-payments.md create mode 100644 skills/app-builder/references/migrations.md create mode 100644 skills/app-builder/references/rollback.md create mode 100755 skills/app-builder/scripts/scaffold.sh create mode 100644 skills/debugging/SKILL.md create mode 160000 todo-app create mode 160000 todo-app-web diff --git a/AGENTS.md b/AGENTS.md index 796e9cc..7bee665 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,6 +13,7 @@ Before doing anything else: 2. Read `USER.md` — this is who you're helping 3. Read `memory/YYYY-MM-DD.md` (today + yesterday) for recent context 4. **If in MAIN SESSION** (direct chat with your human): Also read `MEMORY.md` +5. **On new session greeting**: Show todos from `notes/tasks/personal-tasks.md` Don't ask permission. Just do it. @@ -48,6 +49,12 @@ Capture what matters. Decisions, context, things to remember. Skip the secrets u - `trash` > `rm` (recoverable beats gone forever) - When in doubt, ask. +## Code Habits + +- **ALWAYS push your code** after making changes. Don't leave commits sitting locally. +- After any coding session: `git add -A && git commit -m "..." && git push` +- If you forget, Donovan will be annoyed. Don't forget. + ## External vs Internal **Safe to do freely:** diff --git a/HEARTBEAT.md b/HEARTBEAT.md index 5973eb7..dd6bf63 100644 --- a/HEARTBEAT.md +++ b/HEARTBEAT.md @@ -7,5 +7,23 @@ Check `gmail unread` for new messages in hammer7839283@gmail.com. If new mail from Donovan (forwarded), read and summarize or take action. Track last check in `memory/heartbeat-state.json`. +### Todo App (2-3x daily) +Check the todo app at https://api.todo.donovankelly.xyz for updates: +- Auth: sign in as hammer@donovankelly.xyz (service account, ID: 1HltUpL3R0qZkVxIu0oQ3UoqBCHuuMpV) + - Get Hammer API key or creds from Bitwarden +- Check for tasks assigned to Hammer (assigneeId = Hammer's user ID) +- Check for new tasks or recently updated tasks +- Check for tasks with upcoming due dates (next 48h) +- Report anything urgent to Donovan +Track last check in `memory/heartbeat-state.json`. + ### Skills (weekly) Check clawdhub for new useful skills. + +### Clawdbot Community (weekly) +Research what people are doing with Clawdbot: +- Search YouTube for Clawdbot videos/tutorials, get transcripts +- Search X/Twitter for Clawdbot posts and workflows +- Check Discord community for ideas +- Compile interesting workflow ideas and add to `notes/tasks/ideas.md` +- Summarize findings for Donovan diff --git a/TOOLS.md b/TOOLS.md index dfc6785..219e222 100644 --- a/TOOLS.md +++ b/TOOLS.md @@ -4,6 +4,12 @@ Skills define *how* tools work. This file is for *your* specifics — the stuff ## Rules +### 🔒 Never Use HTTP for External Communication +- All external communication MUST use HTTPS — no exceptions +- Never expose services over plain HTTP on public IPs +- If a service only supports HTTP internally, put it behind a TLS-terminating reverse proxy +- This applies to webhooks, APIs, and any cross-server communication + ### 🔐 Never Ask for Secrets in Chat - Don't ask Donovan to paste API keys, passwords, or credentials in messages - Instead: walk him through adding them to Bitwarden or `~/.clawdbot/.env` himself @@ -30,9 +36,16 @@ Things like: - Bitwarden CLI installed at `/home/clawdbot/.npm-global/bin/bw` - API credentials go in `~/.clawdbot/.env` (BW_CLIENTID, BW_CLIENTSECRET, BW_PASSWORD) - Bitwarden data: `~/.config/Bitwarden CLI/` +- **Always store credentials in the shared org vault:** + - Organization: `4e3ffbdb-0f8b-4f7a-a276-b0a30160e33f` (Hammer's Credentials) + - Collection: `320f9e42-607e-4180-8533-b0a30160e342` (Default collection) + - Set `organizationId` and `collectionIds` when creating items — never leave them in personal vault ### Infrastructure - Host: Hostinger VPS, Ubuntu +- VPS IP: 72.60.68.214 +- Domain: hammer.donovankelly.xyz (points to this VPS) +- Dokploy server: 191.101.0.153 (separate VPS, hosts queue app etc.) - User: clawdbot (sudo, needs password) ### Email diff --git a/donovan-portfolio b/donovan-portfolio new file mode 160000 index 0000000..e49a4a4 --- /dev/null +++ b/donovan-portfolio @@ -0,0 +1 @@ +Subproject commit e49a4a451234461dcb70e0a11b548986efb2da80 diff --git a/hammer-queue b/hammer-queue new file mode 160000 index 0000000..e874caf --- /dev/null +++ b/hammer-queue @@ -0,0 +1 @@ +Subproject commit e874cafbecb78e60d169b827574f4d8add01eb85 diff --git a/hook-proxy.log b/hook-proxy.log new file mode 100644 index 0000000..b0bab10 --- /dev/null +++ b/hook-proxy.log @@ -0,0 +1 @@ +Hook proxy listening on 0.0.0.0:18790 diff --git a/hook-proxy.ts b/hook-proxy.ts new file mode 100644 index 0000000..002b21c --- /dev/null +++ b/hook-proxy.ts @@ -0,0 +1,21 @@ +const server = Bun.serve({ + port: 18790, + hostname: "0.0.0.0", + async fetch(req) { + const url = new URL(req.url); + if (!url.pathname.startsWith("/hooks")) { + return new Response("Not Found", { status: 404 }); + } + const target = `http://127.0.0.1:18789${url.pathname}${url.search}`; + const resp = await fetch(target, { + method: req.method, + headers: req.headers, + body: req.method !== "GET" ? await req.text() : undefined, + }); + return new Response(resp.body, { + status: resp.status, + headers: resp.headers, + }); + }, +}); +console.log(`Hook proxy listening on 0.0.0.0:${server.port}`); diff --git a/memory/2026-01-28.md b/memory/2026-01-28.md new file mode 100644 index 0000000..faa8a0c --- /dev/null +++ b/memory/2026-01-28.md @@ -0,0 +1,137 @@ +# 2026-01-28 + +## Todo App Deployment & Features + +### Deployed to Dokploy +- **API** (`todo-app-v2`) deployed and running at `https://api.todo.donovankelly.xyz` + - Source: raw compose, builds from `https://git.infra.nkode.tech/hammer/todo-app.git` + - Compose ID: `e07fMO8TXcHI_SkKqBWrl` + - ENV: APP_URL=https://app.todo.donovankelly.xyz (was wrong before, pointed to API) + - Hammer API key: stored in Dokploy env as HAMMER_API_KEY +- **Frontend** (`todo-app-web`) deployed at `https://app.todo.donovankelly.xyz` + - Source: raw compose, builds from `https://git.infra.nkode.tech/hammer/todo-app-web.git` + - Compose ID: `ofMFmzQhEYK-3LfH5QmU_` +- **DB** (`todo-app-db`) Postgres running on Dokploy + - Postgres ID: `WrvHLAc1kaqsrpTaSGOG4` + - Not externally exposed (firewall blocks) +- Dokploy API: `https://app.dokploy.com`, auth via `X-API-Key` header, key in Bitwarden ("dokploy api key") +- Dokploy server: 191.101.0.153 (Hostzinger kvm2), server ID: QppaqcotG-I-6fUYCNolg +- Deploy pattern: `compose.deploy` via tRPC API, poll `deployment.allByCompose` for status + +### Fixed cross-subdomain auth +- BetterAuth cookies were scoped to `api.todo.donovankelly.xyz` only +- Added `crossSubDomainCookies` config with domain `.donovankelly.xyz` +- Added `https://app.todo.donovankelly.xyz` to trustedOrigins + +### Fixed project loading +- `getProject` API route had invalid `isArchived` filter on tasks table (column doesn't exist on tasks) +- Removed the bad filter, projects load correctly now + +### Fixed invite links +- APP_URL env was set to API domain, generating wrong invite URLs +- Updated to `https://app.todo.donovankelly.xyz` + +### User accounts +- Donovan's admin account: `donovan@donovankelly.xyz`, creds in BW shared vault "Todo App (donovankelly.xyz)" +- Hammer service account: `hammer@donovankelly.xyz`, ID: `1HltUpL3R0qZkVxIu0oQ3UoqBCHuuMpV`, role: service +- BetterAuth has `auth.api.setPassword({ body: { userId, newPassword } })` for password resets + +### Features implemented (frontend) +- **Unit tests:** 60 tests (API client, auth store, tasks store, utils) +- **Project creation:** Inline form in sidebar with color picker +- **Task assignment:** Assignee selector + avatar display on tasks +- **Kanban board:** Board view at `/project/:id/board` with section columns +- **Collapsible sidebar:** Toggle to icon-only mode, persists in localStorage +- **Task detail panel:** Right slide-over with editable fields (title, description, due date, priority, assignee, project) +- **Due date picker:** Fixed broken hidden input, now visible styled input with clear button +- **Completed tasks archive:** Collapsible "Completed" section at bottom of views + "Done" column in board +- **Project selector in task detail:** Move tasks between projects +- **Section management:** Add/rename/delete sections from board view +- **Admin: role selector on invites** — pick admin/user when inviting +- **Admin: change user roles** — dropdown in users table +- **Admin: password reset** — key icon to reset any user's password + +### Infrastructure notes +- Local Caddy container (`todo-caddy`) on this VPS is unnecessary — domains point to Dokploy +- Git remotes: `gitea` for todo-app, `origin` for todo-app-web + +### Network App Frontend +- Built SPA at `/home/clawdbot/clawd/network-app-web` (Vite + React + TS + Tailwind) +- Gitea: `hammer/network-app-web` +- Dokploy compose ID: `Sa1LrtH5uu-a7chrtebXb` +- Domain: `https://app.donovankelly.xyz` +- API at: `https://api.donovankelly.xyz` (compose ID: `UKrNvUyMCdaSWkl6DcAGA`) +- DB: `network-app-db` (Postgres, compose ID: `KzFkJETXrW_oMaiPsUb2o`) +- Features: Clients CRM, Events tracking, AI email generation, Profile/settings +- Pages: Dashboard, Clients list, Client detail, Events, Emails, Settings, Login + +### Bitwarden rule +- Donovan confirmed: ALWAYS store credentials in Hammer's Credentials org → Default collection +- Updated TOOLS.md with org/collection IDs + +### Lessons learned +- Always create BW items in shared org vault from the start +- Dokploy compose services: use `sourceType: raw` with git URL in build context (not `sourceType: git`) +- BetterAuth password hashing: don't try to manually hash with bcrypt — use `auth.api.setPassword()` or `auth.api.signUpEmail()` +- APP_URL must point to frontend, not API (used for invite link generation) + +### Hammer Queue — Task System +- **URL:** https://queue.donovankelly.xyz +- **API auth:** Bearer token from BW "Hammer Queue (donovankelly.xyz)" → API_BEARER_TOKEN +- **Creds saved to:** ~/.clawdbot/.env as HAMMER_QUEUE_URL + HAMMER_QUEUE_TOKEN +- **Purpose:** When Donovan asks me to do something, log it here immediately so it survives context compaction +- **Active tasks:** "update queue app" (detail panel, webhook integration) +- **High priority queued:** That2ndGuy pitch deck, Network App dev +- **Lesson:** ALWAYS write tasks to the queue when asked to do something. Don't rely on conversation memory. + +### Dokploy API Key Rotated +- Donovan added new key to BW "dokploy api key" (64 chars) +- Updated ~/.clawdbot/.env with DOKPLOY_API_KEY +- Old key was expiring/rotating — new one works + +### Hammer Queue — Continued Development (Late Session) + +#### Dokploy Access +- Created Dokploy account: hammer7839283@gmail.com, role: member +- Creds in BW: "Dokploy (app.dokploy.com)" in shared org vault +- Member role can't access compose services (authorization error) — need admin for full UI access +- Dokploy API has NO endpoint to read deployment build logs — logs only via UI WebSocket or SSH +- Learned: `deployment.allByCompose` works with API key auth for listing deployments + +#### Deploy Debugging Saga +- 7 consecutive deploy failures, root causes: + 1. **Unused `useState` import** in TaskCard.tsx — TypeScript strict mode (`tsc -b`) rejected it, Docker frontend build failed silently + 2. **`serial` column type** — `drizzle-kit push` can't add serial to existing table with data. Fixed by using `integer` + app-level sequencing + startup backfill +- **Key lesson:** ALWAYS run `bun run build` locally before deploying. A 2-second local build catches what 30 minutes of API spelunking can't. +- Created debugging skill at `skills/debugging/SKILL.md` — living document, update with every new lesson + +#### Features Shipped (HQ-15 completed) +- Sequential task IDs: HQ-1, HQ-2, etc. (integer column + backfill on startup) +- API resolves tasks by UUID, number, or HQ-N prefix +- GET /api/tasks/:id endpoint +- Editable task fields in detail panel: click-to-edit title/description, clickable priority/source selectors +- Webhook env vars (CLAWDBOT_HOOK_URL, CLAWDBOT_HOOK_TOKEN) deployed to Dokploy compose +- Compose file updated to pass webhook vars to backend service + +#### Queue App Infrastructure +- Compose ID: `kBdwrcZodIRyNIvQ-wrzG` +- Git remote: `origin` → `https://git.infra.nkode.tech/hammer/hammer-queue.git` +- Deploy pattern: push to Gitea → `compose.deploy` via Dokploy API → poll `deployment.allByCompose` +- Frontend: Vite + React + TS + Tailwind, builds with `bun run build` (tsc -b && vite build) +- Backend: Elysia + Drizzle + Postgres, starts with `bun run db:push && bun run start` + +#### Task Management Rules (from Donovan) +- When asked to do something → add to Hammer Queue immediately (survives context compaction) +- If task stalls or stops → mark it as such, move out of active +- Don't leave tasks "active" while doing something else +- Break large tasks into smaller ones when shipping partial work + +#### New Tasks Created +- HQ-16: Maintain Debugging Skill (living document) +- HQ-17: Queue App: Projects with Context (remaining from HQ-15) + +#### Hammer Queue Connection Info +- URL: https://queue.donovankelly.xyz +- API auth: Bearer token from BW "Hammer Queue (donovankelly.xyz)" → API_BEARER_TOKEN +- Creds in ~/.clawdbot/.env: HAMMER_QUEUE_URL, HAMMER_QUEUE_TOKEN +- Also has HAMMER_QUEUE_API_KEY (older, may be redundant) diff --git a/memory/2026-01-29.md b/memory/2026-01-29.md new file mode 100644 index 0000000..5cf5613 --- /dev/null +++ b/memory/2026-01-29.md @@ -0,0 +1,51 @@ +# 2026-01-29 + +## Lessons Learned +- **Don't update Donovan in chat about task progress** — put comments and updates directly in the task (HammerQueue or wherever the task lives). He doesn't want chat noise for status updates. +- **Never expose services publicly without asking first** — I enabled hammer.donovankelly.xyz which exposed the Clawdbot Control UI to anyone. Always ask before making something internet-facing. +- **Never use HTTP for external communication** — all cross-server comms must be HTTPS. Added to TOOLS.md rules. +- **Always build and test before pushing** — run `bun run build` (or equivalent) locally before pushing to git. Dokploy builds from git, so broken code = failed deploys. + +## Hammer Queue Webhook Fix Verified +- Cron job confirmed the webhook integration is working (3:59 AM UTC) + +## Hammer Queue → Hammer Dashboard +- Renamed domain from queue.donovankelly.xyz to dash.donovankelly.xyz via Dokploy API +- Dokploy is at app.dokploy.com (cloud), API key in Bitwarden +- Compose ID: kBdwrcZodIRyNIvQ-wrzG +- Fixed TS build errors (unused var, useEffect return type) before successful deploy +- hammer.donovankelly.xyz Caddy proxy DISABLED — exposed Clawdbot Control UI without auth. Must add auth layer before re-enabling. + +## Dashboard Improvements (Task Worker) - 6:00 AM UTC +- Added Dashboard overview page at `/` — stats grid, active task cards, up-next queue, recent activity feed, recently completed +- Added progress note input UI in TaskDetailPanel (textarea + Cmd+Enter + Add button) +- Added search bar and priority filter to Queue page +- Committed chat backend relay code (WebSocket proxy from dashboard to Clawdbot gateway) +- Gateway-relay.ts now falls back to VITE_WS_TOKEN env var +- Completed "Save/cancel button" task (was already implemented, just marked done) +- Chat still blocked: needs Caddy WSS proxy re-enabled on hammer.donovankelly.xyz (restricted to WS, not control UI) +- Three deploys to Dokploy + +## Task Worker: Due Dates, Subtasks, Task Detail Page - 7:00 AM UTC +- Added `due_date` and `subtasks` JSONB columns to tasks schema +- Backend: full CRUD for subtasks (add/toggle/delete at /api/tasks/:id/subtasks) +- Backend: dueDate support in create/update task endpoints +- TaskDetailPanel: due date picker with overdue/due-soon badges, subtask checklist with progress bar +- New TaskPage component: full-page task view at /task/HQ-{number} routes +- Dashboard cards now link to /task/HQ-{number}, show subtask progress bars and due date badges +- Frontend types updated with assigneeName, assigneeId, dueDate, subtasks +- Drizzle migration 0001_mighty_callisto.sql (uses db:push on deploy) +- Deployed to Dokploy +- Chat still blocked: needs Caddy (no sudo access to install) + +## Projects Feature (HQ-17) - 5:00 AM UTC +- Built full Projects feature for Hammer Dashboard +- Backend: `projects` table (name, description, context, repos, links), full CRUD API at `/api/projects` +- Backend: added `projectId` FK on tasks table, supported in create/update task APIs +- Frontend: Projects page with card grid, detail view, context editor, repo list, task assignment +- Frontend: Project selector added to TaskDetailPanel (dropdown in task detail) +- Sidebar nav now: Queue → Projects → Chat +- Created initial projects: "Hammer Dashboard" (with full context), "Network App (NWM CRM)" +- Assigned 7 dashboard tasks to Hammer Dashboard project, 1 task to Network App +- HQ-17 completed, HQ-21 updated with progress +- All deployed to dash.donovankelly.xyz via Dokploy diff --git a/memory/heartbeat-state.json b/memory/heartbeat-state.json index cc95a36..61fb961 100644 --- a/memory/heartbeat-state.json +++ b/memory/heartbeat-state.json @@ -1,6 +1,14 @@ { "lastChecks": { - "email": null, - "skills": null + "email": 1769660400, + "todoApp": 1738094400, + "hammerQueue": 1769660400, + "skills": 1769523930, + "community": null + }, + "notes": { + "skills": "clawdhub search timed out", + "todoApp": "API key in .env is placeholder 'hammer-todo-key'. Auth still fails.", + "hammerQueue": "Primary task system. Creds in .env. Tasks now have taskNumber (HQ-N). API resolves by UUID, number, or HQ-N. Cron task-worker runs every 30min. assigneeId/assigneeName fields now supported." } } diff --git a/network-app-api b/network-app-api new file mode 160000 index 0000000..11ee9b9 --- /dev/null +++ b/network-app-api @@ -0,0 +1 @@ +Subproject commit 11ee9b946f296a3cf6145d36ecb1a8b11e67d854 diff --git a/network-app-mobile b/network-app-mobile new file mode 160000 index 0000000..b191cfe --- /dev/null +++ b/network-app-mobile @@ -0,0 +1 @@ +Subproject commit b191cfe083285417d035cb02cb56bd967cdf495e diff --git a/network-app-web b/network-app-web new file mode 160000 index 0000000..b6de50b --- /dev/null +++ b/network-app-web @@ -0,0 +1 @@ +Subproject commit b6de50ba5e1874c5c2a9f70f6883cc2dfba8e38d diff --git a/notes b/notes index 4f14da9..a05cead 160000 --- a/notes +++ b/notes @@ -1 +1 @@ -Subproject commit 4f14da9136b5acff5d270a93191895bbb2c77fce +Subproject commit a05cead8ed1010921492b7a3f59b626b813cffca diff --git a/skills/app-builder.skill b/skills/app-builder.skill new file mode 100644 index 0000000000000000000000000000000000000000..ccb433083e394f836dc54cb63e9ef6b9c98c83b6 GIT binary patch literal 13870 zcma)?1yEeswuW(cf_rdx5AGh^gEsCO+$FfXLlYc=y9Rgn009EQ9fIZc+*kL7J1=u* zpmuc?ReY!STKivT$$l#b3H1sL3=9rzu zI5Dfr$;ity*_!%j{j^)RNE)TqMEL) zRY^8+lZ}i@rInLNE+<-zfR@=~+oCX9QrT@;8FA|8VaHC;`*=|&tqoLv9ROW766TY6`oYg3Op zf*qNciBfq9cf4zK98u4PUbqN`OsBhd8(}1bQfDGG!c1h zq&ftp^6TA6F-C>VTlb*oHERdZB)tj@0_WfF75y?R+Ov4Yxl5%>^ZB|vWG4?1n=pBx z#Rm;WEDx{UFtO~S1bl?_9)Wa%*u4$1GAN}}K#JRxUEEvB8hJ|a9!7h+kgGdPGRis{ zH?9^X5pAK}5FH-6hrDfvKYy9L4O1pG7IisB047%K=jo@P0RXoubxY_uf!e*2?$aSk z3)I))+0G`gQ3Vj)R`kx6?nev}``B&>PW~2y}4N6y~RHr_==Ml2;U3( zazZYRKO*Sj0CdP-w^d=2#*eC-H3j;5(pnUX2iyr-FXc0EJ2;k%5PsY(t0*Ur&@|tw zfubQuQ>XFQ_2k6iX%Z*;pjJfTJ3KnCJpjP-<+$^Np7tKnq-02e?_Rlxz%U9b5}}Nb z&<-FKoMpoq2%}GsfZm{L!hv*amrRFer2|v=7V}(&{cU;Jic^oqPvXO(8Y2<+t2Ak=Kqd7NR+EU?mRJHFzsbx~1j&*60ulkOQ$hlVT}k%k?b#7b?102kkG9q>Ll z*DY7SSZc@^oCzwf%D}!kMeXiY*Nuh~Uv1KKmIcHSn9Q&4T4a@+(CR#-kwet4>9oU(T0{V95#8YvSkE44Jvr)u`a!UU3PGx~{7 ztvz$#Uzf4L3eRY0hX)aB1fkKdmznCs7~`lY`zwa2%OXN`Rxhw~OZYJtY(H=(cc`fG z@U)S5-U~k2j@jMLPHBQ4E67dO_#jv)RiPfbqY0?)s#IA$?Zi}z%G+^~qSxr}+9bcj z^iLvil>jP1!Np3YZ}QII0J1A`DNZ*Aj}TG<5Z|YV8xBo$)ViUt!)bHV^1U%a*Oj$p z2>$r7B949?sf8d4$u?Ig3I~^n^g14J0bRdhf`;IgltnC60eu5W|0!v>$olb*n(d0kPTrpZ~ckt;Ty4M$M%Y0v8NkxwNw#ba|vm>EAwR_^M z42M^9u0dyMhS71X_Z$2WBiywwW2avTf8K4EZ{C~nd8O%h`OT823?EnWr)VR(k_R^+ z7Zi;;$raVT*YkV0r_#R`%6j8*p3_OpVf*%cJ(ve{>fy+6^4N6FR z$>*p8DfcD%+Q}u4a{aC7-_@EBt2T_a)MFsmD>79c&N4*4Ycc{cxxp*vPvnlhRyH!f z2tKt6xaij?0na(0$+&RzBCs%yn2%EUtH)AXofB|1#(h6-Ft&9Cz-)q)( znI|LecHp#^4U8Ye+y6_9mO+V;4fFNo76cerC^Q%t#h(%*Cx98i31DXeaAvl(G?iYO8iD!e~CXno;6!gYcs~eTKv+A;c@ir4hs}bG|mXcb8YVSFUpgzquyA z4|z(Ms6tMo!;nnb%SvZwH*bP8`KC3(&>O8q@GV%Q#^EaPQ2&)H9}CylYxUP_6ZSD^ zhhne#kY)9;O9-=+Tn8wS3_mPf7kG`Non6&d*yMD}60lUfA`I^1>jxxJz++7m)~guq zTTVTD)sKZhx2fVine$>JHM6Agwgh!`{p^m=hPB-FS7lWNDB7@uTIaJQeQn0$iTKC9<0a`Q0c&1q3oePl+S}?L`kv2EpajpMl^&plNr)K`Q2}gtFRO%r)NY3 z!I*F=ea62fwp53Jk2vn5~|v#%>E*)f9m+;?+kV!?^Ai0CZ?0R>xHVvtx}& zIm=UpiFN+M^dfF3M=%i&@~$K27aPPzD}wV0mI%PolI;-hIoWJ2-B6$p?g-M0L#A_#Bv1rPWv z6opF5n2;}7Wv$o)_k_T?Hn{_xRAH~P8Cczn64(dFMJS{E+ zjPg-7DkjW^_7*~6ksT_G9*{iRsc#7lruI<7>^X9gQZesX36y+$_T9Kzb0?|mcCS8l zQc|#+KA7$A*%U%Ik-Fa4c)c@n#>29aB5;8nRnbpYAGnJwDb*zLCme46IB+A__Ig!= zQjaXz29L=KDS2V59)(1}^UC?qjAl`(olbV&z}$h0m?=!NuYc#euV9G1Lc#Mg@9pai zcak=V)Z4a=Htux^=9_wl`=8*yBr*gISJIS#@RTDMi{NH3Th>N21Go=vanz0yF%=;t`fl#Dy2uwRsQc76a(rSc z+_-l4Q?LQ$+3a<7?0aLx!UmI(dewQckJ@=o4%#C~0;ii`uY+Lxn1RVCm5X2e88ayH z(NJ1R+I*X4ulap%`)KQ{>RnX?5{^gv#)qe4T1OLxIgxXdv5|HGjQNlgx>7-J>d7Ep@F`O}Q4Xl~zi`uy|>T*AHY&IU8g-R|^$i>KX(hdyee z#6hJznlZe9N~dlG2BE6@v@`VSeBHr;x0#65)XyAI0Ck&(Fed2X&gu;UbEuL0erl#} zSo!9+P}^z4N6JUZ&8KL5X$3vFKqW(ev6{Rgw~F}%aZ_U*4T_EFy&)qbf$BrC&zcG{ zNSiK<1gD?FB4yd9&a$u;!)qeeLgSefWSPnXF}S~426-pg4X{BTxI#({xP+^Esk zP}^1cq{~aaDJqp|u|??~E*PwIP%&D=e|{*tC-|WF9r)IH>@xXh`#pI;*U!gt6~Dr^ zvNsK&z})C zU5X7>JRE%O?tUbeamGhzlArtZ9mfU08d2aRNe>1uZS`dEj>V_UB-{FpL0Eq+8Ppte zmj2@F2Nx=YfiaYngDv5l(ok?o2_F5<2Eti5GKK`x_dSzxzLAP<5l+u?M%f3daji_u z52en>c!GW3;WeFiIN~upp4;$3mi%0Ij04h(EO;BdWsfTi@OnLdg8fqIEJ&pWaZ_yP zpch#O=tcJWuT|<~Z)0O@WMchCy{|T|xGId+`9Lcfp++hz9CFhOgiz5@lbr?oSf;}D zW0+63_?M*eqRmka6r^Z#n(Y-=8BW|%rP`1kVlnELMR}b zJ*xjB8~3OBM#`P%2gV8(vcV54ip^4r=OI79phwA0hq-o4;a7N~AX{L#s7&8#E9gz7 zv)PPV6GKMbV%<9v&%n~21(xwLzXqU6Pm~?O@$iQp(oGs5ZEUOO%oZ04NfGxKmMcW@ z!ywk@m**XQY4tXYp-`AS6k5OP|L9QO&M=mU2w@35B_hb`i10=|ro0r)dNf!`_4WE` zt@`HLmdk?7E?wXIY=X&{(U|2b@)>mowce>SUd?B4p)Gv%65M98rW->BC_9X)g@Jf1 z!&tAKQZiLywc#IDbGkOCx%w!w7ROOM9K&*)`-Q|EOE>{=6nRgmkL+bxO+=&b-_o6W zc+kPGP3IXkqtaoe`>h>jZ#S9cD))cJ9^lm&CZ^Vxk(`*uo1yyV!@WsBFp99MMOguKC2WhL4zD*V2MaN?LwLNLu z_Lj~5>9I0u5JwaYQT`xvDr)d-Z_$3i+v}t~_k_hmmg~Hq_E~KPb-Faz$qBqhjK!zO z>w4Y>xbb276|_~K)kNlg#iVg>z$x3U48|petyKl}@iX<4-DmWU!@@La^3)wY^;%K4 zYlHRvVCQ)ou2OG~Pi1c;Pbr6qpc6T|?FXli`MB^lBnM2+yNa~WTkC9+kStm65q}A0 z8zh)eEe!}-C@`>g&})t6uLWZRFgLPcbTIO;1=zX#k^KSGN8Mg?qpja+&f!f`2($S1 zN1BFh#0ansE~QsWxP$YR0EuOy6=HC-cOjiFLM|y=fOcn_@3%rV<}1r|ZzG6WchlK8 zE|{E%8|tngqvy&@3B*#4kLvfbbv05#)pxT;_Vjs|^O>EoEQ@$+rIr#D@&>!fR(8Pbo&}%OCPondD&dlMvT-uX7$d3cy|2Qo3jK)FRYe4bD}+t>oKn%S?UtgyqNc<`V_vUOE4X# z2*@s^g*XZlbyidzduz#?MPPFo!AFL|9bX7Q1>OOV#4c6fh@Lsi6&6glA;t zjvg%7ifO-L_e&Etk4V9on8 zHQvJ&rN|O+vSty%Uz9o{j=K~vyw=&|x7IM7whGb@gV9J=pYvFXe7B3}k^FHj?ukK| z8e@QLswJAQ%B#IY05~zY{j?sm?L&uagOdPX8WO(iVi{2yt$}G9Cq6G3vHZzDhxfsR zqRF&t)D7lQAvU(e*kaczqZ~+MOxeuFt+6^|p5QHkio43L0A8cPth?_|T%N8OGPElK zj4#u8zB)ej!+@Sqfo3CFYerAlv$Q?uvxWGeIf5;zlFu0AY$O0M(2orjbAa-}5)}+Z z2p96RX_aS9H81oE&eN@A2ws<2eha)jlj#XfWbDWpH`$kM3~8gQCi}FuukaCQg)O<= zrpk9T3r|;$$My5u?rC&~n7jHx7~dY9VTkfjvr1>T6z-$zcD-hxDI_s9hD!ioSi6GW zbBZ#iZgKs3<8WbnUCJB$fHBJ6^0C3}*%`WMTj>hZXN^Rdp!zD^M^KY1BLB^sy+}bf zL?>mTveXWtP}Fcfupau-$N+?_4kz$Cm5FJCP!9^r6;93zhZN~h`Vq=AFbv8pN;R<*&CE1J#H_A}{>LRHU+0 zDcmX7X)-FFMu)rB=Z4L-l!~TVg3Bh235ON5Kg_3)T23^ZYXRsYdaA{SrZEf^31MWI zY<+&LrVW^LlQrCpCGu9G&!FAd94Cm6U9S?z6Xknl3J3@QVx5{q9)G-^3Ud^IZ&}2A zj`Mbygtq(qX1JxHw!)AWg<|dbNAEmq;?VT{v5xVf1MlMu7xb&K152J(MVa?1`bMhZ zN}4tnBh>6bg31AtQxF?rfwE|S7%(uBKZP4-6DLat7iVT?6C*P-dmB?GXNzdH z@t9REw9ZRfDn0U{;`lU$)yP%btzyZ|g=O3?`V_#Bz+lFm_ zXSHrCHdo<05%ulxcx2>v`{}1)7P3*L!qM~b$;2PLIKWb6)8zJ194Z&s3A0O_uBhC1 zu1rB+bdl~OdD|Xc*|P1Pg@%$N-NA`I*Z}}E-yhR*FI3};v-G2LCyNWq$CHB?HKdh} z+V@9Cm+OY--@9Bo2@321;1*Cr@@9%;aTJU17k1KM#$+eb=r*w^c-QMv8NwDf`X?-5 zgzaGNmUy>AYfhnR3m7grH)VfpdR*z3f|*ymjdHE=`+0wMtAQ`eY$PWT9o!iG)LdQB z33L)Rj%71R5BVJL*o7PTxfhj4S9LH3^?t&!*&W6EW2H|{Zj@J89^zw_UBSi;S6-^_$|uRz8%$pCrkdy1MDtsRgQYT_2khil84~`>BNLSZxb# zDNyW5XN;7HoolNQOgGFDCCj=_k;qWPTSva_zJ`?M;}w#ZA&yI1za_B0$_G+5*_T}Q z*&AQS^;_bXNk)8 zaj5V{TEFCb@?1AJYa>Cys)M5h1O7a=8FxS?_-5v`Ov$k%LF?9tr(Y)CEXURgv=8IN)GlO`xoo=}(F>a&`uQ1myy-b+9pV0Wh1|n^*&! z7)|VL9qgR}Odhs2mC8Me!7OMUmsn5WDH3`Lo_)gY7Aar@rA$c_A1KVj~Z;7zz`N;0o%3?D;PKt8czk$uxc&I~mn{Yv-oc!)<|m!FowyJakM z2oFVquFZYQKB`R@AoN(xE!((?>!oEs$AHNlGeL=f0L2;oYLIH>9STo14&J(vR1&q| zEOmXCxl^d$czxt4=Fygw|8)coMl)anBP$I>2-h3(mx<3nCO#xglcoommKLH>i z=9Vs&=63c@fYDJUndr%JCE9Tr#)UoEVFsq@y&1W2`t6yS(P5>%8E5zyRHk8O*9~g?1``2X zEZK84?H*Oz^1DpQ2u9{giZy*erw>qAl)2-W0W_x^iv-5!I*DSaDmK+ad@{G>*dHd8 z%IaJl_x8ogJUm~rqFh}m8~4Ow+N)=P4y%x-g%ve>7qX2frGgzLwPlPR*B&k+{ESU> z7F9{icNEROQud*_{;(n%&t{dx?Ft z#@+frp?v)5yRwug^rzz6Fl7?O5sp^(>FgsuLgqSx5Mp)N+@o%yaW+_b%yR@1Wgo{M zF}EQ?zBNdi)~TV2T$S!0VZ#!SJA7D6NMr3~9z4t>s7MQC@Z$}B`R@v3(9Bn4NTeWR zYJeod^*8?elSEAIt^d`}`9mrR9d^AeuR`2&zM^2sKdR}MNh1}1LZFMIfhtqP{$#IS zZm8QR7GKusl?^7N3{)gy$n~0VpETyLhaH}lAo8z_pND>%AivoIpPeJ9}fLJJl*|;yQbAAg_eIu+lyCtKeF+WYe|(0coX%ms+QSNkSsL zwsPadx9!Zy+-*%rpiD|^X3mH<6)V7{1&^RyVz^_jY*;!YXLX{C@%5$0g}h0MY2Dzm zlRDX=Vx*!)&TLe?s~~{>K5qKWRM)thkt}v3ot*j6hfb)w6`laS(_3B2*ADhmdsFnX z&|HMd;>AesnhX#-Fg6G04QA%s4A5bVz-Dzw#ULeZZi?M;$U z6AuHjr{CZXbSb$9v?t^R6ZXq&jamk3A9-M!5&SWEImDK z08A$Kc4n65OfJsqQEeE3ENBuB-u*ff<{VdV$yT&~ln_Z{kKtmhK@A3MZR{GEcullu zE=CuE3D7V!^p7p8XtD^)RKZk9yJz>TtHS26gy!-R&$e@J+1H-n`KO{*T;eu|zZsUs znids5^XB(l4Ap+NGcR_rd#l*)D5w_v48T@44MwoVg(T^_LY+$ZkWWTE?Gw4Kk+eDtLHCnVt$sn};(8|jD6LwnYU>?$tNrSH&ozSAek{W0y z;>}94r`ucfPirKo@%4OquJhQ02P-b37}V$AL|ENdFxWcInIWNVKbl6P$8^!$Cnx1X zw|ZAb2X>|H&WlMT#1vm76xHg3!(>t9D)9Prq@X=nU_g?nuWou&d2rDsGo=uFeN`5> zN~+p>r{on476$*Ae!k8u830Y(zk^S;I_wXpM?rAGszmU`lmt~KU(|As=Nkz62+fR7v^2y&iW?i2iI4Ky}Gb?C9@l@C&aQ7k+#%Ow@ z_fwp4XB1?_{67r(H^y`a7&F_MTiU(<*U5>FW3`81MGA5E4GF8@(EOxUL%yV#-7103 zs!-H+XPW5*cfQdQ_nHhQIE;Gm`Aed)$g^1JB~tN00*%?7l|~aaChbyCP4YgT?X!YM zz4AlVi7E<@##6~`hA4ma2ezf3ODZGOO-e)vX^d3}$b zNgsvE-GMS9vIiEaAUR^6l*u@8F3J)kp4w4MxK&1+zomtlFY6n7HW!SLoDh=L}V=gzVuLc?Sx}Agbzvqj(@=*!=^+ z{c%QK3eGiN$UcY#t>0ZRq(80{ZD!_bGD&)TK^7@p7u~y3FTi%$e3ov+z1o)aJq>|x zm-}r@o3p6&$RP@-8E{g5u>Jmh`McXev4uX`j&n6SGwUlDHJCtC5lgD(YTAp}8%UCm z$@vP@C6-20E`@30$2)aid;x*i#G?)lJl{>Vo~*4Qx74|(SLUhpcT*bS zwc>3ooLgPl)Lj0iA3go3xmVD`%L2(Pyw^|79bx?!QM5Bh@qa?FXjB0e^J}T3_IM7E zyVAyMGl%a4b>*2qUhams$;ucjb4P4F5!n?6mdGZGr|XsU^2o+MPzUW-5nwn+#+Jw+ z?5dFJnnX4^YIRDIB!}+4wmc^oH2fQ0nW&8AD$cE6uX5D!IeE;ZAxq_@y z)3D%+iHp1i8H4YqauUe%m2suYyZgl}6%KUd8?i$d)Iq+W0UdefzX^+fqz!-g!^YB> z*~rz!0^}0gx4PD#K;V0;nbkxym%=)br&6@UHj+h@Xp3qhy+X)YK}9LE{%yC5xm2r*yy@d=zcKPn+6M(N){N?=jmGVy-A-hZRq33bm9i7(p)|ECSSWmnS5k7zp@R_V5 z)>D4hA1I(wDFieoYo>@q)KRmLD#X9i33oiMp*0BKyO$P56Q){NGc<@eSUtE=wBfal z+l^pup32wNFIW(aXdsCl{9&CjcJc9uDN{bIvx?rX^)tD^((z_ALv(2$X^E*!IdG1u zy`{B->Xa$9mJ^FIM<+$rO29Tke1-k9^ayGU&LNdY!o?msL-6Db7~oxw4b_s*(AW$G{>=Y+y!20fpd&H+rOLalSIb$&q9r1zV!~KF473F>-t5cx^cBR1Z366X zfKwI|2bI%hUviG^XH`;)N3iFtErXsiQx@792h?e8nD{i^n>(XvxDxTHqG(* zjza3K4<$|ipgmD&uR5(d3VeK8V5}6G@cIO35m-M!dpzHNto}GJu$)Nmnb#gvKichE(f&Npj{h zrfbi5@zG?(rtJMlP#{!;k|W$b71WGe)2Y2X6I?Y{&B}=mYWB$L-HS2;9CX@Ta-aj^ zL8ojDwEk&40G+mfhiHk|Hk4i#B#B$^kaQh11))4+xWMQSgHf_pq$M6SF=%uLU6}E& z228TOZ0V0HP|rdq%8mBXC7V6V^M~}btF6sx*WRdH=M@;yNfaC1P(LCfTEI(xQEpn^ za_!w`u|$TB>k!2>ytby#Zo>G$lR>=^UZ@q>cjLWsYIYgAb*P(FuueH}kqkJ{9uT_d zWj%QD_CW;guD7E^2@a$sPS5~`;~#3t$==lkbQQ$KBE&JJVcoele?wOOZu$$EXt8$v_% z)9e)0Gl$>=8%TJRcvHdYLP^^%iK07J&N+vK32pLwRV6hoCH~B(7!vo#l>_v0$n;bp zoD+Q-3pz5f*2^5yqZ67n{%RLq(JEx)-5xXF;GadzU!|<%jhNMZYLv7zJcmTV1k)G2 zKX##k6y1mqP+36PV}KiP_mLEYqz01OSZf<*Jc`Rm$H|k))ILiypieSjPbDx;@dlXD zFN{7s%=c|y+^%m|&u{$L84|wZV+qrk@4htTkB#BSUz6yYg~TzAS;V7gv2i6%f+u9R zqkB))(VwIq%uK{51Rdrru^i4($3&r{n#-NO|{G+ z?`}6-I-YvRZSWPAglqv;`&A=5an$wOi`4tISS^L}WxS#(D|}2zuJ`c=kR}O!?D__# zVMA#rADC-{ppC*EkT0K!F9=g)18i357mf4f^o4`-Yg!2^j_oc$Ckq_n735#vbq5`< zfBn=4{PFR-o9{0Pzq=InzyA*gRv&-||DS}vx+3weemCp? zMf-~Sm$Vm?|ChYqjY5C%Ffsp<_x}c?FR8zqc>J6C6cC8>pVZ&XKVHIq*X;c_tUh24 z|36`WZWR8huLbI7y=4Ec%lV7VP59s0zj~f8nZIjg{$dvY9rHzF^Ck6n{k&h)TcZE| zP5)$t7u~&=&-h*c>en-@L368@^7*r+|FfI*lKZ=!(J!tN>ECnzUkB+W{&zi^UwA>X zzsJ8UAYYPy*Rc3Sj(q*!$^Y5Dc**@;)&Gl|L;m;NUp;`A+}{VHOlQ476<|1NR=1s4YmHvY-iewoI< zr2j5a`$ew>jZXd{{dacxU7qv{9tSGg{|WrhS<_oNXwU>23=9kO%LJ-q4*nROf&C9H C-4pu& literal 0 HcmV?d00001 diff --git a/skills/app-builder/SKILL.md b/skills/app-builder/SKILL.md new file mode 100644 index 0000000..908bcd9 --- /dev/null +++ b/skills/app-builder/SKILL.md @@ -0,0 +1,132 @@ +--- +name: app-builder +description: Build and deploy web applications following a standardized process. Use when asked to create a new app, prototype, SaaS tool, or web project. Covers the full lifecycle from ideation through production deployment with a repeatable stack (React + Vite + Tailwind, Elysia + Bun, PostgreSQL + Drizzle, BetterAuth, Dokploy). Use for any request like "build me an app", "create a tool", "make a dashboard", or "prototype this idea". +--- + +# App Builder + +Standardized process for building and deploying web applications. Every app follows the same stack, structure, and lifecycle. + +## Stack + +| Layer | Tech | Why | +|-------|------|-----| +| Frontend | React + Vite + TypeScript + Tailwind | Fast, typed, utility CSS | +| Backend | Elysia + Bun + TypeScript | Fast, type-safe, great DX | +| Database | PostgreSQL + Drizzle ORM | Industry standard + lightweight ORM | +| Auth | BetterAuth (invite-only signup) | TypeScript-native, modern | +| AI (if needed) | LangChain.js | Model-agnostic, swap providers | +| Email (if needed) | Resend | Simple API, 3k free/month | +| Jobs (if needed) | pg-boss | Postgres-backed job queue | +| Deploy | Dokploy (self-hosted) | Full control, predictable costs | +| Git | Gitea (git.infra.nkode.tech) | Self-hosted, private | + +## Lifecycle Phases + +### Phase 1: Ideation +1. Gather requirements from the user (what, who, why) +2. Write a requirements doc at `notes/projects//requirements.md` +3. Research competitors if relevant +4. Get user approval on scope before proceeding + +### Phase 2: Scaffold +1. Create project directory at `/home/clawdbot/clawd/` +2. Use the template structure from `assets/template/` as a starting point +3. Create Gitea repos (one for monorepo, or separate frontend/backend) +4. Customize schema for the specific app +5. Ensure Hammer service account routes are included + +### Phase 3: Development +1. Build features incrementally +2. Write tests from day one +3. Use local docker-compose for development +4. Commit and push frequently +5. Update the task queue dashboard with progress + +### Phase 4: Test Deployment +1. Create Dokploy compose deployment +2. Deploy to test subdomain: `test-.donovankelly.xyz` +3. Set environment variables in Dokploy +4. Store all credentials in Bitwarden shared vault +5. Verify basic functionality + +### Phase 5: User Review +1. Notify user that test deployment is ready +2. User tests and provides feedback +3. Iterate on feedback (return to Phase 3 as needed) +4. Get explicit approval before going to production + +### Phase 6: Production Deployment +1. Create production Dokploy compose +2. Deploy to production subdomain: `.donovankelly.xyz` +3. Configure production environment variables +4. Verify all features work +5. Set up monitoring/health checks + +### Phase 7: Maintenance +- Feature requests go through test env first +- DB migrations via Drizzle (`bun run db:push` for dev, `bun run db:migrate` for prod) +- Keep staging synced with prod schema +- Rollback: Dokploy supports redeploying previous builds + +## Every App Must Have + +- [ ] **BetterAuth with `disableSignUp: true` (invite-only)** — NO EXCEPTIONS. Every app requires authentication. No public-facing pages without login. +- [ ] Hammer service account + `/api/hammer/*` routes (bearer token auth) +- [ ] All API routes behind auth (session or bearer token) +- [ ] Structured logging (console.log with JSON in prod) +- [ ] Unit tests (Vitest for frontend, bun:test for backend) +- [ ] Health check endpoint (`GET /api/health`) — only unauthenticated route allowed +- [ ] docker-compose.yml (local dev) +- [ ] docker-compose.dokploy.yml (production) +- [ ] `.env.example` with all required vars documented +- [ ] All secrets in Bitwarden shared vault + +**Auth is not optional.** Even internal tools, dashboards, and single-user apps must have login. If Donovan is the only user, he still logs in. No public read access to any data. + +## Environment Variable Naming + +Standard env vars across all apps: +``` +DATABASE_URL=postgresql://... +PORT=3001 +NODE_ENV=production +APP_URL=https://app. +ALLOWED_ORIGINS=https://app. +BETTER_AUTH_SECRET= +HAMMER_API_KEY= +RESEND_API_KEY= +FROM_EMAIL= +``` + +## Credential Management + +- All credentials go in Bitwarden shared vault (org: Hammer's Credentials) +- Create a Bitwarden entry per app: ` ()` +- Store HAMMER_API_KEY as a field on the app's BW entry +- Add HAMMER_API_KEY to `~/.clawdbot/.env` as `HAMMER__API_KEY` +- Never hardcode secrets, never echo them in chat + +## Dokploy Deployment + +See `references/deploy.md` for detailed Dokploy deployment steps. + +## Rollback Protocol + +See `references/rollback.md` for rollback and recovery procedures. + +## DB Migration Strategy + +See `references/migrations.md` for database migration best practices. + +## Legal & Payments + +See `references/legal-payments.md` for legal protection and payment processing options. + +## Project Documentation + +For every app, create in `notes/projects//`: +- `README.md` — overview, stack, status +- `requirements.md` — feature spec +- `architecture.md` — technical design (if complex) +- `feasibility.md` — assessment (if needed) diff --git a/skills/app-builder/assets/template/.env.example b/skills/app-builder/assets/template/.env.example new file mode 100644 index 0000000..1b41e46 --- /dev/null +++ b/skills/app-builder/assets/template/.env.example @@ -0,0 +1,20 @@ +# Database +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/APP_TEMPLATE + +# Server +PORT=3001 +NODE_ENV=development + +# URLs +APP_URL=http://localhost:5173 +ALLOWED_ORIGINS=http://localhost:5173 + +# Auth +BETTER_AUTH_SECRET=dev-secret-change-in-production + +# Hammer Service Account +HAMMER_API_KEY=hammer-dev-key-12345 + +# Email (optional) +# RESEND_API_KEY= +# FROM_EMAIL= diff --git a/skills/app-builder/assets/template/.gitignore b/skills/app-builder/assets/template/.gitignore new file mode 100644 index 0000000..4274b51 --- /dev/null +++ b/skills/app-builder/assets/template/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.env +*.log +.DS_Store diff --git a/skills/app-builder/assets/template/api/Dockerfile b/skills/app-builder/assets/template/api/Dockerfile new file mode 100644 index 0000000..e72c455 --- /dev/null +++ b/skills/app-builder/assets/template/api/Dockerfile @@ -0,0 +1,7 @@ +FROM oven/bun:1-alpine +WORKDIR /app +COPY package.json bun.lock* ./ +RUN bun install --frozen-lockfile 2>/dev/null || bun install +COPY . . +EXPOSE 3001 +CMD ["bun", "run", "src/index.ts"] diff --git a/skills/app-builder/assets/template/api/drizzle.config.ts b/skills/app-builder/assets/template/api/drizzle.config.ts new file mode 100644 index 0000000..9233f37 --- /dev/null +++ b/skills/app-builder/assets/template/api/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + schema: './src/db/schema.ts', + out: './drizzle', + dialect: 'postgresql', + dbCredentials: { + url: process.env.DATABASE_URL!, + }, +}); diff --git a/skills/app-builder/assets/template/api/package.json b/skills/app-builder/assets/template/api/package.json new file mode 100644 index 0000000..43ec53d --- /dev/null +++ b/skills/app-builder/assets/template/api/package.json @@ -0,0 +1,23 @@ +{ + "name": "APP_TEMPLATE-api", + "version": "0.0.1", + "scripts": { + "dev": "bun run --watch src/index.ts", + "start": "bun run src/index.ts", + "db:generate": "drizzle-kit generate", + "db:push": "drizzle-kit push", + "db:migrate": "drizzle-kit migrate", + "db:studio": "drizzle-kit studio", + "test": "bun test" + }, + "dependencies": { + "better-auth": "^1.0.0", + "drizzle-orm": "^0.38.0", + "elysia": "^1.2.0", + "postgres": "^3.4.0" + }, + "devDependencies": { + "@types/bun": "latest", + "drizzle-kit": "^0.30.0" + } +} diff --git a/skills/app-builder/assets/template/api/src/db/index.ts b/skills/app-builder/assets/template/api/src/db/index.ts new file mode 100644 index 0000000..9b15e49 --- /dev/null +++ b/skills/app-builder/assets/template/api/src/db/index.ts @@ -0,0 +1,6 @@ +import { drizzle } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; +import * as schema from './schema'; + +const client = postgres(process.env.DATABASE_URL!); +export const db = drizzle(client, { schema }); diff --git a/skills/app-builder/assets/template/api/src/db/schema.ts b/skills/app-builder/assets/template/api/src/db/schema.ts new file mode 100644 index 0000000..59949ec --- /dev/null +++ b/skills/app-builder/assets/template/api/src/db/schema.ts @@ -0,0 +1,54 @@ +import { pgTable, text, timestamp, boolean, uuid } from 'drizzle-orm/pg-core'; + +// BetterAuth tables (managed by BetterAuth, do not modify) +export const users = pgTable('users', { + id: text('id').primaryKey(), + name: text('name').notNull(), + email: text('email').notNull().unique(), + emailVerified: boolean('email_verified').default(false), + image: text('image'), + role: text('role').default('user'), + createdAt: timestamp('created_at').defaultNow(), + updatedAt: timestamp('updated_at').defaultNow(), +}); + +export const sessions = pgTable('sessions', { + id: text('id').primaryKey(), + userId: text('user_id').notNull().references(() => users.id), + token: text('token').notNull().unique(), + expiresAt: timestamp('expires_at').notNull(), + createdAt: timestamp('created_at').defaultNow(), + updatedAt: timestamp('updated_at').defaultNow(), +}); + +export const accounts = pgTable('accounts', { + id: text('id').primaryKey(), + userId: text('user_id').notNull().references(() => users.id), + accountId: text('account_id').notNull(), + providerId: text('provider_id').notNull(), + password: text('password'), + createdAt: timestamp('created_at').defaultNow(), + updatedAt: timestamp('updated_at').defaultNow(), +}); + +export const verifications = pgTable('verifications', { + id: text('id').primaryKey(), + identifier: text('identifier').notNull(), + value: text('value').notNull(), + expiresAt: timestamp('expires_at').notNull(), + createdAt: timestamp('created_at').defaultNow(), + updatedAt: timestamp('updated_at').defaultNow(), +}); + +// ============================================ +// APP-SPECIFIC TABLES — Customize below +// ============================================ + +// Example: +// export const items = pgTable('items', { +// id: uuid('id').primaryKey().defaultRandom(), +// title: text('title').notNull(), +// userId: text('user_id').references(() => users.id), +// createdAt: timestamp('created_at').defaultNow(), +// updatedAt: timestamp('updated_at').defaultNow(), +// }); diff --git a/skills/app-builder/assets/template/api/src/index.ts b/skills/app-builder/assets/template/api/src/index.ts new file mode 100644 index 0000000..464a667 --- /dev/null +++ b/skills/app-builder/assets/template/api/src/index.ts @@ -0,0 +1,14 @@ +import { Elysia } from 'elysia'; +import { cors } from '@elysiajs/cors'; +import { hammerRoutes } from './routes/hammer'; + +const app = new Elysia() + .use(cors({ + origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:5173'], + credentials: true, + })) + .get('/api/health', () => ({ status: 'ok', timestamp: new Date().toISOString() })) + .use(hammerRoutes) + .listen(Number(process.env.PORT) || 3001); + +console.log(`🚀 Server running on port ${app.server?.port}`); diff --git a/skills/app-builder/assets/template/api/src/lib/auth.ts b/skills/app-builder/assets/template/api/src/lib/auth.ts new file mode 100644 index 0000000..5e9481c --- /dev/null +++ b/skills/app-builder/assets/template/api/src/lib/auth.ts @@ -0,0 +1,17 @@ +import { betterAuth } from 'better-auth'; +import { drizzleAdapter } from 'better-auth/adapters/drizzle'; +import { db } from '../db'; + +export const auth = betterAuth({ + database: drizzleAdapter(db, { provider: 'pg' }), + emailAndPassword: { + enabled: true, + disableSignUp: true, // Invite-only + }, + trustedOrigins: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:5173'], + advanced: { + crossSubDomainCookies: process.env.NODE_ENV === 'production' + ? { domain: process.env.COOKIE_DOMAIN || '' } + : undefined, + }, +}); diff --git a/skills/app-builder/assets/template/api/src/routes/hammer.ts b/skills/app-builder/assets/template/api/src/routes/hammer.ts new file mode 100644 index 0000000..7815566 --- /dev/null +++ b/skills/app-builder/assets/template/api/src/routes/hammer.ts @@ -0,0 +1,38 @@ +import { Elysia, t } from 'elysia'; +import { db } from '../db'; +import { users } from '../db/schema'; +import { eq } from 'drizzle-orm'; + +const validateHammerAuth = (authHeader: string | undefined): boolean => { + if (!authHeader) return false; + const token = authHeader.replace('Bearer ', ''); + return token === process.env.HAMMER_API_KEY; +}; + +export const hammerRoutes = new Elysia({ prefix: '/api/hammer' }) + .derive(({ request, set }) => { + const authHeader = request.headers.get('authorization'); + if (!validateHammerAuth(authHeader)) { + set.status = 401; + throw new Error('Invalid API key'); + } + return {}; + }) + + .get('/me', async ({ set }) => { + const hammerUser = await db.query.users.findFirst({ + where: eq(users.role, 'service'), + }); + if (!hammerUser) { + set.status = 404; + throw new Error('Hammer service account not found'); + } + return { + id: hammerUser.id, + name: hammerUser.name, + email: hammerUser.email, + role: hammerUser.role, + }; + }); + + // Add app-specific Hammer routes below diff --git a/skills/app-builder/assets/template/docker-compose.dokploy.yml b/skills/app-builder/assets/template/docker-compose.dokploy.yml new file mode 100644 index 0000000..6c5a7c5 --- /dev/null +++ b/skills/app-builder/assets/template/docker-compose.dokploy.yml @@ -0,0 +1,27 @@ +services: + api: + build: + context: ./api + dockerfile: Dockerfile + restart: unless-stopped + ports: + - 3001 + environment: + - DATABASE_URL=${DATABASE_URL} + - PORT=3001 + - NODE_ENV=production + - APP_URL=${APP_URL} + - ALLOWED_ORIGINS=${ALLOWED_ORIGINS} + - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} + - HAMMER_API_KEY=${HAMMER_API_KEY} + command: sh -c 'bun run db:push && bun run src/index.ts' + + web: + build: + context: ./web + dockerfile: Dockerfile + restart: unless-stopped + ports: + - 80 + depends_on: + - api diff --git a/skills/app-builder/assets/template/docker-compose.yml b/skills/app-builder/assets/template/docker-compose.yml new file mode 100644 index 0000000..738f217 --- /dev/null +++ b/skills/app-builder/assets/template/docker-compose.yml @@ -0,0 +1,40 @@ +services: + db: + image: postgres:16-alpine + restart: unless-stopped + ports: + - 5432:5432 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: APP_TEMPLATE + volumes: + - pgdata:/var/lib/postgresql/data + + api: + build: + context: ./api + dockerfile: Dockerfile + restart: unless-stopped + ports: + - 3001:3001 + env_file: .env + depends_on: + - db + volumes: + - ./api/src:/app/src + + web: + build: + context: ./web + dockerfile: Dockerfile + restart: unless-stopped + ports: + - 5173:5173 + depends_on: + - api + volumes: + - ./web/src:/app/src + +volumes: + pgdata: diff --git a/skills/app-builder/assets/template/web/Dockerfile b/skills/app-builder/assets/template/web/Dockerfile new file mode 100644 index 0000000..bd574ee --- /dev/null +++ b/skills/app-builder/assets/template/web/Dockerfile @@ -0,0 +1,12 @@ +FROM node:22-alpine AS build +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm install +COPY . . +RUN npm run build + +FROM nginx:alpine +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/skills/app-builder/assets/template/web/nginx.conf b/skills/app-builder/assets/template/web/nginx.conf new file mode 100644 index 0000000..6850d82 --- /dev/null +++ b/skills/app-builder/assets/template/web/nginx.conf @@ -0,0 +1,17 @@ +server { + listen 80; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /api/ { + proxy_pass http://api:3001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/skills/app-builder/assets/template/web/package.json b/skills/app-builder/assets/template/web/package.json new file mode 100644 index 0000000..1315ffb --- /dev/null +++ b/skills/app-builder/assets/template/web/package.json @@ -0,0 +1,28 @@ +{ + "name": "APP_TEMPLATE-web", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "test": "vitest" + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.0.0" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.0", + "autoprefixer": "^10.4.0", + "postcss": "^8.4.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.6.0", + "vite": "^6.0.0", + "vitest": "^2.0.0" + } +} diff --git a/skills/app-builder/references/deploy.md b/skills/app-builder/references/deploy.md new file mode 100644 index 0000000..de05394 --- /dev/null +++ b/skills/app-builder/references/deploy.md @@ -0,0 +1,66 @@ +# Dokploy Deployment Guide + +## Prerequisites +- Dokploy server at 191.101.0.153 (Hostinger KVM2) +- Dokploy API key in Bitwarden ("dokploy api key") +- Gitea repos created at git.infra.nkode.tech + +## Compose File Structure + +Every app uses `docker-compose.dokploy.yml`: + +```yaml +services: + api: + build: + context: ./apps/api # or ./api, ./backend + dockerfile: Dockerfile + restart: unless-stopped + ports: + - 3001 + environment: + - DATABASE_URL=${DATABASE_URL} + - PORT=3001 + - NODE_ENV=production + - APP_URL=${APP_URL} + - ALLOWED_ORIGINS=${ALLOWED_ORIGINS} + - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} + - HAMMER_API_KEY=${HAMMER_API_KEY} + command: sh -c 'bun run db:push && bun run src/index.ts' + + web: + build: + context: ./apps/web # or ./web, ./frontend + dockerfile: Dockerfile + restart: unless-stopped + ports: + - 80 + depends_on: + - api +``` + +## Deployment Steps + +1. Push code to Gitea +2. Create compose in Dokploy: + - Source type: raw compose with git URL + - Set env vars in Dokploy UI +3. Configure domains in Dokploy: + - API: `api..donovankelly.xyz` → api service port + - Web: `app..donovankelly.xyz` → web service port +4. Enable HTTPS (Dokploy handles Let's Encrypt) +5. Deploy and verify health check + +## Domain Pattern + +- Test: `test-.donovankelly.xyz` +- Production: `.donovankelly.xyz` +- API: `api..donovankelly.xyz` (or `api.todo.donovankelly.xyz`) +- Frontend: `app..donovankelly.xyz` (or `app.todo.donovankelly.xyz`) + +## Environment Variables + +Set in Dokploy compose env (not in docker-compose file): +- All `${VAR}` references resolve from Dokploy env settings +- Generate secrets with `openssl rand -hex 32` +- Store everything in Bitwarden immediately after creating diff --git a/skills/app-builder/references/legal-payments.md b/skills/app-builder/references/legal-payments.md new file mode 100644 index 0000000..9a96939 --- /dev/null +++ b/skills/app-builder/references/legal-payments.md @@ -0,0 +1,47 @@ +# Legal Protection & Payments + +## Legal (Lean Approach) + +### What Pieter Levels Uses +Pieter Levels (maker of Nomad List, Remote OK, Photo AI) keeps it minimal: +- Simple Terms of Service page +- Simple Privacy Policy page +- Generated with free/cheap tools, not expensive services like Termly + +### Recommended Approach +1. **Terms of Service** — Use a free generator (TermsFeed free tier, GetTerms.io) or write a simple one +2. **Privacy Policy** — Required if collecting any user data. Free generators available +3. **Cookie Banner** — Only needed if using analytics/tracking cookies +4. **Business Entity** — LLC ($50-150 depending on state) for liability protection +5. **Don't over-engineer** — Until you have paying users, simple legal pages are fine + +### When to Upgrade +- Taking payments → need proper ToS with refund policy +- Handling health data → HIPAA considerations +- EU users → GDPR compliance (data export, deletion rights) +- Enterprise clients → may need SOC 2, BAA agreements + +## Payments + +### Options (Easiest to Hardest) + +| Service | Fees | Best For | Setup Time | +|---------|------|----------|------------| +| Lemon Squeezy | 5% + $0.50 | Merchant of record, handles tax/VAT | 1 day | +| Paddle | 5% + $0.50 | Same as Lemon Squeezy, more established | 1 day | +| Stripe | 2.9% + $0.30 | Full control, most flexible | 2-3 days | +| Gumroad | 10% | Digital products, simplest | Hours | + +### Recommendation +- **Start with Lemon Squeezy or Paddle** — they handle sales tax, VAT, and act as merchant of record (you don't need a business entity) +- **Move to Stripe** when you need more control or lower fees at scale +- Both have simple JS SDKs and webhook integrations + +### Integration Pattern +``` +User clicks "Subscribe" → Redirect to payment provider checkout +→ Provider handles payment → Webhook to your API +→ API updates user subscription status in DB +``` + +Keep payment logic out of your app. Let the provider handle checkout, invoicing, and tax. diff --git a/skills/app-builder/references/migrations.md b/skills/app-builder/references/migrations.md new file mode 100644 index 0000000..461b396 --- /dev/null +++ b/skills/app-builder/references/migrations.md @@ -0,0 +1,49 @@ +# Database Migration Strategy + +## Drizzle ORM Migrations + +### Development (Local / Test) +Use `db:push` for rapid iteration: +```bash +bun run db:push +``` +This syncs schema directly — fast but destructive. Fine for dev/test. + +### Production +Use `db:migrate` with generated migration files: +```bash +bun run db:generate # creates SQL migration file +bun run db:migrate # applies migration +``` + +### Workflow +1. Change schema in `src/db/schema.ts` +2. Run `bun run db:generate` — creates migration in `drizzle/` +3. Review the generated SQL +4. Write rollback SQL in `drizzle/rollback/` (same filename) +5. Test migration on staging +6. Apply to production + +## Safe Migration Practices + +### Adding columns +- Always add as nullable or with a default value +- Never add non-nullable columns without defaults to tables with existing data + +### Removing columns +- First deploy: stop reading/writing the column in code +- Second deploy: remove the column from schema +- Two-phase approach prevents errors during rolling deploys + +### Renaming columns +- Don't rename directly — add new column, migrate data, remove old column + +### Adding indexes +- Use `CREATE INDEX CONCURRENTLY` for large tables (avoids locks) +- Drizzle may not generate concurrent indexes — check generated SQL + +## Backup Before Migration +Always backup before production migrations: +```bash +pg_dump -Fc $DATABASE_URL > backup-$(date +%Y%m%d-%H%M%S).dump +``` diff --git a/skills/app-builder/references/rollback.md b/skills/app-builder/references/rollback.md new file mode 100644 index 0000000..ffbb10b --- /dev/null +++ b/skills/app-builder/references/rollback.md @@ -0,0 +1,43 @@ +# Rollback & Recovery + +## Levels of Rollback + +### 1. Code Rollback (Most Common) +- Dokploy keeps previous builds +- Redeploy previous compose version from Dokploy UI +- Or: `git revert` the breaking commit, push, redeploy + +### 2. Database Rollback +- Drizzle doesn't auto-generate down migrations +- For schema changes, write explicit rollback SQL before deploying +- Keep a `migrations/rollback/` directory with undo scripts +- For data issues: restore from Dokploy Postgres backup + +### 3. Full Rollback +- Dokploy allows complete service redeployment from any previous state +- Database: restore from backup +- Last resort: rebuild from Gitea source at known-good commit + +## Pre-Deploy Checklist + +Before any production deployment: +- [ ] Feature works in test environment +- [ ] User has approved in test +- [ ] DB migration tested (if schema changed) +- [ ] Rollback SQL written (if schema changed) +- [ ] Health check passes after deploy + +## Staging Environment + +- Staging should mirror production schema +- Periodically sync staging DB schema with prod +- Never sync prod DATA to staging (privacy) +- Test migrations on staging before prod + +## If Something Breaks in Prod + +1. **Assess severity** — is the app down or is it a bug? +2. **If app is down** — redeploy previous Dokploy build immediately +3. **If it's a bug** — fix in dev, test, deploy fix +4. **If DB is corrupted** — restore from backup, investigate cause +5. **Notify user** with what happened and what was done diff --git a/skills/app-builder/scripts/scaffold.sh b/skills/app-builder/scripts/scaffold.sh new file mode 100755 index 0000000..1c0c758 --- /dev/null +++ b/skills/app-builder/scripts/scaffold.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Scaffold a new app from the standard template +# Usage: scaffold.sh [--api-only] +# +# Creates project structure, initializes git, and pushes to Gitea. + +set -euo pipefail + +APP_NAME="${1:?Usage: scaffold.sh }" +API_ONLY="${2:-}" +BASE_DIR="/home/clawdbot/clawd" +PROJECT_DIR="$BASE_DIR/$APP_NAME" +SKILL_DIR="$(dirname "$(realpath "$0")")/.." +TEMPLATE_DIR="$SKILL_DIR/assets/template" +GITEA_URL="https://git.infra.nkode.tech" + +if [ -d "$PROJECT_DIR" ]; then + echo "Error: $PROJECT_DIR already exists" + exit 1 +fi + +echo "🔨 Scaffolding $APP_NAME..." + +# Create project structure +mkdir -p "$PROJECT_DIR" +cp -r "$TEMPLATE_DIR/api" "$PROJECT_DIR/api" +if [ "$API_ONLY" != "--api-only" ]; then + cp -r "$TEMPLATE_DIR/web" "$PROJECT_DIR/web" +fi + +# Copy root files +cp "$TEMPLATE_DIR/docker-compose.yml" "$PROJECT_DIR/" +cp "$TEMPLATE_DIR/docker-compose.dokploy.yml" "$PROJECT_DIR/" +cp "$TEMPLATE_DIR/.env.example" "$PROJECT_DIR/" +cp "$TEMPLATE_DIR/.gitignore" "$PROJECT_DIR/" + +# Replace placeholder app name +find "$PROJECT_DIR" -type f \( -name "*.ts" -o -name "*.json" -o -name "*.yml" -o -name "*.md" -o -name "*.env*" \) \ + -exec sed -i "s/APP_TEMPLATE/$APP_NAME/g" {} + + +# Initialize git +cd "$PROJECT_DIR" +git init +git add -A +git commit -m "Initial scaffold from app-builder template" + +echo "" +echo "✅ Project scaffolded at $PROJECT_DIR" +echo "" +echo "Next steps:" +echo " 1. Create Gitea repo: $GITEA_URL/hammer/$APP_NAME" +echo " 2. git remote add origin $GITEA_URL/hammer/$APP_NAME.git" +echo " 3. git push -u origin main" +echo " 4. Customize the schema in api/src/db/schema.ts" +echo " 5. Run: cd $PROJECT_DIR && docker compose up" diff --git a/skills/debugging/SKILL.md b/skills/debugging/SKILL.md new file mode 100644 index 0000000..bb4011f --- /dev/null +++ b/skills/debugging/SKILL.md @@ -0,0 +1,68 @@ +# Debugging Skill + +Hard-won lessons from real failures. Follow this before spending time on dead ends. + +## Rule 1: Build Locally First +Before deploying to any remote environment (Dokploy, CI, etc.), **always verify the build locally**: +- `bun run build` / `npm run build` — catches TypeScript errors, unused imports, missing modules +- `docker build .` — catches Dockerfile issues, dependency problems +- Run the test suite if one exists + +**Why:** A TypeScript strict mode error (`TS6133: unused import`) caused 7 consecutive Dokploy deploy failures. A 2-second local build would have caught it instantly. Instead, 30 minutes were wasted trying to read remote logs that weren't accessible. + +*Learned: 2026-01-29 — hammer-queue deploy failures* + +## Rule 2: Reproduce Before Escalating +When a remote deploy/service fails: +1. **Build locally first** (Rule 1) +2. **Run locally** if possible (`bun run dev`, `docker compose up`) +3. **Check the runtime** — does the app start? Does `db:push`/migration work? +4. Only after local reproduction fails should you investigate server-side issues + +**Why:** The `serial` column type worked in schema definition but broke `drizzle-kit push` on an existing table with data. Building locally passed, but running locally would have caught the db migration failure. + +*Learned: 2026-01-29 — serial column broke db:push on existing table* + +## Rule 3: Check the Obvious First +Before diving into API spelunking or log hunting: +- Compiler errors? (`tsc`, build output) +- Missing imports or unused imports? (strict mode) +- Schema changes compatible with existing data? +- Environment variables set correctly? +- Ports/URLs correct? + +## Rule 4: When Blind to Logs, Create Your Own +If you can't access remote logs (no API endpoint, no SSH, no UI access): +- **Don't** spend more than 5 minutes hunting for log endpoints +- **Do** reproduce the issue locally where you CAN see logs +- **Do** add health check endpoints that report startup errors +- **Do** add structured error logging that surfaces in API responses + +## Rule 5: Time-Box Dead Ends +If an approach isn't working after 5-10 minutes, switch strategies: +- Can't read Dokploy logs via API? → Build locally instead +- Can't SSH to server? → Use available APIs differently +- API returning opaque errors? → Test the component in isolation + +## Rule 6: Schema Migration Safety +When modifying database schemas on existing tables: +- `serial` columns can't be safely added to existing tables via `drizzle-kit push` — use `integer` with app-level sequencing instead +- Always consider: "What happens to existing rows?" +- Test migrations against a database with real data, not just empty tables +- Prefer nullable columns with backfill logic over NOT NULL additions + +## Deployment Checklist +Before every deploy: +``` +[ ] Local build passes (frontend + backend) +[ ] TypeScript strict mode clean (no unused imports/vars) +[ ] Schema changes tested against existing data +[ ] Environment variables verified +[ ] Push to git +[ ] Deploy +[ ] Verify health endpoint after deploy +[ ] Verify API functionality +``` + +--- +*This skill is a living document. Update it every time a new debugging lesson is learned.* diff --git a/todo-app b/todo-app new file mode 160000 index 0000000..617eaac --- /dev/null +++ b/todo-app @@ -0,0 +1 @@ +Subproject commit 617eaacc5fffa83c1fb25bf06b8e825bc5315b3d diff --git a/todo-app-web b/todo-app-web new file mode 160000 index 0000000..6f26e11 --- /dev/null +++ b/todo-app-web @@ -0,0 +1 @@ +Subproject commit 6f26e1117c85a51a53f983fcdce839062bc7f7b5