heyiam 0.2.29 → 0.3.0
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/config.js +10 -1
- package/dist/db.js +1 -2
- package/dist/export.js +40 -25
- package/dist/format-utils.js +5 -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/types.js +0 -1
- package/dist/public/assets/index-BZ65TU_Y.js +40 -0
- package/dist/public/assets/index-CqCaW2cb.css +1 -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 +204 -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 +1178 -0
- package/dist/render/templates/bauhaus/portfolio.liquid +179 -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 +1641 -0
- package/dist/render/templates/blueprint/portfolio.liquid +167 -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 +1285 -0
- package/dist/render/templates/canvas/portfolio.liquid +215 -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 +1436 -0
- package/dist/render/templates/carbon/portfolio.liquid +170 -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 +1091 -0
- package/dist/render/templates/chalk/portfolio.liquid +199 -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 +1157 -0
- package/dist/render/templates/circuit/portfolio.liquid +162 -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 +1403 -0
- package/dist/render/templates/cosmos/portfolio.liquid +232 -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 +1151 -0
- package/dist/render/templates/daylight/portfolio.liquid +217 -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 +1311 -0
- package/dist/render/templates/editorial/portfolio.liquid +126 -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 +822 -0
- package/dist/render/templates/ember/portfolio.liquid +318 -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 +1283 -0
- package/dist/render/templates/glacier/portfolio.liquid +271 -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 +1200 -0
- package/dist/render/templates/grid/portfolio.liquid +265 -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 +1441 -0
- package/dist/render/templates/kinetic/portfolio.liquid +170 -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 +944 -0
- package/dist/render/templates/meridian/portfolio.liquid +255 -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 +1369 -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 +525 -0
- package/dist/render/templates/mono/portfolio.liquid +291 -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 +1016 -0
- package/dist/render/templates/neon/portfolio.liquid +217 -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 +1265 -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 +1223 -0
- package/dist/render/templates/obsidian/portfolio.liquid +257 -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 +1401 -0
- package/dist/render/templates/paper/portfolio.liquid +267 -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 +1509 -0
- package/dist/render/templates/parallax/portfolio.liquid +305 -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 +1874 -0
- package/dist/render/templates/parchment/portfolio.liquid +290 -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 +1397 -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 +233 -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 +1049 -0
- package/dist/render/templates/showcase/portfolio.liquid +231 -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 +1279 -0
- package/dist/render/templates/signal/portfolio.liquid +227 -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 +1395 -0
- package/dist/render/templates/strata/portfolio.liquid +192 -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 +1350 -0
- package/dist/render/templates/styles.css +1190 -0
- package/dist/render/templates/terminal/portfolio.liquid +118 -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 +492 -0
- package/dist/render/templates/verdant/portfolio.liquid +333 -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 +1257 -0
- package/dist/render/templates/zen/portfolio.liquid +136 -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 +1207 -0
- package/dist/render/templates.js +90 -0
- package/dist/routes/context.js +15 -10
- package/dist/routes/enhance.js +17 -40
- package/dist/routes/export.js +14 -4
- package/dist/routes/preview.js +480 -108
- package/dist/routes/projects.js +11 -19
- package/dist/routes/publish.js +15 -17
- package/dist/routes/settings.js +94 -1
- package/dist/routes/sse.js +9 -0
- package/dist/server.js +8 -2
- package/dist/settings.js +17 -9
- package/package.json +2 -4
- package/dist/public/assets/index-CC9G8EF1.js +0 -21
- package/dist/public/assets/index-Dalqz2mC.css +0 -1
package/dist/routes/preview.js
CHANGED
|
@@ -3,15 +3,264 @@ import path from 'node:path';
|
|
|
3
3
|
import { readFileSync, existsSync } from 'node:fs';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { getAuthToken } from '../auth.js';
|
|
6
|
-
import { loadEnhancedData, loadProjectEnhanceResult } from '../settings.js';
|
|
6
|
+
import { loadEnhancedData, loadProjectEnhanceResult, getDefaultTemplate, getPortfolioProfile } from '../settings.js';
|
|
7
7
|
import { SCREENSHOTS_DIR } from '../screenshot.js';
|
|
8
|
-
import { renderProjectHtml, renderSessionHtml } from '../render/index.js';
|
|
9
|
-
import {
|
|
8
|
+
import { renderProjectHtml, renderSessionHtml, renderPortfolioHtml } from '../render/index.js';
|
|
9
|
+
import { getTemplateCss, isValidTemplate, getTemplateInfo } from '../render/templates.js';
|
|
10
|
+
import { getMockPortfolioData, getMockProjectData, getMockProjectArc, getMockFullSessions, getMockSessionData } from '../render/mock-data.js';
|
|
11
|
+
import { buildSessionRenderData, buildProjectRenderData } from '../render/build-render-data.js';
|
|
10
12
|
import { buildAgentSummary } from './context.js';
|
|
11
13
|
import { displayNameFromDir } from '../sync.js';
|
|
14
|
+
import { toSlug } from '../format-utils.js';
|
|
15
|
+
import { getSessionsByProject } from '../db.js';
|
|
12
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
/**
|
|
18
|
+
* In-memory cache for expensive buildProjectPreviewData calls.
|
|
19
|
+
* Keyed by project param — the render data is template-agnostic,
|
|
20
|
+
* so we cache once and re-render with different templates cheaply.
|
|
21
|
+
* TTL: 30 seconds (long enough for template browser to load all iframes).
|
|
22
|
+
*/
|
|
23
|
+
const previewDataCache = new Map();
|
|
24
|
+
const PREVIEW_CACHE_TTL = 30_000;
|
|
25
|
+
/** Clear the preview data cache. Exported for testing. */
|
|
26
|
+
export function clearPreviewCache() {
|
|
27
|
+
previewDataCache.clear();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build project render data and enhance result from a project parameter.
|
|
31
|
+
* Shared between the full-page preview and the JSON render endpoint.
|
|
32
|
+
*/
|
|
33
|
+
async function buildProjectPreviewData(ctx, projectParam, queryOverrides) {
|
|
34
|
+
// Check cache (only when no query overrides, which are rare)
|
|
35
|
+
if (!queryOverrides?.repoUrl && !queryOverrides?.projectUrl) {
|
|
36
|
+
const cached = previewDataCache.get(projectParam);
|
|
37
|
+
if (cached && Date.now() - cached.ts < PREVIEW_CACHE_TTL) {
|
|
38
|
+
return cached.data;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const rawProjects = await ctx.getProjects();
|
|
42
|
+
const rawProj = rawProjects.find((p) => p.name === projectParam || p.dirName === projectParam);
|
|
43
|
+
if (!rawProj) {
|
|
44
|
+
throw new ProjectNotFoundError(projectParam);
|
|
45
|
+
}
|
|
46
|
+
const proj = await ctx.getProjectWithStats(rawProj);
|
|
47
|
+
const cached = loadProjectEnhanceResult(proj.dirName);
|
|
48
|
+
const auth = getAuthToken();
|
|
49
|
+
const enhanceResult = cached?.result;
|
|
50
|
+
// ── Fast path: read ALL session data from SQLite (single query, no JSONL parsing) ──
|
|
51
|
+
const dbSessions = getSessionsByProject(ctx.db, rawProj.dirName);
|
|
52
|
+
const dbById = new Map(dbSessions.map((r) => [r.id, r]));
|
|
53
|
+
/** Convert a SQLite SessionRow + optional enhanced data into a SessionCard */
|
|
54
|
+
function rowToCard(row, sid) {
|
|
55
|
+
const enhanced = loadEnhancedData(sid);
|
|
56
|
+
const skills = enhanced?.skills ?? (row.skills ? JSON.parse(row.skills) : []);
|
|
57
|
+
const title = enhanced?.title ?? row.title ?? sid;
|
|
58
|
+
const devTake = (enhanced?.developerTake ?? '').slice(0, 2000);
|
|
59
|
+
const slug = toSlug(title, 80);
|
|
60
|
+
// Check for child sessions (subagents) in DB
|
|
61
|
+
const children = dbSessions.filter((r) => r.parent_session_id === sid);
|
|
62
|
+
let agentSummary;
|
|
63
|
+
if (children.length > 0) {
|
|
64
|
+
agentSummary = {
|
|
65
|
+
is_orchestrated: true,
|
|
66
|
+
agents: children.map((c) => ({
|
|
67
|
+
role: c.agent_role ?? 'agent',
|
|
68
|
+
duration_minutes: c.duration_minutes ?? 0,
|
|
69
|
+
loc_changed: (c.loc_added ?? 0) + (c.loc_removed ?? 0),
|
|
70
|
+
})),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
token: sid,
|
|
75
|
+
slug,
|
|
76
|
+
title,
|
|
77
|
+
devTake,
|
|
78
|
+
durationMinutes: row.duration_minutes ?? 0,
|
|
79
|
+
turns: row.turns ?? 0,
|
|
80
|
+
locChanged: (row.loc_added ?? 0) + (row.loc_removed ?? 0),
|
|
81
|
+
linesAdded: row.loc_added ?? 0,
|
|
82
|
+
linesDeleted: row.loc_removed ?? 0,
|
|
83
|
+
filesChanged: row.files_changed ?? 0,
|
|
84
|
+
skills,
|
|
85
|
+
recordedAt: row.start_time ?? new Date().toISOString(),
|
|
86
|
+
sourceTool: row.source ?? 'claude',
|
|
87
|
+
agentSummary,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// Build selected session cards from DB
|
|
91
|
+
const sessionCards = [];
|
|
92
|
+
if (cached?.selectedSessionIds) {
|
|
93
|
+
for (const sid of cached.selectedSessionIds) {
|
|
94
|
+
const row = dbById.get(sid);
|
|
95
|
+
if (!row)
|
|
96
|
+
continue;
|
|
97
|
+
sessionCards.push(rowToCard(row, sid));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Build ALL session cards from DB (for work timeline + growth chart)
|
|
101
|
+
// Only parent sessions (not subagents) — subagents are included via agentSummary
|
|
102
|
+
const allSessionCards = [];
|
|
103
|
+
const sessionStatsMap = new Map();
|
|
104
|
+
for (const row of dbSessions) {
|
|
105
|
+
if (row.is_subagent)
|
|
106
|
+
continue;
|
|
107
|
+
const enhanced = loadEnhancedData(row.id);
|
|
108
|
+
const skills = enhanced?.skills ?? (row.skills ? JSON.parse(row.skills) : []);
|
|
109
|
+
sessionStatsMap.set(row.id, {
|
|
110
|
+
duration: row.duration_minutes ?? 0,
|
|
111
|
+
date: row.start_time || undefined,
|
|
112
|
+
skills,
|
|
113
|
+
description: enhanced?.context || '',
|
|
114
|
+
});
|
|
115
|
+
allSessionCards.push(rowToCard(row, row.id));
|
|
116
|
+
}
|
|
117
|
+
// Enrich timeline sessions with real stats
|
|
118
|
+
const enrichedTimeline = (enhanceResult?.timeline || []).map((period) => ({
|
|
119
|
+
period: period.period,
|
|
120
|
+
label: period.label,
|
|
121
|
+
sessions: period.sessions.map((s) => {
|
|
122
|
+
const stats = sessionStatsMap.get(s.sessionId);
|
|
123
|
+
return {
|
|
124
|
+
...s,
|
|
125
|
+
duration: stats?.duration ?? 0,
|
|
126
|
+
date: stats?.date,
|
|
127
|
+
skills: stats?.skills,
|
|
128
|
+
description: stats?.description,
|
|
129
|
+
};
|
|
130
|
+
}),
|
|
131
|
+
}));
|
|
132
|
+
const projAny = proj;
|
|
133
|
+
const rawName = projAny.name || displayNameFromDir(projAny.dirName);
|
|
134
|
+
const title = cached?.title || rawName;
|
|
135
|
+
const slug = toSlug(rawName);
|
|
136
|
+
// Metadata from enhance cache (set in sidebar), with query overrides taking priority
|
|
137
|
+
const cachedAny = cached;
|
|
138
|
+
const metaRepoUrl = queryOverrides?.repoUrl || cachedAny?.repoUrl;
|
|
139
|
+
const metaProjectUrl = queryOverrides?.projectUrl || cachedAny?.projectUrl;
|
|
140
|
+
const renderData = buildProjectRenderData({
|
|
141
|
+
username: auth?.username || 'preview',
|
|
142
|
+
slug,
|
|
143
|
+
title,
|
|
144
|
+
narrative: enhanceResult?.narrative || projAny.description || '',
|
|
145
|
+
repoUrl: metaRepoUrl,
|
|
146
|
+
projectUrl: metaProjectUrl,
|
|
147
|
+
screenshotUrl: (() => {
|
|
148
|
+
return existsSync(path.join(SCREENSHOTS_DIR, `${slug}.png`))
|
|
149
|
+
? `/screenshots/${slug}.png`
|
|
150
|
+
: undefined;
|
|
151
|
+
})(),
|
|
152
|
+
timeline: enrichedTimeline,
|
|
153
|
+
skills: enhanceResult?.skills || projAny.skills || [],
|
|
154
|
+
totalSessions: projAny.sessionCount,
|
|
155
|
+
totalLoc: projAny.totalLoc,
|
|
156
|
+
totalDurationMinutes: projAny.totalDuration,
|
|
157
|
+
totalAgentDurationMinutes: projAny.totalAgentDuration,
|
|
158
|
+
totalFilesChanged: projAny.totalFiles,
|
|
159
|
+
totalTokens: (projAny.totalInputTokens || 0) + (projAny.totalOutputTokens || 0) || undefined,
|
|
160
|
+
sessionCards,
|
|
161
|
+
allSessionCards,
|
|
162
|
+
sessionBaseUrl: `/preview/project/${encodeURIComponent(projectParam)}/session`,
|
|
163
|
+
});
|
|
164
|
+
const result = { renderData, enhanceResult, projName: projAny.name };
|
|
165
|
+
// Cache the result (template-agnostic data, re-rendered cheaply per template)
|
|
166
|
+
previewDataCache.set(projectParam, { data: result, ts: Date.now() });
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
/** Sentinel error for project-not-found so callers can return 404. */
|
|
170
|
+
class ProjectNotFoundError extends Error {
|
|
171
|
+
constructor(project) {
|
|
172
|
+
super(`Project not found: ${project}`);
|
|
173
|
+
this.name = 'ProjectNotFoundError';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
13
176
|
export function createPreviewRouter(ctx) {
|
|
14
177
|
const router = Router();
|
|
178
|
+
// Serve template previews with mock data.
|
|
179
|
+
// Tries static mockup HTML first (fast), falls back to Liquid rendering.
|
|
180
|
+
router.get('/preview/template/:name', (req, res) => {
|
|
181
|
+
const name = String(req.params.name);
|
|
182
|
+
if (!isValidTemplate(name)) {
|
|
183
|
+
res.status(404).send('Template not found');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const page = req.query.page || 'project';
|
|
187
|
+
// 1. Try static mockup HTML (instant, from docs/mockups/)
|
|
188
|
+
const mockupPath = path.resolve(__dirname, '..', '..', '..', 'docs', 'mockups', name, `${page}.html`);
|
|
189
|
+
if (existsSync(mockupPath)) {
|
|
190
|
+
let html = readFileSync(mockupPath, 'utf-8');
|
|
191
|
+
html = html.replace(/\.\.\/assets\//g, '/preview/template-assets/');
|
|
192
|
+
html = html.replace(/\.\/portfolio\.html/g, `/preview/template/${name}?page=portfolio`);
|
|
193
|
+
html = html.replace(/\.\/project\.html/g, `/preview/template/${name}?page=project`);
|
|
194
|
+
html = html.replace(/\.\/session\.html/g, `/preview/template/${name}?page=session`);
|
|
195
|
+
res.setHeader('Content-Type', 'text/html');
|
|
196
|
+
res.setHeader('Cache-Control', 'public, max-age=3600');
|
|
197
|
+
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
|
198
|
+
res.send(html);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
// 2. Fall back to Liquid rendering with mock data
|
|
202
|
+
try {
|
|
203
|
+
let bodyHtml;
|
|
204
|
+
if (page === 'portfolio') {
|
|
205
|
+
bodyHtml = renderPortfolioHtml(getMockPortfolioData(), name);
|
|
206
|
+
}
|
|
207
|
+
else if (page === 'session') {
|
|
208
|
+
bodyHtml = renderSessionHtml(getMockSessionData(), name);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
bodyHtml = renderProjectHtml(getMockProjectData(), { arc: getMockProjectArc(), fullSessions: getMockFullSessions() }, name);
|
|
212
|
+
}
|
|
213
|
+
const css = getTemplateCss(name);
|
|
214
|
+
const fullHtml = `<!DOCTYPE html>
|
|
215
|
+
<html lang="en">
|
|
216
|
+
<head>
|
|
217
|
+
<meta charset="UTF-8">
|
|
218
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
219
|
+
<meta name="heyiam-api-base" content="/api" />
|
|
220
|
+
<title>${name} — ${page} preview</title>
|
|
221
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
222
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
223
|
+
<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" />
|
|
224
|
+
<style>${css}
|
|
225
|
+
/* Preview override */
|
|
226
|
+
body { overflow: auto !important; min-height: auto !important; }
|
|
227
|
+
#root { min-height: auto !important; }
|
|
228
|
+
</style>
|
|
229
|
+
</head>
|
|
230
|
+
<body>
|
|
231
|
+
${bodyHtml}
|
|
232
|
+
<script src="/heyiam-mount.js"></script>
|
|
233
|
+
</body>
|
|
234
|
+
</html>`;
|
|
235
|
+
res.setHeader('Content-Type', 'text/html');
|
|
236
|
+
res.setHeader('Cache-Control', 'public, max-age=3600');
|
|
237
|
+
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
|
238
|
+
res.send(fullHtml);
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
console.error(`[preview/template] Liquid render failed for ${name}/${page}:`, err.message);
|
|
242
|
+
res.status(500).send('Template render failed');
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
// Serve mockup assets (headshots, etc.)
|
|
246
|
+
router.get('/preview/template-assets/:filename', (req, res) => {
|
|
247
|
+
const filename = String(req.params.filename);
|
|
248
|
+
// Only allow expected image files
|
|
249
|
+
if (!/^[\w-]+\.(jpg|png)$/.test(filename)) {
|
|
250
|
+
res.status(400).end();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const assetPath = path.resolve(__dirname, '..', '..', '..', 'docs', 'mockups', 'assets', filename);
|
|
254
|
+
if (existsSync(assetPath)) {
|
|
255
|
+
const ext = filename.endsWith('.png') ? 'image/png' : 'image/jpeg';
|
|
256
|
+
res.setHeader('Content-Type', ext);
|
|
257
|
+
res.setHeader('Cache-Control', 'public, max-age=86400');
|
|
258
|
+
res.send(readFileSync(assetPath));
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
res.status(404).end();
|
|
262
|
+
}
|
|
263
|
+
});
|
|
15
264
|
// Serve local screenshot files
|
|
16
265
|
router.get('/screenshots/:slug.png', (req, res) => {
|
|
17
266
|
const filePath = path.join(SCREENSHOTS_DIR, `${req.params.slug}.png`);
|
|
@@ -24,120 +273,94 @@ export function createPreviewRouter(ctx) {
|
|
|
24
273
|
res.status(404).end();
|
|
25
274
|
}
|
|
26
275
|
});
|
|
276
|
+
// Delete a screenshot file
|
|
277
|
+
router.delete('/api/projects/:project/screenshot', (req, res) => {
|
|
278
|
+
const projectParam = String(req.params.project);
|
|
279
|
+
const slug = toSlug(projectParam);
|
|
280
|
+
const filePath = path.join(SCREENSHOTS_DIR, `${slug}.png`);
|
|
281
|
+
try {
|
|
282
|
+
if (existsSync(filePath)) {
|
|
283
|
+
const { unlinkSync } = require('node:fs');
|
|
284
|
+
unlinkSync(filePath);
|
|
285
|
+
}
|
|
286
|
+
res.json({ ok: true });
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
res.status(500).json({ error: 'Failed to delete screenshot' });
|
|
290
|
+
}
|
|
291
|
+
});
|
|
27
292
|
// Project preview -- serves full standalone HTML page identical to heyi.am
|
|
28
293
|
router.get('/preview/project/:project', async (req, res) => {
|
|
29
294
|
try {
|
|
30
295
|
const projectParam = String(req.params.project);
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
296
|
+
const templateOverride = req.query.template;
|
|
297
|
+
const { renderData, enhanceResult, projName } = await buildProjectPreviewData(ctx, projectParam, {
|
|
298
|
+
repoUrl: req.query.repoUrl,
|
|
299
|
+
projectUrl: req.query.projectUrl,
|
|
300
|
+
});
|
|
301
|
+
// Use template override if valid, otherwise fall back to user default
|
|
302
|
+
const templateName = (templateOverride && isValidTemplate(templateOverride))
|
|
303
|
+
? templateOverride
|
|
304
|
+
: (getDefaultTemplate() || 'editorial');
|
|
305
|
+
let bodyHtml;
|
|
306
|
+
try {
|
|
307
|
+
bodyHtml = renderProjectHtml(renderData, { arc: enhanceResult?.arc }, templateName);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
bodyHtml = renderProjectHtml(renderData, { arc: enhanceResult?.arc }, 'editorial');
|
|
311
|
+
}
|
|
312
|
+
res.type('html').send(ctx.buildPreviewPage(projName, bodyHtml, 'PREVIEW — this is how your project will appear on heyi.am', templateName));
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
if (err instanceof ProjectNotFoundError) {
|
|
34
316
|
res.status(404).send('Project not found');
|
|
35
317
|
return;
|
|
36
318
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}));
|
|
61
|
-
}
|
|
62
|
-
catch { /* skip sessions that fail to parse */ }
|
|
63
|
-
}
|
|
319
|
+
console.error('[preview] Error:', err.message);
|
|
320
|
+
res.status(500).send('Preview rendering failed');
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
// JSON render endpoint -- returns rendered HTML fragment + CSS for embedding in React UI
|
|
324
|
+
router.get('/api/projects/:project/render', async (req, res) => {
|
|
325
|
+
try {
|
|
326
|
+
const projectParam = String(req.params.project);
|
|
327
|
+
const templateOverride = req.query.template;
|
|
328
|
+
const { renderData, enhanceResult } = await buildProjectPreviewData(ctx, projectParam, {
|
|
329
|
+
repoUrl: req.query.repoUrl,
|
|
330
|
+
projectUrl: req.query.projectUrl,
|
|
331
|
+
});
|
|
332
|
+
// Override sessionBaseUrl so Liquid generates SPA-friendly /session/:id links
|
|
333
|
+
// (the cached renderData uses /preview/project/... URLs for the standalone preview)
|
|
334
|
+
const spaRenderData = { ...renderData, sessionBaseUrl: '/session' };
|
|
335
|
+
// Use template override if valid, otherwise fall back to user default
|
|
336
|
+
let templateName = (templateOverride && isValidTemplate(templateOverride))
|
|
337
|
+
? templateOverride
|
|
338
|
+
: (getDefaultTemplate() || 'editorial');
|
|
339
|
+
let html;
|
|
340
|
+
try {
|
|
341
|
+
html = renderProjectHtml(spaRenderData, { arc: enhanceResult?.arc }, templateName);
|
|
64
342
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
description: enhanced?.context || '',
|
|
78
|
-
});
|
|
79
|
-
const allAgentSummary = await buildAgentSummary(meta.children ?? [], (c) => ctx.getSessionStats(c, rawProj.name));
|
|
80
|
-
allSessionCards.push(buildSessionCard({
|
|
81
|
-
sessionId: meta.sessionId,
|
|
82
|
-
session: s,
|
|
83
|
-
enhanced,
|
|
84
|
-
username: auth?.username || 'preview',
|
|
85
|
-
projectSlug: proj.dirName,
|
|
86
|
-
sessionSlug: meta.sessionId,
|
|
87
|
-
sourceTool: s.source || 'claude',
|
|
88
|
-
agentSummary: allAgentSummary,
|
|
89
|
-
}));
|
|
90
|
-
}
|
|
91
|
-
catch { /* skip */ }
|
|
92
|
-
}
|
|
93
|
-
// Enrich timeline sessions with real stats
|
|
94
|
-
const enrichedTimeline = (enhanceResult?.timeline || []).map((period) => ({
|
|
95
|
-
period: period.period,
|
|
96
|
-
label: period.label,
|
|
97
|
-
sessions: period.sessions.map((s) => {
|
|
98
|
-
const stats = sessionStatsMap.get(s.sessionId);
|
|
99
|
-
return {
|
|
100
|
-
...s,
|
|
101
|
-
duration: stats?.duration ?? 0,
|
|
102
|
-
date: stats?.date,
|
|
103
|
-
skills: stats?.skills,
|
|
104
|
-
description: stats?.description,
|
|
105
|
-
};
|
|
106
|
-
}),
|
|
107
|
-
}));
|
|
108
|
-
const projAny = proj;
|
|
109
|
-
const name = projAny.name || displayNameFromDir(projAny.dirName);
|
|
110
|
-
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
111
|
-
const renderData = buildProjectRenderData({
|
|
112
|
-
username: auth?.username || 'preview',
|
|
113
|
-
slug,
|
|
114
|
-
title: name,
|
|
115
|
-
narrative: enhanceResult?.narrative || projAny.description || '',
|
|
116
|
-
repoUrl: req.query.repoUrl || undefined,
|
|
117
|
-
projectUrl: req.query.projectUrl || undefined,
|
|
118
|
-
screenshotUrl: (() => {
|
|
119
|
-
return existsSync(path.join(SCREENSHOTS_DIR, `${slug}.png`))
|
|
120
|
-
? `/screenshots/${slug}.png`
|
|
121
|
-
: undefined;
|
|
122
|
-
})(),
|
|
123
|
-
timeline: enrichedTimeline,
|
|
124
|
-
skills: enhanceResult?.skills || projAny.skills || [],
|
|
125
|
-
totalSessions: projAny.sessionCount,
|
|
126
|
-
totalLoc: projAny.totalLoc,
|
|
127
|
-
totalDurationMinutes: projAny.totalDuration,
|
|
128
|
-
totalAgentDurationMinutes: projAny.totalAgentDuration,
|
|
129
|
-
totalFilesChanged: projAny.totalFiles,
|
|
130
|
-
totalTokens: (projAny.totalInputTokens || 0) + (projAny.totalOutputTokens || 0) || undefined,
|
|
131
|
-
sessionCards,
|
|
132
|
-
allSessionCards,
|
|
133
|
-
sessionBaseUrl: `/preview/project/${encodeURIComponent(projectParam)}/session`,
|
|
343
|
+
catch {
|
|
344
|
+
// Template files may not exist yet (e.g. showcase) -- fall back to editorial
|
|
345
|
+
templateName = 'editorial';
|
|
346
|
+
html = renderProjectHtml(spaRenderData, { arc: enhanceResult?.arc }, templateName);
|
|
347
|
+
}
|
|
348
|
+
const css = getTemplateCss(templateName);
|
|
349
|
+
const screenshotUrl = spaRenderData.project.screenshotUrl || undefined;
|
|
350
|
+
const templateInfo = getTemplateInfo(templateName);
|
|
351
|
+
res.json({
|
|
352
|
+
html, css, template: templateName, screenshotUrl,
|
|
353
|
+
accent: templateInfo?.accent ?? '#084471',
|
|
354
|
+
mode: templateInfo?.mode ?? 'light',
|
|
134
355
|
});
|
|
135
|
-
const bodyHtml = renderProjectHtml(renderData);
|
|
136
|
-
res.type('html').send(ctx.buildPreviewPage(projAny.name, bodyHtml, 'PREVIEW — this is how your project will appear on heyi.am'));
|
|
137
356
|
}
|
|
138
357
|
catch (err) {
|
|
139
|
-
|
|
140
|
-
|
|
358
|
+
if (err instanceof ProjectNotFoundError) {
|
|
359
|
+
res.status(404).json({ error: 'Project not found' });
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
console.error('[api/render] Error:', err.message);
|
|
363
|
+
res.status(500).json({ error: 'Render failed' });
|
|
141
364
|
}
|
|
142
365
|
});
|
|
143
366
|
// Session preview
|
|
@@ -160,6 +383,9 @@ export function createPreviewRouter(ctx) {
|
|
|
160
383
|
const session = await ctx.loadSession(meta.path, rawProj.name, sessionId);
|
|
161
384
|
const enhanced = loadEnhancedData(sessionId);
|
|
162
385
|
const agentSummary = await buildAgentSummary(meta.children ?? [], (c) => ctx.getSessionStats(c, rawProj.name));
|
|
386
|
+
const templateName = req.query.template && isValidTemplate(req.query.template)
|
|
387
|
+
? req.query.template
|
|
388
|
+
: (getDefaultTemplate() || 'editorial');
|
|
163
389
|
const renderData = buildSessionRenderData({
|
|
164
390
|
sessionId,
|
|
165
391
|
session,
|
|
@@ -169,15 +395,161 @@ export function createPreviewRouter(ctx) {
|
|
|
169
395
|
sessionSlug: sessionId,
|
|
170
396
|
sourceTool: session.source || 'claude',
|
|
171
397
|
agentSummary,
|
|
398
|
+
template: templateName,
|
|
172
399
|
});
|
|
173
|
-
const bodyHtml = renderSessionHtml(renderData);
|
|
174
|
-
res.type('html').send(ctx.buildPreviewPage(session.title || sessionId, bodyHtml, 'PREVIEW — this is how your session will appear on heyi.am'));
|
|
400
|
+
const bodyHtml = renderSessionHtml(renderData, templateName);
|
|
401
|
+
res.type('html').send(ctx.buildPreviewPage(session.title || sessionId, bodyHtml, 'PREVIEW — this is how your session will appear on heyi.am', templateName));
|
|
175
402
|
}
|
|
176
403
|
catch (err) {
|
|
177
404
|
console.error('[session-preview] Error:', err.message);
|
|
178
405
|
res.status(500).send('Session preview failed');
|
|
179
406
|
}
|
|
180
407
|
});
|
|
408
|
+
// JSON render endpoint for sessions — returns HTML fragment + CSS for embedding in React UI
|
|
409
|
+
router.get('/api/sessions/:sessionId/render', async (req, res) => {
|
|
410
|
+
try {
|
|
411
|
+
const sessionId = String(req.params.sessionId);
|
|
412
|
+
const templateOverride = req.query.template;
|
|
413
|
+
// Find the session across all projects
|
|
414
|
+
const rawProjects = await ctx.getProjects();
|
|
415
|
+
let foundMeta;
|
|
416
|
+
let foundProj;
|
|
417
|
+
for (const proj of rawProjects) {
|
|
418
|
+
const meta = proj.sessions.find((s) => s.sessionId === sessionId);
|
|
419
|
+
if (meta) {
|
|
420
|
+
foundMeta = meta;
|
|
421
|
+
foundProj = proj;
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (!foundMeta || !foundProj) {
|
|
426
|
+
res.status(404).json({ error: 'Session not found' });
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const auth = getAuthToken();
|
|
430
|
+
const session = await ctx.loadSession(foundMeta.path, foundProj.name, sessionId);
|
|
431
|
+
const enhanced = loadEnhancedData(sessionId);
|
|
432
|
+
const agentSummary = await buildAgentSummary(foundMeta.children ?? [], (c) => ctx.getSessionStats(c, foundProj.name));
|
|
433
|
+
let templateName = (templateOverride && isValidTemplate(templateOverride))
|
|
434
|
+
? templateOverride
|
|
435
|
+
: (getDefaultTemplate() || 'editorial');
|
|
436
|
+
const renderData = buildSessionRenderData({
|
|
437
|
+
sessionId,
|
|
438
|
+
session,
|
|
439
|
+
enhanced,
|
|
440
|
+
username: auth?.username || 'preview',
|
|
441
|
+
projectSlug: foundProj.dirName,
|
|
442
|
+
sessionSlug: sessionId,
|
|
443
|
+
sourceTool: session.source || 'claude',
|
|
444
|
+
agentSummary,
|
|
445
|
+
template: templateName,
|
|
446
|
+
});
|
|
447
|
+
let html;
|
|
448
|
+
try {
|
|
449
|
+
html = renderSessionHtml(renderData, templateName);
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
templateName = 'editorial';
|
|
453
|
+
html = renderSessionHtml(renderData, templateName);
|
|
454
|
+
}
|
|
455
|
+
const css = getTemplateCss(templateName);
|
|
456
|
+
const templateInfo = getTemplateInfo(templateName);
|
|
457
|
+
res.json({
|
|
458
|
+
html, css, template: templateName,
|
|
459
|
+
accent: templateInfo?.accent ?? '#084471',
|
|
460
|
+
mode: templateInfo?.mode ?? 'light',
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
console.error('[api/session/render] Error:', err.message);
|
|
465
|
+
res.status(500).json({ error: 'Session render failed' });
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
// Portfolio preview -- serves full standalone HTML page with real user data
|
|
469
|
+
router.get('/preview/portfolio', async (_req, res) => {
|
|
470
|
+
try {
|
|
471
|
+
const profile = getPortfolioProfile();
|
|
472
|
+
const auth = getAuthToken();
|
|
473
|
+
const templateName = getDefaultTemplate() || 'editorial';
|
|
474
|
+
// Build portfolio projects from real project data
|
|
475
|
+
const rawProjects = await ctx.getProjects();
|
|
476
|
+
const portfolioProjects = [];
|
|
477
|
+
let totalDuration = 0;
|
|
478
|
+
let totalAgentDuration = 0;
|
|
479
|
+
let totalLoc = 0;
|
|
480
|
+
let totalSessions = 0;
|
|
481
|
+
for (const rawProj of rawProjects) {
|
|
482
|
+
try {
|
|
483
|
+
const proj = await ctx.getProjectWithStats(rawProj);
|
|
484
|
+
const cached = loadProjectEnhanceResult(rawProj.dirName);
|
|
485
|
+
const projDuration = proj.totalDuration || 0;
|
|
486
|
+
const projAgentDuration = proj.totalAgentDuration || 0;
|
|
487
|
+
const projLoc = proj.totalLoc || 0;
|
|
488
|
+
const projSessions = proj.sessionCount || 0;
|
|
489
|
+
totalDuration += projDuration;
|
|
490
|
+
totalAgentDuration += projAgentDuration;
|
|
491
|
+
totalLoc += projLoc;
|
|
492
|
+
totalSessions += projSessions;
|
|
493
|
+
const title = cached?.title
|
|
494
|
+
|| proj.name || displayNameFromDir(rawProj.dirName);
|
|
495
|
+
// Session activity for charts
|
|
496
|
+
const dbSessions = getSessionsByProject(ctx.db, rawProj.dirName);
|
|
497
|
+
const sessionActivity = dbSessions
|
|
498
|
+
.filter(s => !s.is_subagent)
|
|
499
|
+
.map(s => ({
|
|
500
|
+
date: s.start_time || '',
|
|
501
|
+
loc: (s.loc_added || 0) + (s.loc_removed || 0),
|
|
502
|
+
durationMinutes: s.duration_minutes || 0,
|
|
503
|
+
}));
|
|
504
|
+
portfolioProjects.push({
|
|
505
|
+
slug: toSlug(rawProj.dirName),
|
|
506
|
+
title,
|
|
507
|
+
narrative: cached?.result?.narrative || proj.description || '',
|
|
508
|
+
totalSessions: projSessions,
|
|
509
|
+
totalLoc: projLoc,
|
|
510
|
+
totalDurationMinutes: projDuration,
|
|
511
|
+
totalAgentDurationMinutes: projAgentDuration,
|
|
512
|
+
totalFilesChanged: proj.totalFiles || 0,
|
|
513
|
+
skills: cached?.result?.skills || proj.skills || [],
|
|
514
|
+
publishedCount: 0,
|
|
515
|
+
sessions: sessionActivity,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
catch { /* skip projects that fail */ }
|
|
519
|
+
}
|
|
520
|
+
// Always use real project data; fall back gracefully for missing profile fields
|
|
521
|
+
const username = auth?.username || 'preview';
|
|
522
|
+
const renderData = {
|
|
523
|
+
user: {
|
|
524
|
+
username,
|
|
525
|
+
accent: '#084471',
|
|
526
|
+
displayName: profile.displayName || '',
|
|
527
|
+
bio: profile.bio || '',
|
|
528
|
+
location: profile.location || '',
|
|
529
|
+
status: 'active',
|
|
530
|
+
email: profile.email,
|
|
531
|
+
phone: profile.phone,
|
|
532
|
+
photoUrl: profile.photoBase64 || undefined,
|
|
533
|
+
linkedinUrl: profile.linkedinUrl,
|
|
534
|
+
githubUrl: profile.githubUrl,
|
|
535
|
+
twitterHandle: profile.twitterHandle,
|
|
536
|
+
websiteUrl: profile.websiteUrl,
|
|
537
|
+
resumeUrl: profile.resumeBase64 ? '#' : undefined,
|
|
538
|
+
},
|
|
539
|
+
projects: portfolioProjects,
|
|
540
|
+
totalDurationMinutes: totalDuration,
|
|
541
|
+
totalAgentDurationMinutes: totalAgentDuration || undefined,
|
|
542
|
+
totalLoc,
|
|
543
|
+
totalSessions,
|
|
544
|
+
};
|
|
545
|
+
const bodyHtml = renderPortfolioHtml(renderData, templateName);
|
|
546
|
+
res.type('html').send(ctx.buildPreviewPage(renderData.user.displayName ? `${renderData.user.displayName}'s Portfolio` : 'Portfolio Preview', bodyHtml, 'PREVIEW — this is how your portfolio will appear on heyi.am', templateName));
|
|
547
|
+
}
|
|
548
|
+
catch (err) {
|
|
549
|
+
console.error('[portfolio-preview] Error:', err.message);
|
|
550
|
+
res.status(500).send('Portfolio preview failed');
|
|
551
|
+
}
|
|
552
|
+
});
|
|
181
553
|
// Serve @heyiam/ui mount script for preview pages
|
|
182
554
|
router.get('/heyiam-mount.js', (_req, res) => {
|
|
183
555
|
// In built dist: dist/mount.js (copied during build)
|