feat(ui): Settings view + per-space project cards (status/research/edit/delete) + theming pass

- Settings (#/settings): API tokens (mint/list/revoke), Agents list, Orthos Mode placeholder
- Per-space Projects: Void-1-style expandable cards — inline status, ↻ Research (Eithan stub),
  Edit/New modal, Delete-with-confirm; migration 019 adds research_status/notes/timestamps;
  POST /api/projects/:id/research stub; GET /api/agent-tokens list
- Global +1 font bump; themed scrollbars; larger/bolder themed topbar

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
root
2026-06-05 00:06:08 +10:00
parent 4a55c24700
commit 80363d3e68
14 changed files with 432 additions and 113 deletions

View File

@@ -5,6 +5,7 @@ import * as spacesRepo from '../../lib/db/repos/spaces.js';
import * as agentsRepo from '../../lib/db/repos/agents.js';
import * as refsRepo from '../../lib/db/repos/refs.js';
import * as pendingChanges from '../../lib/db/repos/pending_changes.js';
import { applyPendingChange } from '../../lib/api/routes/pending_changes.js';
// Regression coverage for docs/security-followups.md:
// the pending_changes.action CHECK previously rejected the extended actions
@@ -69,6 +70,14 @@ describe('ref upsert as a suggest-tier draft', () => {
expect(created).toBeTruthy();
expect(created.title).toBe('Y');
});
it('approving an upsert for an entity whose repo cannot upsert fails clean (ValidationError, not TypeError)', async () => {
// 'upsert' only legitimately originates from refs; guard the dispatch so a
// stray upsert row on another entity_type fails loud, not with a bare TypeError.
await expect(
applyPendingChange({ entity_type: 'task', action: 'upsert', payload: {} }, owner)
).rejects.toThrow(/does not support upsert/i);
});
});
describe('resource dependency mutations are owner-only', () => {

View File

@@ -0,0 +1,30 @@
import { describe, it, expect, beforeAll } from 'vitest';
import request from 'supertest';
import { pool } from '../../lib/db/pool.js';
import { setup } from './helpers.js';
let app, ownerHeaders, projectId;
beforeAll(async () => {
({ app, ownerHeaders } = await setup());
const { rows: [s] } = await pool.query(`INSERT INTO spaces(slug,name) VALUES('p','P') RETURNING id`);
const { rows: [p] } = await pool.query(
`INSERT INTO projects(space_id,slug,name,status) VALUES($1,'proj','Proj','active') RETURNING id`, [s.id]);
projectId = p.id;
});
describe('project research stub', () => {
it('defaults research_status to none', async () => {
const { rows: [p] } = await pool.query(`SELECT research_status FROM projects WHERE id=$1`, [projectId]);
expect(p.research_status).toBe('none');
});
it('POST /research → requested (owner)', async () => {
const res = await request(app).post(`/api/projects/${projectId}/research`).set(ownerHeaders);
expect(res.status).toBe(200);
expect(res.body.research_status).toBe('requested');
expect(res.body.research_requested_at).toBeTruthy();
});
it('404 for an unknown project', async () => {
const res = await request(app).post(`/api/projects/00000000-0000-0000-0000-000000000000/research`).set(ownerHeaders);
expect(res.status).toBe(404);
});
});