heyiam 0.2.29 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -0
- package/dist/auth.js +29 -3
- package/dist/config.js +10 -1
- package/dist/db.js +0 -1
- package/dist/export.js +124 -27
- package/dist/format-utils.js +5 -0
- package/dist/github.js +381 -0
- package/dist/index.js +168 -0
- package/dist/mount.js +300 -102
- package/dist/parsers/claude.js +2 -28
- package/dist/parsers/codex.js +2 -26
- package/dist/parsers/cursor.js +2 -26
- package/dist/parsers/duration.js +35 -0
- package/dist/parsers/gemini.js +2 -20
- package/dist/parsers/index.js +22 -3
- package/dist/parsers/types.js +0 -1
- package/dist/public/assets/index-Coilyhtr.css +1 -0
- package/dist/public/assets/index-D0noVMFu.js +44 -0
- package/dist/public/index.html +2 -2
- package/dist/redact.js +4 -104
- package/dist/render/build-render-data.js +9 -2
- package/dist/render/index.js +32 -5
- package/dist/render/liquid.js +147 -7
- package/dist/render/mock-data.js +303 -0
- package/dist/render/templates/aurora/portfolio.liquid +192 -0
- package/dist/render/templates/aurora/project.liquid +260 -0
- package/dist/render/templates/aurora/session.liquid +223 -0
- package/dist/render/templates/aurora/styles.css +1184 -0
- package/dist/render/templates/bauhaus/portfolio.liquid +169 -0
- package/dist/render/templates/bauhaus/project.liquid +300 -0
- package/dist/render/templates/bauhaus/session.liquid +333 -0
- package/dist/render/templates/bauhaus/styles.css +1645 -0
- package/dist/render/templates/blueprint/portfolio.liquid +153 -0
- package/dist/render/templates/blueprint/project.liquid +286 -0
- package/dist/render/templates/blueprint/session.liquid +248 -0
- package/dist/render/templates/blueprint/styles.css +1289 -0
- package/dist/render/templates/canvas/portfolio.liquid +203 -0
- package/dist/render/templates/canvas/project.liquid +235 -0
- package/dist/render/templates/canvas/session.liquid +223 -0
- package/dist/render/templates/canvas/styles.css +1440 -0
- package/dist/render/templates/carbon/portfolio.liquid +160 -0
- package/dist/render/templates/carbon/project.liquid +249 -0
- package/dist/render/templates/carbon/session.liquid +190 -0
- package/dist/render/templates/carbon/styles.css +1097 -0
- package/dist/render/templates/chalk/portfolio.liquid +189 -0
- package/dist/render/templates/chalk/project.liquid +245 -0
- package/dist/render/templates/chalk/session.liquid +215 -0
- package/dist/render/templates/chalk/styles.css +1161 -0
- package/dist/render/templates/circuit/portfolio.liquid +152 -0
- package/dist/render/templates/circuit/project.liquid +247 -0
- package/dist/render/templates/circuit/session.liquid +205 -0
- package/dist/render/templates/circuit/styles.css +1409 -0
- package/dist/render/templates/cosmos/portfolio.liquid +222 -0
- package/dist/render/templates/cosmos/project.liquid +327 -0
- package/dist/render/templates/cosmos/session.liquid +239 -0
- package/dist/render/templates/cosmos/styles.css +1157 -0
- package/dist/render/templates/daylight/portfolio.liquid +207 -0
- package/dist/render/templates/daylight/project.liquid +229 -0
- package/dist/render/templates/daylight/session.liquid +219 -0
- package/dist/render/templates/daylight/styles.css +1315 -0
- package/dist/render/templates/editorial/portfolio.liquid +110 -0
- package/dist/render/templates/editorial/project.liquid +202 -0
- package/dist/render/templates/editorial/session.liquid +171 -0
- package/dist/render/templates/editorial/styles.css +826 -0
- package/dist/render/templates/ember/portfolio.liquid +306 -0
- package/dist/render/templates/ember/project.liquid +232 -0
- package/dist/render/templates/ember/session.liquid +202 -0
- package/dist/render/templates/ember/styles.css +1289 -0
- package/dist/render/templates/glacier/portfolio.liquid +261 -0
- package/dist/render/templates/glacier/project.liquid +288 -0
- package/dist/render/templates/glacier/session.liquid +217 -0
- package/dist/render/templates/glacier/styles.css +1204 -0
- package/dist/render/templates/grid/portfolio.liquid +255 -0
- package/dist/render/templates/grid/project.liquid +306 -0
- package/dist/render/templates/grid/session.liquid +260 -0
- package/dist/render/templates/grid/styles.css +1445 -0
- package/dist/render/templates/kinetic/portfolio.liquid +158 -0
- package/dist/render/templates/kinetic/project.liquid +242 -0
- package/dist/render/templates/kinetic/session.liquid +228 -0
- package/dist/render/templates/kinetic/styles.css +948 -0
- package/dist/render/templates/meridian/portfolio.liquid +243 -0
- package/dist/render/templates/meridian/project.liquid +376 -0
- package/dist/render/templates/meridian/session.liquid +298 -0
- package/dist/render/templates/meridian/styles.css +1375 -0
- package/dist/render/templates/minimal/portfolio.liquid +71 -0
- package/dist/render/templates/minimal/project.liquid +154 -0
- package/dist/render/templates/minimal/session.liquid +140 -0
- package/dist/render/templates/minimal/styles.css +529 -0
- package/dist/render/templates/mono/portfolio.liquid +281 -0
- package/dist/render/templates/mono/project.liquid +275 -0
- package/dist/render/templates/mono/session.liquid +276 -0
- package/dist/render/templates/mono/styles.css +1022 -0
- package/dist/render/templates/neon/portfolio.liquid +207 -0
- package/dist/render/templates/neon/project.liquid +225 -0
- package/dist/render/templates/neon/session.liquid +195 -0
- package/dist/render/templates/neon/styles.css +1271 -0
- package/dist/render/templates/noir/portfolio.liquid +137 -0
- package/dist/render/templates/noir/project.liquid +220 -0
- package/dist/render/templates/noir/session.liquid +241 -0
- package/dist/render/templates/noir/styles.css +1229 -0
- package/dist/render/templates/obsidian/portfolio.liquid +247 -0
- package/dist/render/templates/obsidian/project.liquid +280 -0
- package/dist/render/templates/obsidian/session.liquid +241 -0
- package/dist/render/templates/obsidian/styles.css +1407 -0
- package/dist/render/templates/paper/portfolio.liquid +257 -0
- package/dist/render/templates/paper/project.liquid +235 -0
- package/dist/render/templates/paper/session.liquid +271 -0
- package/dist/render/templates/paper/styles.css +1513 -0
- package/dist/render/templates/parallax/portfolio.liquid +295 -0
- package/dist/render/templates/parallax/project.liquid +275 -0
- package/dist/render/templates/parallax/session.liquid +295 -0
- package/dist/render/templates/parallax/styles.css +1880 -0
- package/dist/render/templates/parchment/portfolio.liquid +280 -0
- package/dist/render/templates/parchment/project.liquid +289 -0
- package/dist/render/templates/parchment/session.liquid +346 -0
- package/dist/render/templates/parchment/styles.css +1401 -0
- package/dist/render/templates/partials/_beats.liquid +16 -0
- package/dist/render/templates/partials/_breadcrumb.liquid +9 -0
- package/dist/render/templates/partials/_footer.liquid +7 -0
- package/dist/render/templates/partials/_growth-chart.liquid +7 -0
- package/dist/render/templates/partials/_key-decisions.liquid +20 -0
- package/dist/render/templates/partials/_links.liquid +16 -0
- package/dist/render/templates/partials/_narrative.liquid +8 -0
- package/dist/render/templates/partials/_phases.liquid +20 -0
- package/dist/render/templates/partials/_portfolio-header.liquid +20 -0
- package/dist/render/templates/partials/_portfolio-projects.liquid +16 -0
- package/dist/render/templates/partials/_portfolio-stats.liquid +19 -0
- package/dist/render/templates/partials/_qa.liquid +13 -0
- package/dist/render/templates/partials/_screenshot.liquid +15 -0
- package/dist/render/templates/partials/_session-cards.liquid +30 -0
- package/dist/render/templates/partials/_session-header.liquid +39 -0
- package/dist/render/templates/partials/_session-sidebar.liquid +30 -0
- package/dist/render/templates/partials/_skills.liquid +12 -0
- package/dist/render/templates/partials/_source-breakdown.liquid +22 -0
- package/dist/render/templates/partials/_stats.liquid +38 -0
- package/dist/render/templates/partials/_work-timeline.liquid +7 -0
- package/dist/render/templates/project.liquid +7 -4
- package/dist/render/templates/radar/portfolio.liquid +223 -0
- package/dist/render/templates/radar/project.liquid +278 -0
- package/dist/render/templates/radar/session.liquid +300 -0
- package/dist/render/templates/radar/styles.css +1055 -0
- package/dist/render/templates/showcase/portfolio.liquid +221 -0
- package/dist/render/templates/showcase/project.liquid +237 -0
- package/dist/render/templates/showcase/session.liquid +210 -0
- package/dist/render/templates/showcase/styles.css +1284 -0
- package/dist/render/templates/signal/portfolio.liquid +217 -0
- package/dist/render/templates/signal/project.liquid +278 -0
- package/dist/render/templates/signal/session.liquid +282 -0
- package/dist/render/templates/signal/styles.css +1401 -0
- package/dist/render/templates/strata/portfolio.liquid +180 -0
- package/dist/render/templates/strata/project.liquid +282 -0
- package/dist/render/templates/strata/session.liquid +261 -0
- package/dist/render/templates/strata/styles.css +1354 -0
- package/dist/render/templates/styles.css +1190 -0
- package/dist/render/templates/terminal/portfolio.liquid +102 -0
- package/dist/render/templates/terminal/project.liquid +161 -0
- package/dist/render/templates/terminal/session.liquid +145 -0
- package/dist/render/templates/terminal/styles.css +497 -0
- package/dist/render/templates/verdant/portfolio.liquid +321 -0
- package/dist/render/templates/verdant/project.liquid +309 -0
- package/dist/render/templates/verdant/session.liquid +237 -0
- package/dist/render/templates/verdant/styles.css +1261 -0
- package/dist/render/templates/zen/portfolio.liquid +124 -0
- package/dist/render/templates/zen/project.liquid +187 -0
- package/dist/render/templates/zen/session.liquid +203 -0
- package/dist/render/templates/zen/styles.css +1211 -0
- package/dist/render/templates.js +90 -0
- package/dist/routes/auth.js +7 -3
- package/dist/routes/context.js +17 -10
- package/dist/routes/delete.js +195 -0
- package/dist/routes/enhance.js +57 -40
- package/dist/routes/export.js +14 -4
- package/dist/routes/github.js +254 -0
- package/dist/routes/index.js +2 -0
- package/dist/routes/portfolio-render-data.js +160 -0
- package/dist/routes/preview.js +555 -108
- package/dist/routes/projects.js +61 -24
- package/dist/routes/publish.js +320 -31
- package/dist/routes/settings.js +194 -1
- package/dist/routes/sse.js +9 -0
- package/dist/search.js +6 -0
- package/dist/server.js +11 -3
- package/dist/settings.js +112 -9
- package/package.json +3 -4
- package/dist/public/assets/index-CC9G8EF1.js +0 -21
- package/dist/public/assets/index-Dalqz2mC.css +0 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template registry for heyi.am project/session rendering.
|
|
3
|
+
*
|
|
4
|
+
* Built-in templates ship with the CLI. Custom user templates
|
|
5
|
+
* can live in ~/.config/heyiam/templates/{name}/ (Phase 2).
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { resolve, dirname } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const TEMPLATES_DIR = resolve(__dirname, 'templates');
|
|
12
|
+
export const BUILT_IN_TEMPLATES = [
|
|
13
|
+
// Original 5
|
|
14
|
+
{ name: 'editorial', label: 'Editorial', description: 'Classic light theme with card-based layout', accent: '#084471', mode: 'light', tags: [] },
|
|
15
|
+
{ name: 'kinetic', label: 'Kinetic', description: 'Matches the heyi.am landing page — orange accent, section tags, narrative cards', accent: '#f97316', mode: 'dark', tags: ['animated'] },
|
|
16
|
+
{ name: 'terminal', label: 'Terminal', description: 'Green-on-black terminal aesthetic with ASCII elements', accent: '#4ade80', mode: 'dark', tags: ['minimal'] },
|
|
17
|
+
{ name: 'minimal', label: 'Typography', description: 'Ultra-clean light mode with serif typography', accent: '#1c1917', mode: 'light', tags: ['minimal'] },
|
|
18
|
+
{ name: 'showcase', label: 'Showcase', description: 'Cinematic scroll animations with animated charts and stat counters', accent: '#818cf8', mode: 'dark', tags: ['animated'] },
|
|
19
|
+
// New templates
|
|
20
|
+
{ name: 'parallax', label: 'Parallax', description: 'Fixed floating headshot with full-page parallax — content scrolls around the photo', accent: '#60a5fa', mode: 'dark', tags: ['animated'] },
|
|
21
|
+
{ name: 'blueprint', label: 'Blueprint', description: 'Engineering schematic with SVG connector lines, grid background, and dimension annotations', accent: '#64748b', mode: 'light', tags: ['animated'] },
|
|
22
|
+
{ name: 'radar', label: 'Radar', description: 'HUD cockpit with radar navigation widget and cyan-tinted luminous elements', accent: '#22d3ee', mode: 'dark', tags: ['animated', 'data-dense'] },
|
|
23
|
+
{ name: 'strata', label: 'Strata', description: 'Depth-based parallax with overlapping card layers and warm amber palette', accent: '#d97706', mode: 'light', tags: ['animated'] },
|
|
24
|
+
{ name: 'noir', label: 'Noir', description: 'Pure monochrome — black, white, gray only with bold typography and film noir drama', accent: '#e5e5e5', mode: 'dark', tags: ['minimal'] },
|
|
25
|
+
{ name: 'verdant', label: 'Verdant', description: 'Nature-inspired with warm earthy palette, leaf motifs, and organic rounded shapes', accent: '#15803d', mode: 'light', tags: [] },
|
|
26
|
+
{ name: 'neon', label: 'Neon', description: 'Synthwave aesthetic with pink and cyan dual accent and tasteful neon glow effects', accent: '#f472b6', mode: 'dark', tags: ['animated'] },
|
|
27
|
+
{ name: 'paper', label: 'Paper', description: 'Newspaper print aesthetic with multi-column layout, drop caps, and serif typography', accent: '#1a1a1a', mode: 'light', tags: ['minimal'] },
|
|
28
|
+
{ name: 'cosmos', label: 'Cosmos', description: 'Starfield background with gold accent and constellation SVG lines connecting elements', accent: '#fbbf24', mode: 'dark', tags: ['animated'] },
|
|
29
|
+
{ name: 'bauhaus', label: 'Bauhaus', description: 'De Stijl geometric shapes in red, blue, yellow with thick borders and asymmetric grids', accent: '#dc2626', mode: 'light', tags: ['animated'] },
|
|
30
|
+
{ name: 'mono', label: 'Mono', description: '100% monospace terminal — green on black, ASCII bar charts, git-log phases, typing animation', accent: '#4ade80', mode: 'dark', tags: ['minimal', 'data-dense'] },
|
|
31
|
+
{ name: 'glacier', label: 'Glacier', description: 'Frosted glassmorphism with backdrop blur cards, cool blue palette, and soft shadows', accent: '#38bdf8', mode: 'light', tags: ['animated'] },
|
|
32
|
+
{ name: 'ember', label: 'Ember', description: 'Warm dark theme with orange-to-red fire gradient accents and ember glow on stats', accent: '#f97316', mode: 'dark', tags: ['animated'] },
|
|
33
|
+
{ name: 'zen', label: 'Zen', description: 'Japanese minimalism — maximum whitespace, no cards, thin rules, serif display, 640px column', accent: '#78716c', mode: 'light', tags: ['minimal'] },
|
|
34
|
+
{ name: 'circuit', label: 'Circuit', description: 'PCB aesthetic with circuit trace patterns, component pads, and lime green accent', accent: '#a3e635', mode: 'dark', tags: ['animated'] },
|
|
35
|
+
{ name: 'parchment', label: 'Parchment', description: 'Old book aesthetic with all-serif typography, sepia palette, drop caps, and colophon footer', accent: '#92400e', mode: 'light', tags: ['minimal'] },
|
|
36
|
+
{ name: 'aurora', label: 'Aurora', description: 'Northern lights gradient header that slowly shifts — restrained dark with teal magic', accent: '#2dd4bf', mode: 'dark', tags: ['animated'] },
|
|
37
|
+
{ name: 'grid', label: 'Grid', description: 'Bento dashboard layout with mixed-size CSS grid cells like iOS widgets', accent: '#6366f1', mode: 'light', tags: ['data-dense'] },
|
|
38
|
+
{ name: 'obsidian', label: 'Obsidian', description: 'Deep black with purple gem accent and hover shimmer like light catching a gemstone', accent: '#a855f7', mode: 'dark', tags: ['animated'] },
|
|
39
|
+
{ name: 'chalk', label: 'Chalk', description: 'Whiteboard aesthetic with handwritten display font, sketch-style borders, and annotation arrows', accent: '#334155', mode: 'light', tags: [] },
|
|
40
|
+
{ name: 'signal', label: 'Signal', description: 'Mission control dashboard — dense data tables, status badges, and fast-updating metrics', accent: '#ef4444', mode: 'dark', tags: ['data-dense'] },
|
|
41
|
+
{ name: 'canvas', label: 'Canvas', description: 'Art gallery with extreme whitespace, full-bleed images, and large airy typography', accent: '#fb7185', mode: 'light', tags: ['minimal'] },
|
|
42
|
+
{ name: 'meridian', label: 'Meridian', description: 'Topographic map aesthetic with contour line patterns and elevation-style charts', accent: '#34d399', mode: 'dark', tags: ['animated'] },
|
|
43
|
+
{ name: 'carbon', label: 'Carbon', description: 'Brushed metal industrial — diagonal stripe texture, silver chrome palette, no color', accent: '#94a3b8', mode: 'dark', tags: ['minimal'] },
|
|
44
|
+
{ name: 'daylight', label: 'Daylight', description: 'Bright and airy with soft blue shadows, sky blue accent, and friendly rounded shapes', accent: '#0ea5e9', mode: 'light', tags: ['animated'] },
|
|
45
|
+
];
|
|
46
|
+
const BUILT_IN_NAMES = new Set(BUILT_IN_TEMPLATES.map((t) => t.name));
|
|
47
|
+
export const DEFAULT_TEMPLATE = 'editorial';
|
|
48
|
+
export function isValidTemplate(name) {
|
|
49
|
+
return BUILT_IN_NAMES.has(name);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve which template to use.
|
|
53
|
+
* Priority: project override → user default → 'editorial'
|
|
54
|
+
*/
|
|
55
|
+
export function resolveTemplate(projectTemplate, userDefault) {
|
|
56
|
+
if (projectTemplate && isValidTemplate(projectTemplate))
|
|
57
|
+
return projectTemplate;
|
|
58
|
+
if (userDefault && isValidTemplate(userDefault))
|
|
59
|
+
return userDefault;
|
|
60
|
+
return DEFAULT_TEMPLATE;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Load concatenated CSS for a template (base + template-specific).
|
|
64
|
+
* Used by export.ts for standalone HTML and by preview.
|
|
65
|
+
*/
|
|
66
|
+
const cssCache = new Map();
|
|
67
|
+
export function getTemplateCss(templateName) {
|
|
68
|
+
const name = isValidTemplate(templateName) ? templateName : DEFAULT_TEMPLATE;
|
|
69
|
+
const cached = cssCache.get(name);
|
|
70
|
+
if (cached !== undefined)
|
|
71
|
+
return cached;
|
|
72
|
+
let css = '';
|
|
73
|
+
try {
|
|
74
|
+
css = readFileSync(resolve(TEMPLATES_DIR, 'styles.css'), 'utf-8');
|
|
75
|
+
}
|
|
76
|
+
catch { /* empty */ }
|
|
77
|
+
try {
|
|
78
|
+
const templateCss = readFileSync(resolve(TEMPLATES_DIR, name, 'styles.css'), 'utf-8');
|
|
79
|
+
css += '\n\n/* === ' + name + ' template styles === */\n' + templateCss;
|
|
80
|
+
}
|
|
81
|
+
catch { /* no template-specific CSS — fine */ }
|
|
82
|
+
cssCache.set(name, css);
|
|
83
|
+
return css;
|
|
84
|
+
}
|
|
85
|
+
export function getTemplateNames() {
|
|
86
|
+
return BUILT_IN_TEMPLATES.map((t) => t.name);
|
|
87
|
+
}
|
|
88
|
+
export function getTemplateInfo(name) {
|
|
89
|
+
return BUILT_IN_TEMPLATES.find((t) => t.name === name);
|
|
90
|
+
}
|
package/dist/routes/auth.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
-
import { checkAuthStatus, saveAuthToken, deleteAuthToken } from '../auth.js';
|
|
2
|
+
import { checkAuthStatus, saveAuthToken, deleteAuthToken, normalizeUsername } from '../auth.js';
|
|
3
3
|
import { API_URL } from '../config.js';
|
|
4
4
|
export function createAuthRouter(_ctx) {
|
|
5
5
|
const router = Router();
|
|
@@ -100,8 +100,12 @@ export function createAuthRouter(_ctx) {
|
|
|
100
100
|
});
|
|
101
101
|
const data = await response.json();
|
|
102
102
|
if (response.ok && data.access_token) {
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
// Always persist and echo the lowercase form so downstream URL
|
|
104
|
+
// construction and UI display stay consistent with Phoenix's
|
|
105
|
+
// lowercase-only DB constraint.
|
|
106
|
+
const username = normalizeUsername(String(data.username ?? ''));
|
|
107
|
+
saveAuthToken(data.access_token, username);
|
|
108
|
+
res.json({ authenticated: true, username });
|
|
105
109
|
}
|
|
106
110
|
else {
|
|
107
111
|
res.status(response.status).json(data);
|
package/dist/routes/context.js
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* Created during the server.ts refactor to avoid circular dependencies.
|
|
4
4
|
*/
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { readFileSync } from 'node:fs';
|
|
7
6
|
import { fileURLToPath } from 'node:url';
|
|
8
7
|
import { listSessions, parseSession } from '../parsers/index.js';
|
|
9
8
|
import { bridgeToAnalyzer, mergeActiveIntervals, sumIntervalMs } from '../bridge.js';
|
|
10
9
|
import { analyzeSession } from '../analyzer.js';
|
|
11
10
|
import { loadEnhancedData, loadProjectEnhanceResult, getUploadedState, } from '../settings.js';
|
|
11
|
+
import { getTemplateCss } from '../render/templates.js';
|
|
12
12
|
import { archiveSessionFiles } from '../archive.js';
|
|
13
13
|
import { getDatabase, openDatabase, getSessionStats as dbGetSessionStats, getSessionCount, getAllSessionMetas, getAllProjectStats, getSessionsByProject, getProjectUuid, } from '../db.js';
|
|
14
14
|
import { ensureSessionIndexed, displayNameFromDir } from '../sync.js';
|
|
@@ -39,7 +39,16 @@ function computeMergedDurationFromDb(db, projectDir, naiveSumMinutes) {
|
|
|
39
39
|
return mergedMinutes > 0 ? mergedMinutes : naiveSumMinutes;
|
|
40
40
|
}
|
|
41
41
|
import { escapeHtml } from '../format-utils.js';
|
|
42
|
-
|
|
42
|
+
/** Look up a project by name or dirName, sending 404 if not found. Returns null on miss. */
|
|
43
|
+
export async function requireProject(ctx, projectParam, res) {
|
|
44
|
+
const projects = await ctx.getProjects();
|
|
45
|
+
const proj = projects.find((p) => p.name === projectParam || p.dirName === projectParam);
|
|
46
|
+
if (!proj) {
|
|
47
|
+
res.status(404).json({ error: { code: 'PROJECT_NOT_FOUND', message: 'Project not found' } });
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return proj;
|
|
51
|
+
}
|
|
43
52
|
/**
|
|
44
53
|
* Build an agent summary from child session metas. Returns null when
|
|
45
54
|
* there are no children (or none produce valid stats).
|
|
@@ -418,6 +427,7 @@ export function createRouteContext(sessionsBasePath, dbPath) {
|
|
|
418
427
|
uploadedSessionCount: published?.uploadedSessions?.length ?? 0,
|
|
419
428
|
uploadedSessions: published?.uploadedSessions ?? [],
|
|
420
429
|
enhancedAt: enhanceCache?.enhancedAt ?? null,
|
|
430
|
+
enhancedSessionCount: enhanceCache?.selectedSessionIds?.length ?? 0,
|
|
421
431
|
totalAgentDuration: agentRow.total,
|
|
422
432
|
totalInputTokens: dbStats.totalInputTokens,
|
|
423
433
|
totalOutputTokens: dbStats.totalOutputTokens,
|
|
@@ -460,19 +470,16 @@ export function createRouteContext(sessionsBasePath, dbPath) {
|
|
|
460
470
|
uploadedSessionCount: published?.uploadedSessions?.length ?? 0,
|
|
461
471
|
uploadedSessions: published?.uploadedSessions ?? [],
|
|
462
472
|
enhancedAt: enhanceCache?.enhancedAt ?? null,
|
|
473
|
+
enhancedSessionCount: enhanceCache?.selectedSessionIds?.length ?? 0,
|
|
463
474
|
totalAgentDuration,
|
|
464
475
|
totalInputTokens: 0,
|
|
465
476
|
totalOutputTokens: 0,
|
|
466
477
|
};
|
|
467
478
|
}
|
|
468
479
|
// ── buildPreviewPage ─────────────────────────────────────
|
|
469
|
-
function buildPreviewPage(title, bodyHtml, banner) {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
try {
|
|
473
|
-
inlineCss = readFileSync(renderCssPath, 'utf-8');
|
|
474
|
-
}
|
|
475
|
-
catch { /* */ }
|
|
480
|
+
function buildPreviewPage(title, bodyHtml, banner, templateName) {
|
|
481
|
+
// Load full template CSS (base + template-specific) via the same path as the React embed
|
|
482
|
+
const inlineCss = getTemplateCss(templateName || 'editorial');
|
|
476
483
|
const cssTag = `<style>${inlineCss}\n/* Preview override */\nbody { overflow: auto !important; min-height: auto !important; }\n#root { min-height: auto !important; }</style>`;
|
|
477
484
|
const bannerHtml = banner
|
|
478
485
|
? `<div style="background: var(--primary, #084471); color: white; text-align: center; padding: 0.5rem; font-family: 'Inter', sans-serif; font-size: 0.75rem; letter-spacing: 0.05em;">${escapeHtml(banner)}</div>`
|
|
@@ -486,7 +493,7 @@ export function createRouteContext(sessionsBasePath, dbPath) {
|
|
|
486
493
|
<title>${escapeHtml(title)} — Preview</title>
|
|
487
494
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
488
495
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
489
|
-
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
496
|
+
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400&family=Newsreader:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet" />
|
|
490
497
|
${cssTag}
|
|
491
498
|
</head>
|
|
492
499
|
<body>
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { getAuthToken } from '../auth.js';
|
|
3
|
+
import { API_URL, warnIfNonDefaultApiUrl } from '../config.js';
|
|
4
|
+
import { getUploadedState, clearUploadedState, saveUploadedState, loadEnhancedData, saveEnhancedData, } from '../settings.js';
|
|
5
|
+
function sendError(res, status, error) {
|
|
6
|
+
res.status(status).json({ error });
|
|
7
|
+
}
|
|
8
|
+
function validatePathParam(value, field) {
|
|
9
|
+
if (typeof value !== 'string') {
|
|
10
|
+
return { ok: false, error: { code: 'INVALID_PARAM', message: `${field} is required` } };
|
|
11
|
+
}
|
|
12
|
+
const trimmed = value.trim();
|
|
13
|
+
if (trimmed.length === 0) {
|
|
14
|
+
return { ok: false, error: { code: 'INVALID_PARAM', message: `${field} is required` } };
|
|
15
|
+
}
|
|
16
|
+
if (trimmed.length > 200) {
|
|
17
|
+
return { ok: false, error: { code: 'INVALID_PARAM', message: `${field} exceeds 200 characters` } };
|
|
18
|
+
}
|
|
19
|
+
return { ok: true, value };
|
|
20
|
+
}
|
|
21
|
+
export function createDeleteRouter(_ctx) {
|
|
22
|
+
const router = Router();
|
|
23
|
+
/**
|
|
24
|
+
* DELETE /api/projects/:project/remote
|
|
25
|
+
*
|
|
26
|
+
* Removes the project (and all its sessions, per Phoenix contract) from
|
|
27
|
+
* heyi.am. Does NOT touch local archived session data — the user may be
|
|
28
|
+
* mid-edit. Clears the local uploaded-state record so the UI re-reflects
|
|
29
|
+
* "Local only" after the round-trip.
|
|
30
|
+
*
|
|
31
|
+
* :project is the CLI-side directory name, NOT the published slug. We
|
|
32
|
+
* resolve the published slug from local uploaded state — if the user
|
|
33
|
+
* never published from this machine we have no slug to delete against.
|
|
34
|
+
*/
|
|
35
|
+
router.delete('/api/projects/:project/remote', async (req, res) => {
|
|
36
|
+
const projectResult = validatePathParam(req.params.project, 'project');
|
|
37
|
+
if (!projectResult.ok) {
|
|
38
|
+
sendError(res, 400, projectResult.error);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const project = projectResult.value;
|
|
42
|
+
const auth = getAuthToken();
|
|
43
|
+
warnIfNonDefaultApiUrl();
|
|
44
|
+
if (!auth) {
|
|
45
|
+
sendError(res, 401, { code: 'UNAUTHENTICATED', message: 'Authentication required' });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const uploaded = getUploadedState(project);
|
|
49
|
+
if (!uploaded?.slug) {
|
|
50
|
+
sendError(res, 404, {
|
|
51
|
+
code: 'NOT_PUBLISHED',
|
|
52
|
+
message: 'This project has no remote copy to delete',
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const phoenixRes = await fetch(`${API_URL}/api/projects/${encodeURIComponent(uploaded.slug)}`, {
|
|
58
|
+
method: 'DELETE',
|
|
59
|
+
headers: { Authorization: `Bearer ${auth.token}` },
|
|
60
|
+
});
|
|
61
|
+
if (phoenixRes.status === 204) {
|
|
62
|
+
// Strip local uploaded state + session 'uploaded' flags so UI
|
|
63
|
+
// shows the correct status. Failure to clear local flags is
|
|
64
|
+
// non-fatal (the remote copy is already gone).
|
|
65
|
+
try {
|
|
66
|
+
clearUploadedState(project);
|
|
67
|
+
for (const sessionId of uploaded.uploadedSessions ?? []) {
|
|
68
|
+
const enhanced = loadEnhancedData(sessionId);
|
|
69
|
+
if (enhanced?.uploaded) {
|
|
70
|
+
saveEnhancedData(sessionId, { ...enhanced, uploaded: false });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (cleanupErr) {
|
|
75
|
+
console.warn('[delete-project] local cleanup failed:', cleanupErr.message);
|
|
76
|
+
}
|
|
77
|
+
res.json({ ok: true });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (phoenixRes.status === 404) {
|
|
81
|
+
// Remote already gone — still clear local state so UI
|
|
82
|
+
// re-renders as "Local only". Surface 404 so UI can inform
|
|
83
|
+
// the user the remote copy was already missing.
|
|
84
|
+
try {
|
|
85
|
+
clearUploadedState(project);
|
|
86
|
+
}
|
|
87
|
+
catch { /* best effort */ }
|
|
88
|
+
sendError(res, 404, {
|
|
89
|
+
code: 'NOT_FOUND',
|
|
90
|
+
message: 'Project not found on heyi.am (already deleted?)',
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (phoenixRes.status === 401 || phoenixRes.status === 403) {
|
|
95
|
+
sendError(res, phoenixRes.status, {
|
|
96
|
+
code: 'UNAUTHORIZED',
|
|
97
|
+
message: 'Not authorized to delete this project',
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const status = phoenixRes.status >= 500 ? 502 : phoenixRes.status;
|
|
102
|
+
sendError(res, status, {
|
|
103
|
+
code: 'DELETE_FAILED',
|
|
104
|
+
message: `Remote delete failed (HTTP ${phoenixRes.status})`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
const message = err.message;
|
|
109
|
+
console.error('[delete-project] Error:', message);
|
|
110
|
+
sendError(res, 502, { code: 'DELETE_FAILED', message });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
/**
|
|
114
|
+
* DELETE /api/projects/:project/sessions/:sessionId/remote
|
|
115
|
+
*
|
|
116
|
+
* Removes a single session from heyi.am. Local archive is untouched.
|
|
117
|
+
* Updates the local uploaded-state record so the session no longer
|
|
118
|
+
* appears in the "uploaded" set. Leaves the project uploaded-state
|
|
119
|
+
* shell in place when this was the last session — per spec, the user
|
|
120
|
+
* may be mid-edit.
|
|
121
|
+
*/
|
|
122
|
+
router.delete('/api/projects/:project/sessions/:sessionId/remote', async (req, res) => {
|
|
123
|
+
const projectResult = validatePathParam(req.params.project, 'project');
|
|
124
|
+
if (!projectResult.ok) {
|
|
125
|
+
sendError(res, 400, projectResult.error);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const sessionResult = validatePathParam(req.params.sessionId, 'sessionId');
|
|
129
|
+
if (!sessionResult.ok) {
|
|
130
|
+
sendError(res, 400, sessionResult.error);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const project = projectResult.value;
|
|
134
|
+
const sessionId = sessionResult.value;
|
|
135
|
+
const auth = getAuthToken();
|
|
136
|
+
warnIfNonDefaultApiUrl();
|
|
137
|
+
if (!auth) {
|
|
138
|
+
sendError(res, 401, { code: 'UNAUTHENTICATED', message: 'Authentication required' });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const phoenixRes = await fetch(`${API_URL}/api/sessions/${encodeURIComponent(sessionId)}`, {
|
|
143
|
+
method: 'DELETE',
|
|
144
|
+
headers: { Authorization: `Bearer ${auth.token}` },
|
|
145
|
+
});
|
|
146
|
+
if (phoenixRes.status === 204) {
|
|
147
|
+
try {
|
|
148
|
+
const uploaded = getUploadedState(project);
|
|
149
|
+
if (uploaded) {
|
|
150
|
+
const remaining = (uploaded.uploadedSessions ?? []).filter((id) => id !== sessionId);
|
|
151
|
+
saveUploadedState(project, {
|
|
152
|
+
slug: uploaded.slug,
|
|
153
|
+
projectId: uploaded.projectId,
|
|
154
|
+
uploadedSessions: remaining,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const enhanced = loadEnhancedData(sessionId);
|
|
158
|
+
if (enhanced?.uploaded) {
|
|
159
|
+
saveEnhancedData(sessionId, { ...enhanced, uploaded: false });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch (cleanupErr) {
|
|
163
|
+
console.warn('[delete-session] local cleanup failed:', cleanupErr.message);
|
|
164
|
+
}
|
|
165
|
+
res.json({ ok: true });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (phoenixRes.status === 404) {
|
|
169
|
+
sendError(res, 404, {
|
|
170
|
+
code: 'NOT_FOUND',
|
|
171
|
+
message: 'Session not found on heyi.am (already deleted?)',
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (phoenixRes.status === 401 || phoenixRes.status === 403) {
|
|
176
|
+
sendError(res, phoenixRes.status, {
|
|
177
|
+
code: 'UNAUTHORIZED',
|
|
178
|
+
message: 'Not authorized to delete this session',
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const status = phoenixRes.status >= 500 ? 502 : phoenixRes.status;
|
|
183
|
+
sendError(res, status, {
|
|
184
|
+
code: 'DELETE_FAILED',
|
|
185
|
+
message: `Remote delete failed (HTTP ${phoenixRes.status})`,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
const message = err.message;
|
|
190
|
+
console.error('[delete-session] Error:', message);
|
|
191
|
+
sendError(res, 502, { code: 'DELETE_FAILED', message });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
return router;
|
|
195
|
+
}
|
package/dist/routes/enhance.js
CHANGED
|
@@ -3,6 +3,9 @@ import { getProvider } from '../llm/index.js';
|
|
|
3
3
|
import { triageSessions } from '../llm/triage.js';
|
|
4
4
|
import { enhanceProject, refineNarrative } from '../llm/project-enhance.js';
|
|
5
5
|
import { getAnthropicApiKey, saveEnhancedData, loadEnhancedData, deleteEnhancedData, loadFreshProjectEnhanceResult, saveProjectEnhanceResult, loadProjectEnhanceResult, buildProjectFingerprint, getUploadedState, } from '../settings.js';
|
|
6
|
+
import { requireProject } from './context.js';
|
|
7
|
+
import { startSSE } from './sse.js';
|
|
8
|
+
import { invalidatePortfolioPreviewCache } from './preview.js';
|
|
6
9
|
export function createEnhanceRouter(ctx) {
|
|
7
10
|
const router = Router();
|
|
8
11
|
// Triage endpoint -- AI selects which sessions are worth showcasing (SSE stream)
|
|
@@ -11,21 +14,11 @@ export function createEnhanceRouter(ctx) {
|
|
|
11
14
|
res.status(400).json({ error: { code: 'NO_API_KEY', message: 'No Anthropic API key configured. Add one in Settings or set ANTHROPIC_API_KEY.' } });
|
|
12
15
|
return;
|
|
13
16
|
}
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
if (!proj) {
|
|
18
|
-
res.status(404).json({ error: { code: 'PROJECT_NOT_FOUND', message: 'Project not found' } });
|
|
17
|
+
const project = String(req.params.project);
|
|
18
|
+
const proj = await requireProject(ctx, project, res);
|
|
19
|
+
if (!proj)
|
|
19
20
|
return;
|
|
20
|
-
|
|
21
|
-
res.writeHead(200, {
|
|
22
|
-
'Content-Type': 'text/event-stream',
|
|
23
|
-
'Cache-Control': 'no-cache',
|
|
24
|
-
Connection: 'keep-alive',
|
|
25
|
-
});
|
|
26
|
-
const send = (event) => {
|
|
27
|
-
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
28
|
-
};
|
|
21
|
+
const send = startSSE(res);
|
|
29
22
|
try {
|
|
30
23
|
const total = proj.sessions.length;
|
|
31
24
|
const sessionsWithStats = [];
|
|
@@ -62,13 +55,11 @@ export function createEnhanceRouter(ctx) {
|
|
|
62
55
|
// Enhance a single session
|
|
63
56
|
router.post('/api/projects/:project/sessions/:id/enhance', async (req, res) => {
|
|
64
57
|
try {
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const proj =
|
|
68
|
-
if (!proj)
|
|
69
|
-
res.status(404).json({ error: { code: 'PROJECT_NOT_FOUND', message: 'Project not found' } });
|
|
58
|
+
const project = String(req.params.project);
|
|
59
|
+
const id = String(req.params.id);
|
|
60
|
+
const proj = await requireProject(ctx, project, res);
|
|
61
|
+
if (!proj)
|
|
70
62
|
return;
|
|
71
|
-
}
|
|
72
63
|
const meta = proj.sessions.find((s) => s.sessionId === id);
|
|
73
64
|
if (!meta) {
|
|
74
65
|
res.status(404).json({ error: { code: 'SESSION_NOT_FOUND', message: 'Session not found' } });
|
|
@@ -91,6 +82,43 @@ export function createEnhanceRouter(ctx) {
|
|
|
91
82
|
});
|
|
92
83
|
}
|
|
93
84
|
});
|
|
85
|
+
// Update locally-saved enhanced data (partial merge)
|
|
86
|
+
router.patch('/api/sessions/:id/enhanced', (req, res) => {
|
|
87
|
+
const { id } = req.params;
|
|
88
|
+
const existing = loadEnhancedData(id);
|
|
89
|
+
if (!existing) {
|
|
90
|
+
res.status(404).json({ error: { code: 'NOT_FOUND', message: 'No enhanced data for this session' } });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const { title, developerTake, skills, qaPairs, executionSteps } = req.body;
|
|
94
|
+
if (title !== undefined && (typeof title !== 'string' || title.length === 0 || title.length > 200)) {
|
|
95
|
+
res.status(400).json({ error: { code: 'INVALID_INPUT', message: 'title must be 1-200 characters' } });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (developerTake !== undefined && (typeof developerTake !== 'string' || developerTake.length > 2000)) {
|
|
99
|
+
res.status(400).json({ error: { code: 'INVALID_INPUT', message: 'developerTake must be under 2000 characters' } });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (skills !== undefined && (!Array.isArray(skills) || !skills.every((s) => typeof s === 'string'))) {
|
|
103
|
+
res.status(400).json({ error: { code: 'INVALID_INPUT', message: 'skills must be an array of strings' } });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const merged = {
|
|
107
|
+
...existing,
|
|
108
|
+
...(title !== undefined ? { title } : {}),
|
|
109
|
+
...(developerTake !== undefined ? { developerTake } : {}),
|
|
110
|
+
...(skills !== undefined ? { skills } : {}),
|
|
111
|
+
...(qaPairs !== undefined ? { qaPairs } : {}),
|
|
112
|
+
...(executionSteps !== undefined ? { executionSteps } : {}),
|
|
113
|
+
};
|
|
114
|
+
// Strip runtime-only fields before saving — saveEnhancedData re-adds enhancedAt
|
|
115
|
+
const { enhancedAt: _ea, quickEnhanced: qe, ...rest } = merged;
|
|
116
|
+
saveEnhancedData(id, { ...rest, quickEnhanced: qe });
|
|
117
|
+
invalidatePortfolioPreviewCache();
|
|
118
|
+
console.log(`[enhance] Updated enhanced data for ${id}`);
|
|
119
|
+
const updated = loadEnhancedData(id);
|
|
120
|
+
res.json({ ok: true, enhancedAt: updated?.enhancedAt });
|
|
121
|
+
});
|
|
94
122
|
// Delete locally-saved enhanced data
|
|
95
123
|
router.delete('/api/sessions/:id/enhanced', (_req, res) => {
|
|
96
124
|
const { id } = _req.params;
|
|
@@ -124,14 +152,7 @@ export function createEnhanceRouter(ctx) {
|
|
|
124
152
|
res.status(400).json({ error: { code: 'NO_API_KEY', message: 'No Anthropic API key configured. Add one in Settings or set ANTHROPIC_API_KEY.' } });
|
|
125
153
|
return;
|
|
126
154
|
}
|
|
127
|
-
res
|
|
128
|
-
'Content-Type': 'text/event-stream',
|
|
129
|
-
'Cache-Control': 'no-cache',
|
|
130
|
-
Connection: 'keep-alive',
|
|
131
|
-
});
|
|
132
|
-
const send = (data) => {
|
|
133
|
-
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
134
|
-
};
|
|
155
|
+
const send = startSSE(res);
|
|
135
156
|
try {
|
|
136
157
|
const projects = await ctx.getProjects();
|
|
137
158
|
const proj = projects.find((p) => p.name === project || p.dirName === project);
|
|
@@ -237,7 +258,7 @@ export function createEnhanceRouter(ctx) {
|
|
|
237
258
|
});
|
|
238
259
|
// Save project enhance result explicitly
|
|
239
260
|
router.post('/api/projects/:project/enhance-save', async (req, res) => {
|
|
240
|
-
const
|
|
261
|
+
const project = String(req.params.project);
|
|
241
262
|
const { selectedSessionIds, result, title, repoUrl, projectUrl, screenshotBase64 } = req.body;
|
|
242
263
|
if (!Array.isArray(selectedSessionIds) || !result?.narrative) {
|
|
243
264
|
res.status(400).json({ error: { code: 'INVALID_INPUT', message: 'selectedSessionIds and result are required' } });
|
|
@@ -248,13 +269,12 @@ export function createEnhanceRouter(ctx) {
|
|
|
248
269
|
return;
|
|
249
270
|
}
|
|
250
271
|
try {
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
if (!proj) {
|
|
254
|
-
res.status(404).json({ error: { code: 'PROJECT_NOT_FOUND', message: 'Project not found' } });
|
|
272
|
+
const proj = await requireProject(ctx, project, res);
|
|
273
|
+
if (!proj)
|
|
255
274
|
return;
|
|
256
|
-
}
|
|
257
275
|
saveProjectEnhanceResult(proj.dirName, selectedSessionIds, result, undefined, { title, repoUrl, projectUrl, screenshotBase64 });
|
|
276
|
+
// Project title/narrative/skills appear in portfolio listing — bust cache.
|
|
277
|
+
invalidatePortfolioPreviewCache();
|
|
258
278
|
res.json({ saved: true, enhancedAt: new Date().toISOString() });
|
|
259
279
|
}
|
|
260
280
|
catch (err) {
|
|
@@ -263,14 +283,11 @@ export function createEnhanceRouter(ctx) {
|
|
|
263
283
|
});
|
|
264
284
|
// Get cached project enhance result
|
|
265
285
|
router.get('/api/projects/:project/enhance-cache', async (req, res) => {
|
|
266
|
-
const
|
|
286
|
+
const project = String(req.params.project);
|
|
267
287
|
try {
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
if (!proj) {
|
|
271
|
-
res.status(404).json({ error: { code: 'PROJECT_NOT_FOUND', message: 'Project not found' } });
|
|
288
|
+
const proj = await requireProject(ctx, project, res);
|
|
289
|
+
if (!proj)
|
|
272
290
|
return;
|
|
273
|
-
}
|
|
274
291
|
const cached = loadProjectEnhanceResult(proj.dirName);
|
|
275
292
|
if (!cached) {
|
|
276
293
|
res.status(404).json({ error: { code: 'NO_CACHE', message: 'No cached enhance result' } });
|
package/dist/routes/export.js
CHANGED
|
@@ -111,9 +111,14 @@ export function createExportRouter(ctx) {
|
|
|
111
111
|
return;
|
|
112
112
|
}
|
|
113
113
|
const cache = data.enhanceCache ?? buildFallbackCache(data.sessions);
|
|
114
|
-
const
|
|
114
|
+
const proj = data.project;
|
|
115
115
|
const outDir = safeExportPath(outputPath, dirName, 'html');
|
|
116
|
-
const result = await exportHtml(dirName, cache, data.sessions, outDir, 'local', {
|
|
116
|
+
const result = await exportHtml(dirName, cache, data.sessions, outDir, 'local', {
|
|
117
|
+
totalFilesChanged: proj.totalFiles,
|
|
118
|
+
totalAgentDurationMinutes: proj.totalAgentDuration,
|
|
119
|
+
totalInputTokens: proj.totalInputTokens,
|
|
120
|
+
totalOutputTokens: proj.totalOutputTokens,
|
|
121
|
+
});
|
|
117
122
|
res.json(result);
|
|
118
123
|
}
|
|
119
124
|
catch (err) {
|
|
@@ -131,8 +136,13 @@ export function createExportRouter(ctx) {
|
|
|
131
136
|
return;
|
|
132
137
|
}
|
|
133
138
|
const cache = data.enhanceCache ?? buildFallbackCache(data.sessions);
|
|
134
|
-
const
|
|
135
|
-
const htmlFiles = generateHtmlFiles(dirName, cache, data.sessions, 'local', {
|
|
139
|
+
const proj = data.project;
|
|
140
|
+
const htmlFiles = generateHtmlFiles(dirName, cache, data.sessions, 'local', {
|
|
141
|
+
totalFilesChanged: proj.totalFiles,
|
|
142
|
+
totalAgentDurationMinutes: proj.totalAgentDuration,
|
|
143
|
+
totalInputTokens: proj.totalInputTokens,
|
|
144
|
+
totalOutputTokens: proj.totalOutputTokens,
|
|
145
|
+
});
|
|
136
146
|
const zipBuffer = createZipBuffer(htmlFiles);
|
|
137
147
|
const filename = `${dirName.replace(/[^a-zA-Z0-9_-]/g, '_')}.zip`;
|
|
138
148
|
res.setHeader('Content-Type', 'application/zip');
|