heyiam 0.3.7 → 0.3.9
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/dist/archive.js +28 -0
- package/dist/db.js +9 -0
- package/dist/export.js +3 -0
- package/dist/mount.js +43 -18
- package/dist/public/assets/index-LQHTU1Wz.js +37 -0
- package/dist/public/index.html +1 -1
- package/dist/render/build-render-data.js +1 -0
- package/dist/render/liquid.js +14 -1
- package/dist/render/templates/bauhaus/project.liquid +2 -2
- package/dist/render/templates/editorial/project.liquid +1 -1
- package/dist/render/templates/glacier/project.liquid +3 -3
- package/dist/render/templates/kinetic/project.liquid +1 -1
- package/dist/render/templates/minimal/project.liquid +1 -1
- package/dist/render/templates/paper/project.liquid +2 -2
- package/dist/render/templates/partials/_work-timeline.liquid +1 -1
- package/dist/render/templates/project.liquid +1 -1
- package/dist/render/templates/radar/project.liquid +1 -1
- package/dist/render/templates/showcase/project.liquid +1 -1
- package/dist/render/templates/terminal/project.liquid +1 -1
- package/dist/routes/context.js +26 -3
- package/dist/routes/enhance.js +10 -3
- package/dist/routes/preview.js +16 -0
- package/dist/routes/projects.js +8 -1
- package/dist/routes/publish.js +7 -1
- package/dist/settings.js +2 -0
- package/package.json +1 -1
- package/dist/public/assets/index-BDh4ne9u.js +0 -37
package/dist/public/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>app</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-LQHTU1Wz.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-ByoBtx7P.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/dist/render/liquid.js
CHANGED
|
@@ -110,7 +110,20 @@ export function renderProject(data, extras, templateName) {
|
|
|
110
110
|
const slug = typeof existingSlug === 'string' && existingSlug !== ''
|
|
111
111
|
? existingSlug
|
|
112
112
|
: (slugByToken.get(id) ?? '');
|
|
113
|
-
|
|
113
|
+
const merged = { ...rest, slug, rawLog: [] };
|
|
114
|
+
// When dates are hidden, strip every timestamp from the session so
|
|
115
|
+
// nothing leaks through the page source. The chart and overlay treat
|
|
116
|
+
// a missing `date` as "ordinal mode" — no axis labels, no gap markers.
|
|
117
|
+
if (data.hideSessionDates) {
|
|
118
|
+
delete merged.date;
|
|
119
|
+
delete merged.endTime;
|
|
120
|
+
const children = merged.children;
|
|
121
|
+
if (Array.isArray(children)) {
|
|
122
|
+
for (const c of children)
|
|
123
|
+
delete c.date;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return merged;
|
|
114
127
|
});
|
|
115
128
|
// Encode JSON safe for single-quoted HTML attributes
|
|
116
129
|
const sessionsJson = JSON.stringify(chartSessions).replace(/'/g, ''');
|
|
@@ -218,7 +218,7 @@
|
|
|
218
218
|
<tr>
|
|
219
219
|
<th scope="col">#</th>
|
|
220
220
|
<th scope="col">Session</th>
|
|
221
|
-
<th scope="col">Date</th>
|
|
221
|
+
{% unless hideSessionDates %}<th scope="col">Date</th>{% endunless %}
|
|
222
222
|
<th scope="col">Duration</th>
|
|
223
223
|
<th scope="col">LOC</th>
|
|
224
224
|
<th scope="col">Source</th>
|
|
@@ -230,7 +230,7 @@
|
|
|
230
230
|
<tr>
|
|
231
231
|
<td><span class="session-num">{{ forloop.index }}</span></td>
|
|
232
232
|
<td><a href="{{ sessionBaseUrl }}/{{ s.slug }}{{ sessionSuffix }}" class="session-title-link">{{ s.title }}</a></td>
|
|
233
|
-
<td>{{ s.recordedAt | formatDateShort }}</td>
|
|
233
|
+
{% unless hideSessionDates %}<td>{{ s.recordedAt | formatDateShort }}</td>{% endunless %}
|
|
234
234
|
<td>{{ s.durationMinutes | formatDuration }}</td>
|
|
235
235
|
<td>{{ s.locChanged | localeNumber }}</td>
|
|
236
236
|
<td>{% if s.sourceTool %}<span class="session-source-badge badge-{{ s.sourceTool | downcase | replace: ' ', '-' }}">{{ s.sourceTool }}</span>{% endif %}</td>
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
{% if sessionsJson %}
|
|
91
91
|
<section class="ed-card-section" aria-label="Agent work timeline">
|
|
92
92
|
<h2 class="ed-section-title">Work Timeline</h2>
|
|
93
|
-
<div data-work-timeline data-sessions='{{ sessionsJson | raw }}'></div>
|
|
93
|
+
<div data-work-timeline data-sessions='{{ sessionsJson | raw }}'{% if hideSessionDates %} data-hide-dates="1"{% endif %}></div>
|
|
94
94
|
</section>
|
|
95
95
|
{% endif %}
|
|
96
96
|
|
|
@@ -131,7 +131,7 @@
|
|
|
131
131
|
{% assign labelY = barY | minus: 7 %}
|
|
132
132
|
<rect x="{{ barX }}" y="{{ barY }}" width="{{ barWidth }}" height="{{ barH }}" class="chart-bar" aria-label="{{ s.title }}: {{ s.locChanged }} LOC"/>
|
|
133
133
|
<text x="{{ labelX }}" y="{{ labelY }}" class="chart-value" text-anchor="middle">{{ s.locChanged }}</text>
|
|
134
|
-
<text x="{{ labelX }}" y="180" class="chart-label" text-anchor="middle">{{ s.recordedAt | formatDateShort }}</text>
|
|
134
|
+
<text x="{{ labelX }}" y="180" class="chart-label" text-anchor="middle">{% if hideSessionDates %}#{{ forloop.index }}{% else %}{{ s.recordedAt | formatDateShort }}{% endif %}</text>
|
|
135
135
|
{% endfor %}
|
|
136
136
|
</svg>
|
|
137
137
|
</div>
|
|
@@ -223,7 +223,7 @@
|
|
|
223
223
|
<tr>
|
|
224
224
|
<th scope="col">#</th>
|
|
225
225
|
<th scope="col">Title</th>
|
|
226
|
-
<th scope="col">Date</th>
|
|
226
|
+
{% unless hideSessionDates %}<th scope="col">Date</th>{% endunless %}
|
|
227
227
|
<th scope="col">Duration</th>
|
|
228
228
|
<th scope="col">LOC</th>
|
|
229
229
|
<th scope="col">Source</th>
|
|
@@ -235,7 +235,7 @@
|
|
|
235
235
|
<tr>
|
|
236
236
|
<td>{{ forloop.index }}</td>
|
|
237
237
|
<td><a href="{{ sessionBaseUrl }}/{{ s.slug }}{{ sessionSuffix }}" class="session-title-link">{{ s.title }}</a></td>
|
|
238
|
-
<td>{{ s.recordedAt | formatDateShort }}</td>
|
|
238
|
+
{% unless hideSessionDates %}<td>{{ s.recordedAt | formatDateShort }}</td>{% endunless %}
|
|
239
239
|
<td>{{ s.durationMinutes | formatDuration }}</td>
|
|
240
240
|
<td>{{ s.locChanged | localeNumber }}</td>
|
|
241
241
|
<td>{% if s.sourceTool %}<span class="session-source-badge">{{ s.sourceTool }}</span>{% endif %}</td>
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
<div class="section-tag">Agent work</div>
|
|
108
108
|
<h2 class="section-title">Work timeline</h2>
|
|
109
109
|
<div class="timeline-container">
|
|
110
|
-
<div data-work-timeline data-sessions='{{ sessionsJson | raw }}'></div>
|
|
110
|
+
<div data-work-timeline data-sessions='{{ sessionsJson | raw }}'{% if hideSessionDates %} data-hide-dates="1"{% endif %}></div>
|
|
111
111
|
</div>
|
|
112
112
|
</section>
|
|
113
113
|
{% endif %}
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
{% if sessionsJson %}
|
|
56
56
|
<div class="mn-section">
|
|
57
57
|
<h2 class="mn-section-heading">Work Timeline</h2>
|
|
58
|
-
<div data-work-timeline data-sessions='{{ sessionsJson | raw }}'></div>
|
|
58
|
+
<div data-work-timeline data-sessions='{{ sessionsJson | raw }}'{% if hideSessionDates %} data-hide-dates="1"{% endif %}></div>
|
|
59
59
|
</div>
|
|
60
60
|
<hr class="mn-rule" />
|
|
61
61
|
{% endif %}
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
<tr>
|
|
101
101
|
<th scope="col">#</th>
|
|
102
102
|
<th scope="col">Title</th>
|
|
103
|
-
<th scope="col">Date</th>
|
|
103
|
+
{% unless hideSessionDates %}<th scope="col">Date</th>{% endunless %}
|
|
104
104
|
<th scope="col" class="text-right">Duration</th>
|
|
105
105
|
<th scope="col" class="text-right">LOC</th>
|
|
106
106
|
<th scope="col">Source</th>
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
<tr>
|
|
115
115
|
<td>{{ forloop.index }}</td>
|
|
116
116
|
<td>{{ s.title }}</td>
|
|
117
|
-
<td>{{ s.recordedAt | formatDateShort }}</td>
|
|
117
|
+
{% unless hideSessionDates %}<td>{{ s.recordedAt | formatDateShort }}</td>{% endunless %}
|
|
118
118
|
<td class="text-right">{{ s.durationMinutes | formatDuration }}</td>
|
|
119
119
|
<td class="text-right">{{ s.locChanged | localeNumber }}</td>
|
|
120
120
|
<td>{{ s.sourceTool }}</td>
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
<h3 class="section-header__title">Work timeline</h3>
|
|
4
4
|
<span class="section-header__meta">sessions over time</span>
|
|
5
5
|
</div>
|
|
6
|
-
<div data-work-timeline data-sessions='{{ sessionsJson | raw }}'></div>
|
|
6
|
+
<div data-work-timeline data-sessions='{{ sessionsJson | raw }}'{% if hideSessionDates %} data-hide-dates="1"{% endif %}></div>
|
|
7
7
|
</div>
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
<h3 class="section-header__title">Work timeline</h3>
|
|
95
95
|
<span class="section-header__meta">sessions over time</span>
|
|
96
96
|
</div>
|
|
97
|
-
<div data-work-timeline data-sessions='{{ sessionsJson | raw }}'></div>
|
|
97
|
+
<div data-work-timeline data-sessions='{{ sessionsJson | raw }}'{% if hideSessionDates %} data-hide-dates="1"{% endif %}></div>
|
|
98
98
|
</div>
|
|
99
99
|
|
|
100
100
|
{%- comment -%} Growth Chart mount point {%- endcomment -%}
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
<section class="radar-section" aria-label="Work timeline">
|
|
104
104
|
<div class="radar-label">Work Timeline</div>
|
|
105
105
|
<div class="chart-card">
|
|
106
|
-
<div data-work-timeline data-sessions='{{ sessionsJson | raw }}'></div>
|
|
106
|
+
<div data-work-timeline data-sessions='{{ sessionsJson | raw }}'{% if hideSessionDates %} data-hide-dates="1"{% endif %}></div>
|
|
107
107
|
</div>
|
|
108
108
|
</section>
|
|
109
109
|
{% endif %}
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
<h3>Work Timeline</h3>
|
|
91
91
|
<span class="sc-section-label">sessions over time</span>
|
|
92
92
|
</div>
|
|
93
|
-
<div data-work-timeline data-sessions='{{ sessionsJson | raw }}'></div>
|
|
93
|
+
<div data-work-timeline data-sessions='{{ sessionsJson | raw }}'{% if hideSessionDates %} data-hide-dates="1"{% endif %}></div>
|
|
94
94
|
</div>
|
|
95
95
|
</section>
|
|
96
96
|
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
{% if sessionsJson %}
|
|
70
70
|
<div class="term-section">
|
|
71
71
|
<div class="term-comment"># work timeline</div>
|
|
72
|
-
<div data-work-timeline data-sessions='{{ sessionsJson | raw }}'></div>
|
|
72
|
+
<div data-work-timeline data-sessions='{{ sessionsJson | raw }}'{% if hideSessionDates %} data-hide-dates="1"{% endif %}></div>
|
|
73
73
|
</div>
|
|
74
74
|
{% endif %}
|
|
75
75
|
|
package/dist/routes/context.js
CHANGED
|
@@ -9,8 +9,8 @@ import { bridgeToAnalyzer, mergeActiveIntervals, sumIntervalMs } from '../bridge
|
|
|
9
9
|
import { analyzeSession } from '../analyzer.js';
|
|
10
10
|
import { loadEnhancedData, loadProjectEnhanceResult, getUploadedState, } from '../settings.js';
|
|
11
11
|
import { getTemplateCss } from '../render/templates.js';
|
|
12
|
-
import { archiveSessionFiles } from '../archive.js';
|
|
13
|
-
import { getDatabase, openDatabase, getSessionStats as dbGetSessionStats, getSessionCount, getAllSessionMetas, getAllProjectStats, getSessionsByProject, getProjectUuid, } from '../db.js';
|
|
12
|
+
import { archiveSessionFiles, findReadableSessionPath } from '../archive.js';
|
|
13
|
+
import { getDatabase, openDatabase, getSessionStats as dbGetSessionStats, getSessionCount, getAllSessionMetas, getAllProjectStats, getSessionsByProject, getProjectUuid, getSessionRow, updateSessionPath, } from '../db.js';
|
|
14
14
|
import { ensureSessionIndexed, displayNameFromDir } from '../sync.js';
|
|
15
15
|
export { displayNameFromDir };
|
|
16
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -298,11 +298,34 @@ export function createRouteContext(sessionsBasePath, dbPath) {
|
|
|
298
298
|
};
|
|
299
299
|
}
|
|
300
300
|
async function loadSession(sessionPath, projectName, sessionId) {
|
|
301
|
-
const
|
|
301
|
+
const readablePath = await resolveSessionReadPath(sessionPath, sessionId);
|
|
302
|
+
const parsed = await parseSession(readablePath);
|
|
302
303
|
const analyzerInput = bridgeToAnalyzer(parsed, { sessionId, projectName });
|
|
303
304
|
const session = analyzeSession(analyzerInput);
|
|
304
305
|
return mergeEnhancedData(session);
|
|
305
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Returns a path to a readable session file. If the originally-indexed
|
|
309
|
+
* path still exists, returns it unchanged. Otherwise falls back to the
|
|
310
|
+
* archive copy and heals the DB cache so future reads skip the lookup.
|
|
311
|
+
*
|
|
312
|
+
* This compensates for source tools (notably Claude Code's 30-day
|
|
313
|
+
* cleanup) deleting session files after we've indexed them.
|
|
314
|
+
*/
|
|
315
|
+
async function resolveSessionReadPath(originalPath, sessionId) {
|
|
316
|
+
const row = getSessionRow(db, sessionId);
|
|
317
|
+
const projectDir = row?.project_dir;
|
|
318
|
+
if (!projectDir)
|
|
319
|
+
return originalPath;
|
|
320
|
+
const readable = await findReadableSessionPath(originalPath, projectDir);
|
|
321
|
+
if (!readable)
|
|
322
|
+
return originalPath;
|
|
323
|
+
if (readable !== originalPath) {
|
|
324
|
+
updateSessionPath(db, sessionId, readable);
|
|
325
|
+
console.log(`[loadSession] Healed stale path for ${sessionId} → archive copy`);
|
|
326
|
+
}
|
|
327
|
+
return readable;
|
|
328
|
+
}
|
|
306
329
|
// ── getSessionStats ──────────────────────────────────────
|
|
307
330
|
async function getSessionStats(meta, projectName) {
|
|
308
331
|
try {
|
package/dist/routes/enhance.js
CHANGED
|
@@ -5,7 +5,7 @@ import { enhanceProject, refineNarrative } from '../llm/project-enhance.js';
|
|
|
5
5
|
import { getAnthropicApiKey, saveEnhancedData, loadEnhancedData, deleteEnhancedData, loadFreshProjectEnhanceResult, saveProjectEnhanceResult, loadProjectEnhanceResult, buildProjectFingerprint, getUploadedState, } from '../settings.js';
|
|
6
6
|
import { requireProject } from './context.js';
|
|
7
7
|
import { startSSE } from './sse.js';
|
|
8
|
-
import { invalidatePortfolioPreviewCache } from './preview.js';
|
|
8
|
+
import { invalidatePortfolioPreviewCache, invalidateProjectPreviewCache } from './preview.js';
|
|
9
9
|
export function createEnhanceRouter(ctx) {
|
|
10
10
|
const router = Router();
|
|
11
11
|
// Triage endpoint -- AI selects which sessions are worth showcasing (SSE stream)
|
|
@@ -259,7 +259,11 @@ export function createEnhanceRouter(ctx) {
|
|
|
259
259
|
// Save project enhance result explicitly
|
|
260
260
|
router.post('/api/projects/:project/enhance-save', async (req, res) => {
|
|
261
261
|
const project = String(req.params.project);
|
|
262
|
-
|
|
262
|
+
// FIXME(security): screenshotBase64 is accepted with no size cap and no
|
|
263
|
+
// `data:image/(png|jpeg|jpg|webp);base64,...` shape check. Local-only CLI
|
|
264
|
+
// so not a privilege issue today, but worth a regex + ~4 MB cap for
|
|
265
|
+
// defense-in-depth and to keep the cache JSON from ballooning.
|
|
266
|
+
const { selectedSessionIds, result, title, repoUrl, projectUrl, screenshotBase64, hideSessionDates } = req.body;
|
|
263
267
|
if (!Array.isArray(selectedSessionIds) || !result?.narrative) {
|
|
264
268
|
res.status(400).json({ error: { code: 'INVALID_INPUT', message: 'selectedSessionIds and result are required' } });
|
|
265
269
|
return;
|
|
@@ -272,9 +276,12 @@ export function createEnhanceRouter(ctx) {
|
|
|
272
276
|
const proj = await requireProject(ctx, project, res);
|
|
273
277
|
if (!proj)
|
|
274
278
|
return;
|
|
275
|
-
saveProjectEnhanceResult(proj.dirName, selectedSessionIds, result, undefined, { title, repoUrl, projectUrl, screenshotBase64 });
|
|
279
|
+
saveProjectEnhanceResult(proj.dirName, selectedSessionIds, result, undefined, { title, repoUrl, projectUrl, screenshotBase64, hideSessionDates });
|
|
276
280
|
// Project title/narrative/skills appear in portfolio listing — bust cache.
|
|
277
281
|
invalidatePortfolioPreviewCache();
|
|
282
|
+
// Per-project render cache is keyed by the URL param the client passed.
|
|
283
|
+
invalidateProjectPreviewCache(project);
|
|
284
|
+
invalidateProjectPreviewCache(proj.dirName);
|
|
278
285
|
res.json({ saved: true, enhancedAt: new Date().toISOString() });
|
|
279
286
|
}
|
|
280
287
|
catch (err) {
|
package/dist/routes/preview.js
CHANGED
|
@@ -54,6 +54,15 @@ function portfolioCacheKey(templateName) {
|
|
|
54
54
|
export function invalidatePortfolioPreviewCache() {
|
|
55
55
|
portfolioPreviewCache.clear();
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Drop the per-project render-data cache for `projectParam`. Call after any
|
|
59
|
+
* mutation that affects the rendered project page (title, URLs, narrative,
|
|
60
|
+
* screenshot, skills, timeline). Without this, the 30s TTL on
|
|
61
|
+
* previewDataCache masks the change in the React UI.
|
|
62
|
+
*/
|
|
63
|
+
export function invalidateProjectPreviewCache(projectParam) {
|
|
64
|
+
previewDataCache.delete(projectParam);
|
|
65
|
+
}
|
|
57
66
|
/** Test helper: read current cache entry without mutating. */
|
|
58
67
|
export function _getPortfolioPreviewCacheEntry(templateName = 'editorial') {
|
|
59
68
|
return portfolioPreviewCache.get(portfolioCacheKey(templateName));
|
|
@@ -177,6 +186,12 @@ async function buildProjectPreviewData(ctx, projectParam, queryOverrides) {
|
|
|
177
186
|
repoUrl: metaRepoUrl,
|
|
178
187
|
projectUrl: metaProjectUrl,
|
|
179
188
|
screenshotUrl: (() => {
|
|
189
|
+
// Manual uploads land only in the enhance-cache JSON (no disk write),
|
|
190
|
+
// so check the cache first — mirrors export.ts:resolveScreenshotDataUri.
|
|
191
|
+
const b64 = cachedAny?.screenshotBase64;
|
|
192
|
+
if (b64) {
|
|
193
|
+
return b64.startsWith('data:') ? b64 : `data:image/png;base64,${b64}`;
|
|
194
|
+
}
|
|
180
195
|
return existsSync(path.join(SCREENSHOTS_DIR, `${slug}.png`))
|
|
181
196
|
? `/screenshots/${slug}.png`
|
|
182
197
|
: undefined;
|
|
@@ -193,6 +208,7 @@ async function buildProjectPreviewData(ctx, projectParam, queryOverrides) {
|
|
|
193
208
|
allSessionCards,
|
|
194
209
|
sessionBaseUrl: `/preview/project/${encodeURIComponent(projectParam)}/session`,
|
|
195
210
|
sessionSuffix: '.html',
|
|
211
|
+
hideSessionDates: cachedAny?.hideSessionDates,
|
|
196
212
|
});
|
|
197
213
|
const result = { renderData, enhanceResult, projName: projAny.name };
|
|
198
214
|
// Cache the result (template-agnostic data, re-rendered cheaply per template)
|
package/dist/routes/projects.js
CHANGED
|
@@ -217,7 +217,14 @@ export function createProjectsRouter(ctx) {
|
|
|
217
217
|
res.status(400).json({ error: { code: 'NO_CACHE', message: 'Project must be enhanced before managing sessions' } });
|
|
218
218
|
return;
|
|
219
219
|
}
|
|
220
|
-
saveProjectEnhanceResult(proj.dirName, selectedSessionIds, cache.result, undefined, {
|
|
220
|
+
saveProjectEnhanceResult(proj.dirName, selectedSessionIds, cache.result, undefined, {
|
|
221
|
+
title: cache.title,
|
|
222
|
+
repoUrl: cache.repoUrl,
|
|
223
|
+
projectUrl: cache.projectUrl,
|
|
224
|
+
screenshotBase64: cache.screenshotBase64,
|
|
225
|
+
template: cache.template,
|
|
226
|
+
hideSessionDates: cache.hideSessionDates,
|
|
227
|
+
});
|
|
221
228
|
invalidatePortfolioPreviewCache();
|
|
222
229
|
res.json({ ok: true, selectedSessionIds });
|
|
223
230
|
}
|
package/dist/routes/publish.js
CHANGED
|
@@ -55,7 +55,7 @@ export function createPublishRouter(ctx) {
|
|
|
55
55
|
// Render project preview HTML
|
|
56
56
|
router.post('/api/projects/:project/render-preview', async (req, res) => {
|
|
57
57
|
try {
|
|
58
|
-
const { username, slug, title, narrative, repoUrl, projectUrl, screenshotUrl, timeline, skills, totalSessions, totalLoc, totalDurationMinutes, totalAgentDurationMinutes, totalFilesChanged, totalTokens, sessionCards, } = req.body;
|
|
58
|
+
const { username, slug, title, narrative, repoUrl, projectUrl, screenshotUrl, timeline, skills, totalSessions, totalLoc, totalDurationMinutes, totalAgentDurationMinutes, totalFilesChanged, totalTokens, sessionCards, hideSessionDates, } = req.body;
|
|
59
59
|
const renderData = buildProjectRenderData({
|
|
60
60
|
username: username || 'preview',
|
|
61
61
|
slug, title, narrative,
|
|
@@ -69,6 +69,7 @@ export function createPublishRouter(ctx) {
|
|
|
69
69
|
totalFilesChanged: totalFilesChanged || 0,
|
|
70
70
|
totalTokens,
|
|
71
71
|
sessionCards: sessionCards || [],
|
|
72
|
+
hideSessionDates,
|
|
72
73
|
});
|
|
73
74
|
const templateName = getDefaultTemplate() || 'editorial';
|
|
74
75
|
const html = renderProjectHtml(renderData, undefined, templateName);
|
|
@@ -181,6 +182,11 @@ export function createPublishRouter(ctx) {
|
|
|
181
182
|
send({ type: 'screenshot', status: 'capturing' });
|
|
182
183
|
const raw = screenshotBase64.includes(',') ? screenshotBase64.split(',')[1] : screenshotBase64;
|
|
183
184
|
imageBuffer = Buffer.from(raw, 'base64');
|
|
185
|
+
// FIXME(security): ext detection only handles png/jpg, so an SVG
|
|
186
|
+
// (allowed by the file picker's `accept="image/*"`) gets uploaded
|
|
187
|
+
// to S3 with `Content-Type: image/png` while the bytes are SVG.
|
|
188
|
+
// CSP on heyi.am mitigates script execution, but reject SVG here
|
|
189
|
+
// (or convert it) instead of relying on downstream defenses.
|
|
184
190
|
ext = screenshotBase64.startsWith('data:image/jpeg') || screenshotBase64.startsWith('data:image/jpg') ? 'jpg' : 'png';
|
|
185
191
|
}
|
|
186
192
|
else if (projectUrl) {
|
package/dist/settings.js
CHANGED
|
@@ -163,6 +163,8 @@ export function saveProjectEnhanceResult(projectDirName, selectedSessionIds, res
|
|
|
163
163
|
...(extras?.repoUrl ? { repoUrl: extras.repoUrl } : {}),
|
|
164
164
|
...(extras?.projectUrl ? { projectUrl: extras.projectUrl } : {}),
|
|
165
165
|
...(extras?.screenshotBase64 ? { screenshotBase64: extras.screenshotBase64 } : {}),
|
|
166
|
+
...(extras?.template ? { template: extras.template } : {}),
|
|
167
|
+
...(extras?.hideSessionDates ? { hideSessionDates: true } : {}),
|
|
166
168
|
result,
|
|
167
169
|
};
|
|
168
170
|
writeFileSync(projectEnhancePath(projectDirName, configDir), JSON.stringify(cache, null, 2), { mode: 0o600 });
|