memory: log task worker session - due dates, subtasks, task page
This commit is contained in:
@@ -13,6 +13,7 @@ Before doing anything else:
|
|||||||
2. Read `USER.md` — this is who you're helping
|
2. Read `USER.md` — this is who you're helping
|
||||||
3. Read `memory/YYYY-MM-DD.md` (today + yesterday) for recent context
|
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`
|
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.
|
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)
|
- `trash` > `rm` (recoverable beats gone forever)
|
||||||
- When in doubt, ask.
|
- 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
|
## External vs Internal
|
||||||
|
|
||||||
**Safe to do freely:**
|
**Safe to do freely:**
|
||||||
|
|||||||
18
HEARTBEAT.md
18
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.
|
If new mail from Donovan (forwarded), read and summarize or take action.
|
||||||
Track last check in `memory/heartbeat-state.json`.
|
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)
|
### Skills (weekly)
|
||||||
Check clawdhub for new useful skills.
|
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
|
||||||
|
|||||||
13
TOOLS.md
13
TOOLS.md
@@ -4,6 +4,12 @@ Skills define *how* tools work. This file is for *your* specifics — the stuff
|
|||||||
|
|
||||||
## Rules
|
## 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
|
### 🔐 Never Ask for Secrets in Chat
|
||||||
- Don't ask Donovan to paste API keys, passwords, or credentials in messages
|
- 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
|
- 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`
|
- Bitwarden CLI installed at `/home/clawdbot/.npm-global/bin/bw`
|
||||||
- API credentials go in `~/.clawdbot/.env` (BW_CLIENTID, BW_CLIENTSECRET, BW_PASSWORD)
|
- API credentials go in `~/.clawdbot/.env` (BW_CLIENTID, BW_CLIENTSECRET, BW_PASSWORD)
|
||||||
- Bitwarden data: `~/.config/Bitwarden CLI/`
|
- 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
|
### Infrastructure
|
||||||
- Host: Hostinger VPS, Ubuntu
|
- 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)
|
- User: clawdbot (sudo, needs password)
|
||||||
|
|
||||||
### Email
|
### Email
|
||||||
|
|||||||
1
donovan-portfolio
Submodule
1
donovan-portfolio
Submodule
Submodule donovan-portfolio added at e49a4a4512
1
hammer-queue
Submodule
1
hammer-queue
Submodule
Submodule hammer-queue added at e874cafbec
1
hook-proxy.log
Normal file
1
hook-proxy.log
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Hook proxy listening on 0.0.0.0:18790
|
||||||
21
hook-proxy.ts
Normal file
21
hook-proxy.ts
Normal file
@@ -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}`);
|
||||||
137
memory/2026-01-28.md
Normal file
137
memory/2026-01-28.md
Normal file
@@ -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)
|
||||||
51
memory/2026-01-29.md
Normal file
51
memory/2026-01-29.md
Normal file
@@ -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
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
{
|
{
|
||||||
"lastChecks": {
|
"lastChecks": {
|
||||||
"email": null,
|
"email": 1769660400,
|
||||||
"skills": null
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
network-app-api
Submodule
1
network-app-api
Submodule
Submodule network-app-api added at 11ee9b946f
1
network-app-mobile
Submodule
1
network-app-mobile
Submodule
Submodule network-app-mobile added at b191cfe083
1
network-app-web
Submodule
1
network-app-web
Submodule
Submodule network-app-web added at b6de50ba5e
2
notes
2
notes
Submodule notes updated: 4f14da9136...a05cead8ed
BIN
skills/app-builder.skill
Normal file
BIN
skills/app-builder.skill
Normal file
Binary file not shown.
132
skills/app-builder/SKILL.md
Normal file
132
skills/app-builder/SKILL.md
Normal file
@@ -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/<app-name>/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/<app-name>`
|
||||||
|
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-<app>.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: `<app>.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.<domain>
|
||||||
|
ALLOWED_ORIGINS=https://app.<domain>
|
||||||
|
BETTER_AUTH_SECRET=<random>
|
||||||
|
HAMMER_API_KEY=<random>
|
||||||
|
RESEND_API_KEY=<if needed>
|
||||||
|
FROM_EMAIL=<if needed>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Credential Management
|
||||||
|
|
||||||
|
- All credentials go in Bitwarden shared vault (org: Hammer's Credentials)
|
||||||
|
- Create a Bitwarden entry per app: `<App Name> (<domain>)`
|
||||||
|
- Store HAMMER_API_KEY as a field on the app's BW entry
|
||||||
|
- Add HAMMER_API_KEY to `~/.clawdbot/.env` as `HAMMER_<APP>_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/<app-name>/`:
|
||||||
|
- `README.md` — overview, stack, status
|
||||||
|
- `requirements.md` — feature spec
|
||||||
|
- `architecture.md` — technical design (if complex)
|
||||||
|
- `feasibility.md` — assessment (if needed)
|
||||||
20
skills/app-builder/assets/template/.env.example
Normal file
20
skills/app-builder/assets/template/.env.example
Normal file
@@ -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=
|
||||||
5
skills/app-builder/assets/template/.gitignore
vendored
Normal file
5
skills/app-builder/assets/template/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
7
skills/app-builder/assets/template/api/Dockerfile
Normal file
7
skills/app-builder/assets/template/api/Dockerfile
Normal file
@@ -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"]
|
||||||
10
skills/app-builder/assets/template/api/drizzle.config.ts
Normal file
10
skills/app-builder/assets/template/api/drizzle.config.ts
Normal file
@@ -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!,
|
||||||
|
},
|
||||||
|
});
|
||||||
23
skills/app-builder/assets/template/api/package.json
Normal file
23
skills/app-builder/assets/template/api/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
skills/app-builder/assets/template/api/src/db/index.ts
Normal file
6
skills/app-builder/assets/template/api/src/db/index.ts
Normal file
@@ -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 });
|
||||||
54
skills/app-builder/assets/template/api/src/db/schema.ts
Normal file
54
skills/app-builder/assets/template/api/src/db/schema.ts
Normal file
@@ -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(),
|
||||||
|
// });
|
||||||
14
skills/app-builder/assets/template/api/src/index.ts
Normal file
14
skills/app-builder/assets/template/api/src/index.ts
Normal file
@@ -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}`);
|
||||||
17
skills/app-builder/assets/template/api/src/lib/auth.ts
Normal file
17
skills/app-builder/assets/template/api/src/lib/auth.ts
Normal file
@@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
38
skills/app-builder/assets/template/api/src/routes/hammer.ts
Normal file
38
skills/app-builder/assets/template/api/src/routes/hammer.ts
Normal file
@@ -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
|
||||||
@@ -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
|
||||||
40
skills/app-builder/assets/template/docker-compose.yml
Normal file
40
skills/app-builder/assets/template/docker-compose.yml
Normal file
@@ -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:
|
||||||
12
skills/app-builder/assets/template/web/Dockerfile
Normal file
12
skills/app-builder/assets/template/web/Dockerfile
Normal file
@@ -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;"]
|
||||||
17
skills/app-builder/assets/template/web/nginx.conf
Normal file
17
skills/app-builder/assets/template/web/nginx.conf
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
skills/app-builder/assets/template/web/package.json
Normal file
28
skills/app-builder/assets/template/web/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
66
skills/app-builder/references/deploy.md
Normal file
66
skills/app-builder/references/deploy.md
Normal file
@@ -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.<app>.donovankelly.xyz` → api service port
|
||||||
|
- Web: `app.<app>.donovankelly.xyz` → web service port
|
||||||
|
4. Enable HTTPS (Dokploy handles Let's Encrypt)
|
||||||
|
5. Deploy and verify health check
|
||||||
|
|
||||||
|
## Domain Pattern
|
||||||
|
|
||||||
|
- Test: `test-<app>.donovankelly.xyz`
|
||||||
|
- Production: `<app>.donovankelly.xyz`
|
||||||
|
- API: `api.<app>.donovankelly.xyz` (or `api.todo.donovankelly.xyz`)
|
||||||
|
- Frontend: `app.<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
|
||||||
47
skills/app-builder/references/legal-payments.md
Normal file
47
skills/app-builder/references/legal-payments.md
Normal file
@@ -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.
|
||||||
49
skills/app-builder/references/migrations.md
Normal file
49
skills/app-builder/references/migrations.md
Normal file
@@ -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
|
||||||
|
```
|
||||||
43
skills/app-builder/references/rollback.md
Normal file
43
skills/app-builder/references/rollback.md
Normal file
@@ -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
|
||||||
55
skills/app-builder/scripts/scaffold.sh
Executable file
55
skills/app-builder/scripts/scaffold.sh
Executable file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Scaffold a new app from the standard template
|
||||||
|
# Usage: scaffold.sh <app-name> [--api-only]
|
||||||
|
#
|
||||||
|
# Creates project structure, initializes git, and pushes to Gitea.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
APP_NAME="${1:?Usage: scaffold.sh <app-name>}"
|
||||||
|
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"
|
||||||
68
skills/debugging/SKILL.md
Normal file
68
skills/debugging/SKILL.md
Normal file
@@ -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.*
|
||||||
1
todo-app
Submodule
1
todo-app
Submodule
Submodule todo-app added at 617eaacc5f
1
todo-app-web
Submodule
1
todo-app-web
Submodule
Submodule todo-app-web added at 6f26e1117c
Reference in New Issue
Block a user