Fix audit follow-up gates

Fix audit follow-up gates

- remove gray-matter and use local YAML frontmatter handling
- upgrade DOMPurify and clear production advisories
- make CLI/MCP smoke skip cleanly without VK_API_KEY
- reduce initial JS below the Mantine QA budget

Closes #753
Closes #754
Closes #755
This commit is contained in:
Brad Groux 2026-06-26 14:59:20 -05:00 committed by GitHub
parent 5bd4377f32
commit 013c2b5383
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 165 additions and 101 deletions

View file

@ -7,6 +7,7 @@ This document outlines the manual integration steps required to complete feature
**Action:** Export the new prompt registry types
Add these lines to the exports:
```typescript
// Prompt Registry Types
export type {
@ -41,9 +42,10 @@ app.use('/api/prompt-registry', promptRegistryRouter);
**Location:** Find where other routes like `templates`, `tasks`, `chat` are registered. Add the prompt registry route in the same pattern.
**Expected pattern:**
```typescript
app.use('/api/templates', templateRouter);
app.use('/api/prompt-registry', promptRegistryRouter); // <-- Add this line
app.use('/api/prompt-registry', promptRegistryRouter); // <-- Add this line
app.use('/api/tasks', taskRouter);
```
@ -151,7 +153,7 @@ These files are complete and require no further changes:
## Notes
- All files follow existing code patterns (zod validation, gray-matter for storage, React Query hooks)
- All files follow existing code patterns (zod validation, YAML frontmatter storage, React Query hooks)
- No external dependencies added beyond what's already in use
- Storage uses file-based approach (consistent with template-service)
- Endpoints follow REST conventions
@ -160,6 +162,7 @@ These files are complete and require no further changes:
## Questions?
Refer to reference implementations:
- **Template pattern:** `server/src/services/template-service.ts`
- **Route pattern:** `server/src/routes/templates.ts`
- **API client pattern:** `web/src/lib/api/entities.ts`

View file

@ -336,7 +336,7 @@ Tasks are markdown files. Settings are JSON. Workflows are YAML. No database, no
| **Frontend** | React, Vite, Tailwind CSS, Shadcn UI | React 19, Vite 7.3, Tailwind 4.2 |
| **Backend** | Express, WebSocket | Express 5.2 |
| **Language** | TypeScript (strict mode) | 6.0 |
| **Storage** | Markdown files with YAML frontmatter | gray-matter |
| **Storage** | Markdown files with YAML frontmatter | yaml + local frontmatter helper |
| **Git** | simple-git, worktree management | — |
| **Testing** | Playwright (E2E), Vitest (unit) | Playwright 1.58, Vitest 4 |
| **Runtime** | Node.js | 22+ |

View file

@ -110,7 +110,7 @@ The Kanban board is the central interface — a drag-and-drop workspace that ref
- **Task types** — Configurable type system with icons and color-coded card borders (code, research, content, automation, and custom types)
- **Priority levels** — Low, medium, and high with visual indicators on cards
- **Markdown storage** — Tasks stored as human-readable `.md` files with YAML frontmatter (via [gray-matter](https://github.com/jonschlinkert/gray-matter))
- **Markdown storage** — Tasks stored as human-readable `.md` files with YAML frontmatter
- **Dark/light mode** — Ships dark by default with a toggle in Settings → General → Appearance; persists to localStorage; inline script in `index.html` prevents flash of wrong theme on load
- **Filter bar** — Search tasks by text, filter by project and task type; filters persist in URL query params
- **Desktop shell controls** — Native-app-style toolbar with workspace selection, health state, view toggles, and left/right/bottom panel controls shared by the web and macOS app shells

77
pnpm-lock.yaml generated
View file

@ -183,9 +183,6 @@ importers:
file-type:
specifier: ^22.0.1
version: 22.0.1
gray-matter:
specifier: ^4.0.3
version: 4.0.3
helmet:
specifier: ^8.2.0
version: 8.2.0
@ -344,8 +341,8 @@ importers:
specifier: ^2.1.1
version: 2.1.1
dompurify:
specifier: ^3.4.10
version: 3.4.10
specifier: ^3.4.11
version: 3.4.11
lucide-react:
specifier: ^1.18.0
version: 1.18.0(react@19.2.7)
@ -2573,8 +2570,8 @@ packages:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
dompurify@3.4.10:
resolution: {integrity: sha512-0xzNv0e7oYC6yyuOGZIABPM4qtg3QxLFniDNPP4ZP90wR8Yq3zgwpRbrNiT4N3IKqDbbYFEJLV+JWEs19aZ//w==}
dompurify@3.4.11:
resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==}
domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
@ -2797,11 +2794,6 @@ packages:
resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
esprima@4.0.1:
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
engines: {node: '>=4'}
hasBin: true
esquery@1.7.0:
resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
engines: {node: '>=0.10'}
@ -2860,10 +2852,6 @@ packages:
resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
engines: {node: '>= 18'}
extend-shallow@2.0.1:
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
engines: {node: '>=0.10.0'}
extend@3.0.2:
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
@ -3078,10 +3066,6 @@ packages:
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
gray-matter@4.0.3:
resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==}
engines: {node: '>=6.0'}
has-bigints@1.1.0:
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
engines: {node: '>= 0.4'}
@ -3286,10 +3270,6 @@ packages:
is-decimal@2.0.1:
resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
is-extendable@0.1.1:
resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==}
engines: {node: '>=0.10.0'}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
@ -3446,10 +3426,6 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@3.14.2:
resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
hasBin: true
js-yaml@4.2.0:
resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==}
hasBin: true
@ -3517,10 +3493,6 @@ packages:
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
kind-of@6.0.3:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
klona@2.0.6:
resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==}
engines: {node: '>= 8'}
@ -4602,10 +4574,6 @@ packages:
scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
section-matter@1.0.0:
resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==}
engines: {node: '>=4'}
secure-json-parse@4.1.0:
resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==}
@ -4814,10 +4782,6 @@ packages:
resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
engines: {node: '>=12'}
strip-bom-string@1.0.0:
resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==}
engines: {node: '>=0.10.0'}
strip-indent@3.0.0:
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
engines: {node: '>=8'}
@ -6506,7 +6470,7 @@ snapshots:
'@types/dompurify@3.2.0':
dependencies:
dompurify: 3.4.10
dompurify: 3.4.11
'@types/esrecurse@4.3.1': {}
@ -7566,7 +7530,7 @@ snapshots:
dependencies:
domelementtype: 2.3.0
dompurify@3.4.10:
dompurify@3.4.11:
optionalDependencies:
'@types/trusted-types': 2.0.7
@ -7984,8 +7948,6 @@ snapshots:
acorn-jsx: 5.3.2(acorn@8.17.0)
eslint-visitor-keys: 5.0.1
esprima@4.0.1: {}
esquery@1.7.0:
dependencies:
estraverse: 5.3.0
@ -8068,10 +8030,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
extend-shallow@2.0.1:
dependencies:
is-extendable: 0.1.1
extend@3.0.2: {}
fast-copy@4.0.3: {}
@ -8328,13 +8286,6 @@ snapshots:
graceful-fs@4.2.11: {}
gray-matter@4.0.3:
dependencies:
js-yaml: 3.14.2
kind-of: 6.0.3
section-matter: 1.0.0
strip-bom-string: 1.0.0
has-bigints@1.1.0: {}
has-flag@4.0.0: {}
@ -8552,8 +8503,6 @@ snapshots:
is-decimal@2.0.1: {}
is-extendable@0.1.1: {}
is-extglob@2.1.1: {}
is-finalizationregistry@1.1.1:
@ -8692,11 +8641,6 @@ snapshots:
js-tokens@4.0.0: {}
js-yaml@3.14.2:
dependencies:
argparse: 1.0.10
esprima: 4.0.1
js-yaml@4.2.0:
dependencies:
argparse: 2.0.1
@ -8796,8 +8740,6 @@ snapshots:
dependencies:
json-buffer: 3.0.1
kind-of@6.0.3: {}
klona@2.0.6: {}
launder@1.7.1:
@ -10146,11 +10088,6 @@ snapshots:
scheduler@0.27.0: {}
section-matter@1.0.0:
dependencies:
extend-shallow: 2.0.1
kind-of: 6.0.3
secure-json-parse@4.1.0: {}
semver-compare@1.0.0:
@ -10407,8 +10344,6 @@ snapshots:
dependencies:
ansi-regex: 6.2.2
strip-bom-string@1.0.0: {}
strip-indent@3.0.0:
dependencies:
min-indent: 1.0.1

View file

@ -18,8 +18,9 @@ let mcpTaskId;
function usage() {
console.log(`Usage: pnpm smoke:cli-mcp -- [options]
Runs the v5 release-candidate CLI and MCP compatibility smoke against a running
Veritas Kanban server. Requires a write-capable scoped API key.
Runs v5 release-candidate CLI and MCP compatibility checks. Local version/build
checks always run. Live read/write smoke runs when a write-capable scoped API
key is available.
Options:
--api-url <url> API origin. Defaults to VK_API_URL or http://localhost:3001.
@ -100,6 +101,10 @@ function warn(name, detail = '') {
record('warn', name, detail);
}
function skip(name, detail = '') {
record('skip', name, detail);
}
function check(name, condition, detail = '') {
if (condition) pass(name, detail);
else fail(name, detail);
@ -468,11 +473,14 @@ async function main() {
const expectedVersion = options.expectedVersion ?? rootPackage.version;
check('VK_API_URL is configured', Boolean(options.apiUrl), options.apiUrl);
check(
'VK_API_KEY is configured for write-capable CLI/MCP smoke',
Boolean(options.apiKey),
options.apiKey ? 'set' : 'missing'
);
if (options.apiKey) {
pass('VK_API_KEY is configured for write-capable CLI/MCP smoke', 'set');
} else {
skip(
'VK_API_KEY is configured for write-capable CLI/MCP smoke',
'missing; set VK_API_KEY or pass --api-key to run the live read/write smoke'
);
}
check(
'Root package version matches expected',
rootPackage.version === expectedVersion,
@ -491,7 +499,12 @@ async function main() {
await assertFile(cliBin, 'CLI build output exists');
await assertFile(mcpBin, 'MCP build output exists');
if (!options.apiUrl || !options.apiKey) {
if (!options.apiUrl) {
printAndExit();
}
if (!options.apiKey) {
skip('CLI/MCP live read/write smoke', 'skipped because VK_API_KEY is missing');
printAndExit();
}
@ -532,13 +545,24 @@ async function main() {
function printAndExit() {
let failures = 0;
let warnings = 0;
let skips = 0;
for (const entry of checks) {
if (entry.status === 'fail') failures += 1;
if (entry.status === 'warn') warnings += 1;
const icon = entry.status === 'pass' ? 'PASS' : entry.status === 'warn' ? 'WARN' : 'FAIL';
if (entry.status === 'skip') skips += 1;
const icon =
entry.status === 'pass'
? 'PASS'
: entry.status === 'warn'
? 'WARN'
: entry.status === 'skip'
? 'SKIP'
: 'FAIL';
console.log(`${icon} ${entry.name}${printableDetail(entry.detail)}`);
}
console.log(`\nCLI/MCP compatibility smoke: ${failures} failure(s), ${warnings} warning(s)`);
console.log(
`\nCLI/MCP compatibility smoke: ${failures} failure(s), ${warnings} warning(s), ${skips} skip(s)`
);
process.exit(failures > 0 ? 1 : 0);
}

View file

@ -29,7 +29,6 @@
"express": "^5.2.1",
"express-rate-limit": "^8.5.2",
"file-type": "^22.0.1",
"gray-matter": "^4.0.3",
"helmet": "^8.2.0",
"jsonwebtoken": "^9.0.3",
"mammoth": "^1.12.0",

View file

@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import fs from 'fs/promises';
import os from 'os';
import path from 'path';
import matter from 'gray-matter';
import matter from '../utils/frontmatter.js';
import yaml from 'yaml';
import { SqlitePortabilityService } from '../services/sqlite-portability-service.js';
import { SqliteDatabase } from '../storage/sqlite/database.js';

View file

@ -6,7 +6,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import matter from 'gray-matter';
import matter from '../utils/frontmatter.js';
// We need to mock the constructor's process.cwd() dependency
// Instead, let's test the service by creating an instance and overriding the dir

View file

@ -8,7 +8,7 @@
import fs from 'fs/promises';
import path from 'path';
import matter from 'gray-matter';
import matter from '../utils/frontmatter.js';
import { nanoid } from 'nanoid';
import type {
ChatSession,

View file

@ -1,6 +1,6 @@
import fs from 'fs/promises';
import path from 'path';
import matter from 'gray-matter';
import matter, { type MarkdownFrontmatterFile } from '../utils/frontmatter.js';
import { createLogger } from '../lib/logger.js';
const log = createLogger('integrity');
@ -203,7 +203,7 @@ async function checkTaskFiles(
report.filesChecked++;
try {
const raw = await fs.readFile(filePath, 'utf-8');
let parsed: matter.GrayMatterFile<string>;
let parsed: MarkdownFrontmatterFile<string>;
try {
parsed = matter(raw);
} catch (fmErr) {

View file

@ -1,7 +1,7 @@
import { readdir, readFile, writeFile, unlink, mkdir } from 'fs/promises';
import { fileExists } from '../storage/fs-helpers.js';
import { join } from 'path';
import matter from 'gray-matter';
import matter from '../utils/frontmatter.js';
import type {
PromptTemplate,
PromptVersion,
@ -379,6 +379,7 @@ export class PromptRegistryService {
versions.push(data as PromptVersion);
} catch (err) {
log.error({ err: err }, `Error reading version ${file}`);
versions.push({} as PromptVersion);
}
}

View file

@ -4,7 +4,7 @@ import path from 'path';
import readline from 'readline';
import { createGunzip } from 'zlib';
import type { SQLInputValue } from 'node:sqlite';
import matter from 'gray-matter';
import matter from '../utils/frontmatter.js';
import yaml from 'yaml';
import type {
AnyTelemetryEvent,

View file

@ -1,6 +1,6 @@
import fs from 'fs/promises';
import path from 'path';
import matter from 'gray-matter';
import matter from '../utils/frontmatter.js';
export type TaskIdentityLocation = 'active' | 'backlog' | 'archive';
export type TaskIdentityConflictKind = 'task-id' | 'business-id';

View file

@ -1,7 +1,7 @@
import fs from 'fs/promises';
import { watch, batchedMap, type FSWatcher } from '../storage/fs-helpers.js';
import path from 'path';
import matter from 'gray-matter';
import matter from '../utils/frontmatter.js';
import { nanoid } from 'nanoid';
import type {
Task,
@ -543,7 +543,7 @@ export class TaskService {
private taskToMarkdown(task: Task): string {
const { description, reviewComments, ...rest } = task;
// Filter out undefined values (gray-matter can't serialize them)
// Filter out undefined values (YAML can't serialize them)
const frontmatter = this.deepCleanUndefined(rest);
const content = matter.stringify(description || '', frontmatter);

View file

@ -1,7 +1,7 @@
import { readdir, readFile, writeFile, unlink, mkdir } from 'fs/promises';
import { fileExists } from '../storage/fs-helpers.js';
import { join } from 'path';
import matter from 'gray-matter';
import matter from '../utils/frontmatter.js';
import type {
TaskTemplate,
CreateTemplateInput,

View file

@ -7,7 +7,7 @@
import fs from 'fs/promises';
import path from 'path';
import matter from 'gray-matter';
import matter from '../utils/frontmatter.js';
import type { Task } from '@veritas-kanban/shared';
import { createLogger } from '../lib/logger.js';
import { getTasksBacklogDir } from '../utils/paths.js';

View file

@ -0,0 +1,85 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
export interface MarkdownFrontmatterFile<T = string> {
data: Record<string, any>;
content: T;
orig: T;
matter: string;
language: 'yaml';
}
interface Frontmatter {
(input: string): MarkdownFrontmatterFile<string>;
stringify(content: string, data?: Record<string, unknown>): string;
}
const DELIMITER_PATTERN = /^(---|\.\.\.)[ \t]*$/;
function asRecord(value: unknown): Record<string, any> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return {};
}
return value as Record<string, any>;
}
export function parseMarkdownFrontmatter(input: string): MarkdownFrontmatterFile<string> {
const source = input.startsWith('\uFEFF') ? input.slice(1) : input;
const firstLineEnd = source.indexOf('\n');
const firstLine = (firstLineEnd === -1 ? source : source.slice(0, firstLineEnd)).replace(
/\r$/,
''
);
if (firstLine !== '---' || firstLineEnd === -1) {
return {
data: {},
content: source,
orig: input,
matter: '',
language: 'yaml',
};
}
const matterStart = firstLineEnd + 1;
let lineStart = matterStart;
while (lineStart <= source.length) {
const nextLineEnd = source.indexOf('\n', lineStart);
const lineEnd = nextLineEnd === -1 ? source.length : nextLineEnd;
const line = source.slice(lineStart, lineEnd).replace(/\r$/, '');
if (DELIMITER_PATTERN.test(line)) {
const matter = source.slice(matterStart, lineStart);
const contentStart = nextLineEnd === -1 ? lineEnd : nextLineEnd + 1;
const parsed = matter.trim() ? parseYaml(matter) : {};
return {
data: asRecord(parsed),
content: source.slice(contentStart),
orig: input,
matter,
language: 'yaml',
};
}
if (nextLineEnd === -1) break;
lineStart = nextLineEnd + 1;
}
throw new Error('Unclosed YAML frontmatter block');
}
export function stringifyMarkdownFrontmatter(
content: string,
data: Record<string, any> = {}
): string {
const yaml = stringifyYaml(data, { lineWidth: 0 }).trimEnd() || '{}';
const body = content || '';
return body ? `---\n${yaml}\n---\n${body}` : `---\n${yaml}\n---\n`;
}
const frontmatter = parseMarkdownFrontmatter as Frontmatter;
frontmatter.stringify = stringifyMarkdownFrontmatter;
export default frontmatter;

View file

@ -27,7 +27,7 @@
"@veritas-kanban/shared": "workspace:*",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dompurify": "^3.4.10",
"dompurify": "^3.4.11",
"lucide-react": "^1.18.0",
"nanoid": "^5.1.11",
"react": "^19.2.7",

View file

@ -1,6 +1,5 @@
import { lazy, Suspense, useEffect, useState } from 'react';
import { Box } from '@mantine/core';
import { KanbanBoard } from './components/board/KanbanBoard';
import { Header } from './components/layout/Header';
import { Toaster } from './components/ui/toaster';
import { KeyboardProvider } from './hooks/useKeyboard';
@ -32,6 +31,12 @@ const LAZY_VIEW_COMPONENTS = Object.fromEntries(
})
) as Record<NavigationView, ReturnType<typeof lazy>>;
const KanbanBoard = lazy(() =>
import('./components/board/KanbanBoard').then((mod) => ({
default: mod.KanbanBoard,
}))
);
const FloatingChat = lazy(() =>
import('./components/chat/FloatingChat').then((mod) => ({
default: mod.FloatingChat,
@ -84,7 +89,13 @@ function MainContent() {
if (runSessionShareId) return <RunSessionShareView shareId={runSessionShareId} />;
if (view === 'board') return <KanbanBoard />;
if (view === 'board') {
return (
<Suspense fallback={<ViewLoading view="board" />}>
<KanbanBoard />
</Suspense>
);
}
const ViewComponent = LAZY_VIEW_COMPONENTS[view];
return (

View file

@ -36,6 +36,12 @@ export default defineConfig({
},
build: {
chunkSizeWarningLimit: 400,
modulePreload: {
resolveDependencies(_filename, deps, context) {
if (context.hostType !== 'html') return deps;
return deps.filter((dep) => !dep.includes('vendor-dnd'));
},
},
rollupOptions: {
output: {
manualChunks(id) {