heyiam 0.3.4 → 0.3.6
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/export.js +19 -5
- package/dist/mount.js +9 -7
- package/dist/public/assets/{index-Cq04whgG.js → index-7mUuxgqY.js} +3 -3
- package/dist/public/index.html +1 -1
- package/dist/render/build-render-data.js +1 -0
- package/dist/render/liquid.js +12 -2
- package/dist/render/templates/aurora/project.liquid +1 -1
- package/dist/render/templates/bauhaus/project.liquid +1 -1
- package/dist/render/templates/blueprint/project.liquid +1 -1
- package/dist/render/templates/canvas/project.liquid +1 -1
- package/dist/render/templates/carbon/project.liquid +1 -1
- package/dist/render/templates/chalk/project.liquid +1 -1
- package/dist/render/templates/circuit/project.liquid +1 -1
- package/dist/render/templates/cosmos/project.liquid +1 -1
- package/dist/render/templates/daylight/project.liquid +1 -1
- package/dist/render/templates/editorial/project.liquid +1 -1
- package/dist/render/templates/ember/project.liquid +1 -1
- package/dist/render/templates/glacier/project.liquid +1 -1
- package/dist/render/templates/grid/project.liquid +1 -1
- package/dist/render/templates/kinetic/portfolio.liquid +1 -1
- package/dist/render/templates/kinetic/project.liquid +1 -1
- package/dist/render/templates/kinetic/styles.css +15 -9
- package/dist/render/templates/meridian/project.liquid +1 -1
- package/dist/render/templates/minimal/project.liquid +1 -1
- package/dist/render/templates/mono/project.liquid +1 -1
- package/dist/render/templates/neon/project.liquid +1 -1
- package/dist/render/templates/noir/project.liquid +1 -1
- package/dist/render/templates/obsidian/project.liquid +1 -1
- package/dist/render/templates/paper/project.liquid +1 -1
- package/dist/render/templates/parallax/project.liquid +1 -1
- package/dist/render/templates/parchment/project.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/signal/project.liquid +1 -1
- package/dist/render/templates/strata/project.liquid +1 -1
- package/dist/render/templates/terminal/project.liquid +1 -1
- package/dist/render/templates/verdant/project.liquid +1 -1
- package/dist/render/templates/zen/project.liquid +1 -1
- package/dist/routes/portfolio-render-data.js +2 -2
- package/dist/routes/preview.js +18 -2
- package/dist/routes/project-session-upload.js +4 -3
- package/dist/routes/publish.js +118 -16
- package/dist/settings.js +14 -1
- package/package.json +1 -1
package/dist/routes/publish.js
CHANGED
|
@@ -5,7 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
import { getAuthToken } from '../auth.js';
|
|
7
7
|
import { API_URL, PUBLIC_URL, warnIfNonDefaultApiUrl } from '../config.js';
|
|
8
|
-
import { loadEnhancedData, saveUploadedState, getDefaultTemplate, getPortfolioProfile, hashPortfolioProfile, updatePortfolioPublishTarget, getPortfolioPublishState, DEFAULT_PORTFOLIO_TARGET, } from '../settings.js';
|
|
8
|
+
import { loadEnhancedData, saveUploadedState, getDefaultTemplate, getPortfolioProfile, hashPortfolioProfile, updatePortfolioPublishTarget, getPortfolioPublishState, listUploadedProjects, DEFAULT_PORTFOLIO_TARGET, } from '../settings.js';
|
|
9
9
|
import { generatePortfolioHtmlFragment, generateProjectHtmlFragment, generatePortfolioSite, createZipBuffer } from '../export.js';
|
|
10
10
|
import { buildPortfolioRenderData } from './portfolio-render-data.js';
|
|
11
11
|
import { buildProjectDetail } from './context.js';
|
|
@@ -19,6 +19,37 @@ import { displayNameFromDir } from '../sync.js';
|
|
|
19
19
|
import { toSlug } from '../format-utils.js';
|
|
20
20
|
import { getProjectUuid } from '../db.js';
|
|
21
21
|
const IMAGE_KEY_PREFIX = 'images/';
|
|
22
|
+
/**
|
|
23
|
+
* Upload the user's base64-encoded profile photo to S3 via the Phoenix
|
|
24
|
+
* presign endpoints, then save the resulting key on the user record.
|
|
25
|
+
* Returns the public `/_img/:uuid` URL suitable for og:image / <img src>,
|
|
26
|
+
* or `null` if the upload failed (publish still proceeds without a photo).
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* POSTs the user's base64 profile photo to Phoenix, which resizes it into
|
|
30
|
+
* a full (max 1200px) and small (max 600px) variant, uploads both to R2,
|
|
31
|
+
* and persists the two keys on the user record. Returns the full-size
|
|
32
|
+
* URL (for inline <img src>) or `null` on any failure.
|
|
33
|
+
*/
|
|
34
|
+
async function uploadProfilePhoto(photoBase64, auth) {
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch(`${API_URL}/api/portfolio/profile-photo`, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: {
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
Authorization: `Bearer ${auth.token}`,
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify({ photo: photoBase64 }),
|
|
43
|
+
});
|
|
44
|
+
if (!res.ok)
|
|
45
|
+
return null;
|
|
46
|
+
const { full_url } = await res.json();
|
|
47
|
+
return full_url ?? null;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
22
53
|
export function createPublishRouter(ctx) {
|
|
23
54
|
const router = Router();
|
|
24
55
|
// Render project preview HTML
|
|
@@ -351,16 +382,35 @@ export function createPublishRouter(ctx) {
|
|
|
351
382
|
});
|
|
352
383
|
return;
|
|
353
384
|
}
|
|
385
|
+
const send = startSSE(res);
|
|
354
386
|
try {
|
|
387
|
+
send({ type: 'progress', message: 'Preparing portfolio…' });
|
|
355
388
|
const profile = getPortfolioProfile();
|
|
356
389
|
const templateName = getDefaultTemplate() || 'editorial';
|
|
357
|
-
|
|
390
|
+
// If the user has a profile photo, upload it to Phoenix first so the
|
|
391
|
+
// rendered HTML can reference the hosted URL instead of inlining 1.5MB
|
|
392
|
+
// of base64. Phoenix resizes into a full + small variant and stores
|
|
393
|
+
// both in R2. Removals are handled by the upload endpoint below based
|
|
394
|
+
// on the `has_photo` signal — no explicit DELETE needed.
|
|
395
|
+
let photoUrlOverride;
|
|
396
|
+
if (profile.photoBase64) {
|
|
397
|
+
send({ type: 'progress', message: 'Uploading profile photo…' });
|
|
398
|
+
const uploadedUrl = await uploadProfilePhoto(profile.photoBase64, auth);
|
|
399
|
+
if (uploadedUrl)
|
|
400
|
+
photoUrlOverride = uploadedUrl;
|
|
401
|
+
}
|
|
402
|
+
const { renderData, filteredProjects } = await buildPortfolioRenderData(ctx, auth, { photoUrlOverride });
|
|
403
|
+
send({ type: 'progress', message: 'Rendering portfolio HTML…' });
|
|
358
404
|
const renderedHtml = generatePortfolioHtmlFragment(renderData, templateName);
|
|
359
405
|
// Upload individual project pages for every project included in the
|
|
360
406
|
// portfolio. This ensures project detail pages exist on heyi.am even
|
|
361
407
|
// if the user never published them individually.
|
|
362
408
|
const MAX_SLUG_RETRIES = 10;
|
|
409
|
+
const slugMap = new Map();
|
|
410
|
+
send({ type: 'progress', message: `Publishing ${filteredProjects.length} project${filteredProjects.length === 1 ? '' : 's'}…` });
|
|
411
|
+
let projectIndex = 0;
|
|
363
412
|
for (const rawProj of filteredProjects) {
|
|
413
|
+
projectIndex++;
|
|
364
414
|
try {
|
|
365
415
|
const allProjectsList = await ctx.getProjects();
|
|
366
416
|
const projInfo = allProjectsList.find((p) => p.dirName === rawProj.dirName);
|
|
@@ -384,6 +434,7 @@ export function createPublishRouter(ctx) {
|
|
|
384
434
|
|| projRecord.name || displayNameFromDir(rawProj.dirName);
|
|
385
435
|
const baseSlug = toSlug(title);
|
|
386
436
|
const clientProjectId = getProjectUuid(ctx.db, rawProj.dirName);
|
|
437
|
+
send({ type: 'project', project: title, index: projectIndex, total: filteredProjects.length, status: 'creating' });
|
|
387
438
|
const projectHtmlPreview = generateProjectHtmlFragment(rawProj.dirName, cache, detail.sessions, auth.username, {
|
|
388
439
|
totalFilesChanged: projRecord.totalFiles,
|
|
389
440
|
totalAgentDurationMinutes: projRecord.totalAgentDuration,
|
|
@@ -431,7 +482,13 @@ export function createPublishRouter(ctx) {
|
|
|
431
482
|
}
|
|
432
483
|
break;
|
|
433
484
|
}
|
|
434
|
-
if (
|
|
485
|
+
if (projectRes?.ok) {
|
|
486
|
+
const data = await projectRes.json().catch(() => null);
|
|
487
|
+
if (data?.slug && data.slug !== baseSlug) {
|
|
488
|
+
slugMap.set(baseSlug, data.slug);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
435
492
|
const errText = await projectRes?.text().catch(() => '');
|
|
436
493
|
console.warn(`[portfolio-upload] project ${rawProj.dirName} create failed:`, projectRes?.status, errText);
|
|
437
494
|
}
|
|
@@ -463,11 +520,27 @@ export function createPublishRouter(ctx) {
|
|
|
463
520
|
continue;
|
|
464
521
|
}
|
|
465
522
|
const projectData = await projectRes.json();
|
|
523
|
+
if (projectData.slug !== baseSlug) {
|
|
524
|
+
slugMap.set(baseSlug, projectData.slug);
|
|
525
|
+
}
|
|
526
|
+
send({ type: 'project', project: title, index: projectIndex, total: filteredProjects.length, status: 'created' });
|
|
527
|
+
send({ type: 'progress', message: `Uploading ${selectedSessionIds.length} session${selectedSessionIds.length === 1 ? '' : 's'} for ${title}…` });
|
|
466
528
|
const { uploadedSessionCards } = await uploadSelectedSessions(ctx, auth, {
|
|
467
529
|
proj: projInfo,
|
|
468
530
|
projectData,
|
|
469
531
|
selectedSessionIds,
|
|
532
|
+
sessionStatus: 'listed',
|
|
533
|
+
send: (evt) => send({ ...evt, project: title }),
|
|
470
534
|
});
|
|
535
|
+
// Ensure all existing sessions for this project are listed
|
|
536
|
+
await fetch(`${API_URL}/api/sessions/bulk-status`, {
|
|
537
|
+
method: 'PATCH',
|
|
538
|
+
headers: {
|
|
539
|
+
'Content-Type': 'application/json',
|
|
540
|
+
Authorization: `Bearer ${auth.token}`,
|
|
541
|
+
},
|
|
542
|
+
body: JSON.stringify({ project_id: projectData.project_id, status: 'listed' }),
|
|
543
|
+
}).catch((e) => console.warn(`[portfolio-upload] bulk-status failed for ${title}:`, e.message));
|
|
471
544
|
if (uploadedSessionCards.length === 0) {
|
|
472
545
|
continue;
|
|
473
546
|
}
|
|
@@ -515,9 +588,39 @@ export function createPublishRouter(ctx) {
|
|
|
515
588
|
console.warn(`[portfolio-upload] skipping project ${rawProj.dirName}:`, projErr.message);
|
|
516
589
|
}
|
|
517
590
|
}
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
|
|
591
|
+
// Demote sessions on projects that were previously published but are
|
|
592
|
+
// no longer included in the portfolio.
|
|
593
|
+
const includedDirNames = new Set(filteredProjects.map((p) => p.dirName));
|
|
594
|
+
const previouslyUploaded = listUploadedProjects();
|
|
595
|
+
for (const { dirName, state } of previouslyUploaded) {
|
|
596
|
+
if (includedDirNames.has(dirName))
|
|
597
|
+
continue;
|
|
598
|
+
try {
|
|
599
|
+
send({ type: 'progress', message: `Demoting sessions for removed project "${dirName}"…` });
|
|
600
|
+
await fetch(`${API_URL}/api/sessions/bulk-status`, {
|
|
601
|
+
method: 'PATCH',
|
|
602
|
+
headers: {
|
|
603
|
+
'Content-Type': 'application/json',
|
|
604
|
+
Authorization: `Bearer ${auth.token}`,
|
|
605
|
+
},
|
|
606
|
+
body: JSON.stringify({ project_id: state.projectId, status: 'unlisted' }),
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
catch (demoteErr) {
|
|
610
|
+
console.warn(`[portfolio-upload] failed to demote sessions for ${dirName}:`, demoteErr.message);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
// Rewrite project links in the portfolio HTML to match the actual
|
|
614
|
+
// Phoenix-assigned slugs (which may differ due to conflict retries).
|
|
615
|
+
let finalHtml = renderedHtml;
|
|
616
|
+
for (const [originalSlug, actualSlug] of slugMap) {
|
|
617
|
+
const pattern = `/${auth.username}/${originalSlug}"`;
|
|
618
|
+
const replacement = `/${auth.username}/${actualSlug}"`;
|
|
619
|
+
while (finalHtml.includes(pattern)) {
|
|
620
|
+
finalHtml = finalHtml.replace(pattern, replacement);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
send({ type: 'progress', message: 'Publishing landing page…' });
|
|
521
624
|
const phoenixRes = await fetch(`${API_URL}/api/portfolio/upload`, {
|
|
522
625
|
method: 'POST',
|
|
523
626
|
headers: {
|
|
@@ -525,8 +628,9 @@ export function createPublishRouter(ctx) {
|
|
|
525
628
|
Authorization: `Bearer ${auth.token}`,
|
|
526
629
|
},
|
|
527
630
|
body: JSON.stringify({
|
|
528
|
-
html:
|
|
631
|
+
html: finalHtml,
|
|
529
632
|
profile,
|
|
633
|
+
has_photo: Boolean(profile.photoBase64),
|
|
530
634
|
}),
|
|
531
635
|
});
|
|
532
636
|
if (!phoenixRes.ok) {
|
|
@@ -543,9 +647,8 @@ export function createPublishRouter(ctx) {
|
|
|
543
647
|
lastError: errMsg,
|
|
544
648
|
lastErrorAt: new Date().toISOString(),
|
|
545
649
|
});
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
});
|
|
650
|
+
send({ type: 'error', code: 'PORTFOLIO_UPLOAD_FAILED', message: errMsg });
|
|
651
|
+
res.end();
|
|
549
652
|
return;
|
|
550
653
|
}
|
|
551
654
|
const okBody = await phoenixRes.json().catch(() => ({}));
|
|
@@ -561,15 +664,15 @@ export function createPublishRouter(ctx) {
|
|
|
561
664
|
lastError: undefined,
|
|
562
665
|
lastErrorAt: undefined,
|
|
563
666
|
});
|
|
564
|
-
// Published version of the portfolio just changed — drop any cached
|
|
565
|
-
// /preview/portfolio HTML so the next preview reflects reality.
|
|
566
667
|
invalidatePortfolioPreviewCache();
|
|
567
|
-
|
|
668
|
+
send({
|
|
669
|
+
type: 'done',
|
|
568
670
|
ok: true,
|
|
569
671
|
url: publishedUrl ?? `${PUBLIC_URL}/${auth.username}`,
|
|
570
672
|
publishedAt,
|
|
571
673
|
hash,
|
|
572
674
|
});
|
|
675
|
+
res.end();
|
|
573
676
|
}
|
|
574
677
|
catch (err) {
|
|
575
678
|
const errMsg = err.message;
|
|
@@ -581,9 +684,8 @@ export function createPublishRouter(ctx) {
|
|
|
581
684
|
});
|
|
582
685
|
}
|
|
583
686
|
catch { /* don't mask the original error */ }
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
});
|
|
687
|
+
send({ type: 'error', code: 'PORTFOLIO_UPLOAD_FAILED', message: errMsg });
|
|
688
|
+
res.end();
|
|
587
689
|
}
|
|
588
690
|
});
|
|
589
691
|
// Export the portfolio as a downloadable .zip file.
|
package/dist/settings.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
@@ -235,6 +235,19 @@ export function clearUploadedState(projectDirName, configDir) {
|
|
|
235
235
|
if (existsSync(path))
|
|
236
236
|
unlinkSync(path);
|
|
237
237
|
}
|
|
238
|
+
export function listUploadedProjects(configDir) {
|
|
239
|
+
const dir = uploadedDir(configDir);
|
|
240
|
+
if (!existsSync(dir))
|
|
241
|
+
return [];
|
|
242
|
+
return readdirSync(dir)
|
|
243
|
+
.filter((f) => f.endsWith('.json'))
|
|
244
|
+
.map((f) => {
|
|
245
|
+
const dirName = f.replace(/\.json$/, '');
|
|
246
|
+
const state = getUploadedState(dirName, configDir);
|
|
247
|
+
return state ? { dirName, state } : null;
|
|
248
|
+
})
|
|
249
|
+
.filter((x) => x !== null);
|
|
250
|
+
}
|
|
238
251
|
const PORTFOLIO_PUBLISH_FILE = 'portfolio-publish.json';
|
|
239
252
|
const DEFAULT_PORTFOLIO_TARGET = 'heyi.am';
|
|
240
253
|
function portfolioPublishPath(configDir = getDataDir()) {
|