feat: Phase 6 - MCP + Webhooks + Worker + Polish

- MCP server: stateless JSON-RPC 2.0 with 18 tools (tasks, habits, projects, notes, domains, search, activity)
- Webhooks API: CRUD routes under /api/domains/[domainId]/webhooks/ with test endpoint and deliveries log
- Webhook delivery: HMAC-SHA256 signed POST with retry (exponential backoff, max 6)
- Worker rewrite: Drizzle ORM instead of PocketBase, polls jobs table, handles webhook_delivery, recurring_spawn, ai_dispatch
- Rate limiting: token bucket per IP/API key (100 req/min REST, 300 req/min MCP)
- Keyboard help overlay: ? opens Radix Dialog with search/filter, Esc closes
- AI @mention stub: @agent in command palette dispatches CustomEvent
- Mobile responsive: bottom nav, single-column kanban, day view calendar, 44px touch targets
- Accessibility: skip-to-content link, focus rings, aria-labels, color contrast
- E2E tests: mcp.spec.ts, webhooks.spec.ts, realtime.spec.ts added
- Schema: api_keys and webhook_deliveries tables with migration
- Removed old PocketBase-style database.ts from worker
This commit is contained in:
2026-07-29 08:03:28 -04:00
parent eba1d78fb9
commit e5b7d9e2ee
28 changed files with 4959 additions and 884 deletions
+48
View File
@@ -479,3 +479,51 @@ export const webhooks = pgTable(
index('webhooks_active_idx').on(table.active),
]
);
// ── Webhook Deliveries ──────────────────────────────────────────────────────────
export const webhookDeliveries = pgTable(
'webhook_deliveries',
{
id: uuid('id').defaultRandom().primaryKey(),
webhookId: uuid('webhook_id')
.notNull()
.references((): any => webhooks.id, { onDelete: 'cascade' }),
event: text('event').notNull(),
payload: jsonb('payload').$type<Record<string, unknown>>().default({}),
status: text('status').notNull().default('pending'),
statusCode: integer('status_code').default(0),
responseBody: text('response_body'),
attempts: integer('attempts').default(0),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
},
(table) => [
index('webhook_deliveries_webhook_id_idx').on(table.webhookId),
index('webhook_deliveries_status_idx').on(table.status),
index('webhook_deliveries_created_at_idx').on(table.createdAt),
]
);
// ── API Keys ─────────────────────────────────────────────────────────────────────
export const apiKeys = pgTable(
'api_keys',
{
id: uuid('id').defaultRandom().primaryKey(),
userId: uuid('user_id')
.notNull()
.references((): any => users.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
keyHash: text('key_hash').notNull(),
keyPrefix: text('key_prefix').notNull(),
active: boolean('active').default(true),
lastUsedAt: timestamp('last_used_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
},
(table) => [
index('api_keys_user_id_idx').on(table.userId),
index('api_keys_key_hash_idx').on(table.keyHash),
index('api_keys_active_idx').on(table.active),
]
);