heyiam 0.3.0 → 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/dist/auth.js +29 -3
- package/dist/db.js +1 -1
- package/dist/export.js +84 -2
- package/dist/github.js +381 -0
- package/dist/parsers/index.js +22 -3
- 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/render/templates/aurora/portfolio.liquid +10 -22
- package/dist/render/templates/aurora/project.liquid +1 -1
- package/dist/render/templates/aurora/styles.css +6 -0
- package/dist/render/templates/bauhaus/portfolio.liquid +9 -19
- package/dist/render/templates/bauhaus/styles.css +4 -0
- package/dist/render/templates/blueprint/portfolio.liquid +10 -24
- package/dist/render/templates/blueprint/styles.css +4 -0
- package/dist/render/templates/canvas/portfolio.liquid +17 -29
- package/dist/render/templates/canvas/styles.css +4 -0
- package/dist/render/templates/carbon/portfolio.liquid +9 -19
- package/dist/render/templates/carbon/styles.css +6 -0
- package/dist/render/templates/chalk/portfolio.liquid +9 -19
- package/dist/render/templates/chalk/styles.css +4 -0
- package/dist/render/templates/circuit/portfolio.liquid +10 -20
- package/dist/render/templates/circuit/project.liquid +1 -1
- package/dist/render/templates/circuit/styles.css +6 -0
- package/dist/render/templates/cosmos/portfolio.liquid +10 -20
- package/dist/render/templates/cosmos/project.liquid +1 -1
- package/dist/render/templates/cosmos/styles.css +6 -0
- package/dist/render/templates/daylight/portfolio.liquid +10 -20
- package/dist/render/templates/daylight/project.liquid +1 -1
- package/dist/render/templates/daylight/styles.css +4 -0
- package/dist/render/templates/editorial/portfolio.liquid +11 -27
- package/dist/render/templates/editorial/styles.css +4 -0
- package/dist/render/templates/ember/portfolio.liquid +11 -23
- package/dist/render/templates/ember/project.liquid +1 -1
- package/dist/render/templates/ember/styles.css +6 -0
- package/dist/render/templates/glacier/portfolio.liquid +10 -20
- package/dist/render/templates/glacier/project.liquid +1 -1
- package/dist/render/templates/glacier/styles.css +4 -0
- package/dist/render/templates/grid/portfolio.liquid +9 -19
- package/dist/render/templates/grid/styles.css +4 -0
- package/dist/render/templates/kinetic/portfolio.liquid +10 -22
- package/dist/render/templates/kinetic/project.liquid +1 -1
- package/dist/render/templates/kinetic/styles.css +4 -0
- package/dist/render/templates/meridian/portfolio.liquid +11 -23
- package/dist/render/templates/meridian/styles.css +6 -0
- package/dist/render/templates/minimal/portfolio.liquid +10 -10
- package/dist/render/templates/minimal/styles.css +4 -0
- package/dist/render/templates/mono/portfolio.liquid +9 -19
- package/dist/render/templates/mono/styles.css +6 -0
- package/dist/render/templates/neon/portfolio.liquid +10 -20
- package/dist/render/templates/neon/project.liquid +1 -1
- package/dist/render/templates/neon/styles.css +6 -0
- package/dist/render/templates/noir/portfolio.liquid +5 -5
- package/dist/render/templates/noir/styles.css +6 -0
- package/dist/render/templates/obsidian/portfolio.liquid +9 -19
- package/dist/render/templates/obsidian/styles.css +6 -0
- package/dist/render/templates/paper/portfolio.liquid +9 -19
- package/dist/render/templates/paper/styles.css +4 -0
- package/dist/render/templates/parallax/portfolio.liquid +9 -19
- package/dist/render/templates/parallax/styles.css +6 -0
- package/dist/render/templates/parchment/portfolio.liquid +9 -19
- package/dist/render/templates/parchment/styles.css +4 -0
- package/dist/render/templates/radar/portfolio.liquid +9 -19
- package/dist/render/templates/radar/styles.css +6 -0
- package/dist/render/templates/showcase/portfolio.liquid +9 -19
- package/dist/render/templates/showcase/styles.css +5 -0
- package/dist/render/templates/signal/portfolio.liquid +9 -19
- package/dist/render/templates/signal/styles.css +6 -0
- package/dist/render/templates/strata/portfolio.liquid +10 -22
- package/dist/render/templates/strata/styles.css +4 -0
- package/dist/render/templates/terminal/portfolio.liquid +10 -26
- package/dist/render/templates/terminal/styles.css +5 -0
- package/dist/render/templates/verdant/portfolio.liquid +11 -23
- package/dist/render/templates/verdant/project.liquid +1 -1
- package/dist/render/templates/verdant/styles.css +4 -0
- package/dist/render/templates/zen/portfolio.liquid +10 -22
- package/dist/render/templates/zen/styles.css +4 -0
- package/dist/routes/auth.js +7 -3
- package/dist/routes/context.js +2 -0
- package/dist/routes/delete.js +195 -0
- package/dist/routes/enhance.js +40 -0
- 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 +85 -10
- package/dist/routes/projects.js +50 -5
- package/dist/routes/publish.js +306 -15
- package/dist/routes/settings.js +102 -2
- package/dist/search.js +6 -0
- package/dist/server.js +3 -1
- package/dist/settings.js +95 -0
- package/package.json +2 -1
- package/dist/public/assets/index-BZ65TU_Y.js +0 -40
- package/dist/public/assets/index-CqCaW2cb.css +0 -1
|
@@ -11,56 +11,40 @@
|
|
|
11
11
|
{% if user.photoUrl != blank %}
|
|
12
12
|
<div class="term-photo">
|
|
13
13
|
<div class="term-photo__border">
|
|
14
|
-
<img src="{{ user.photoUrl }}" alt="{{ user.displayName }}" class="term-photo__img portfolio-photo" loading="lazy"
|
|
14
|
+
<img src="{{ user.photoUrl }}" alt="{{ user.displayName }}" class="term-photo__img portfolio-photo" loading="lazy" / data-portfolio-field="photoBase64"{% unless user.photoUrl %} data-portfolio-empty="true"{% endunless %}>
|
|
15
15
|
</div>
|
|
16
16
|
</div>
|
|
17
17
|
{% endif %}
|
|
18
18
|
<div class="term-identity">
|
|
19
19
|
{% if user.displayName != blank %}
|
|
20
|
-
<h1 class="term-heading"># {{ user.displayName }}</h1>
|
|
20
|
+
<h1 class="term-heading" data-portfolio-field="displayName"># {{ user.displayName }}</h1>
|
|
21
21
|
{% endif %}
|
|
22
22
|
{% if user.status != blank %}
|
|
23
23
|
<div class="term-status">[ {{ user.status }} ]</div>
|
|
24
24
|
{% endif %}
|
|
25
25
|
{% if user.bio != blank %}
|
|
26
|
-
<blockquote class="term-blockquote">> {{ user.bio }}</blockquote>
|
|
26
|
+
<blockquote class="term-blockquote" data-portfolio-field="bio">> {{ user.bio }}</blockquote>
|
|
27
27
|
{% endif %}
|
|
28
28
|
</div>
|
|
29
29
|
</div>
|
|
30
30
|
|
|
31
31
|
{%- comment -%} Contact as key=value pairs {%- endcomment -%}
|
|
32
|
-
{% if user.location != blank or user.email != blank or user.phone != blank or user.githubUrl != blank or user.linkedinUrl != blank or user.twitterHandle != blank or user.websiteUrl != blank or user.resumeUrl != blank %}
|
|
33
32
|
<div class="term-section">
|
|
34
33
|
<div class="term-comment"># contact</div>
|
|
35
34
|
<table class="term-table term-table--contact">
|
|
36
|
-
{%
|
|
37
|
-
<tr><td class="term-table__key">
|
|
38
|
-
{% endif %}
|
|
39
|
-
{% if user.
|
|
40
|
-
<tr><td class="term-table__key">
|
|
41
|
-
{% endif %}
|
|
42
|
-
{% if user.
|
|
43
|
-
<tr><td class="term-table__key">phone</td><td class="term-table__eq">=</td><td class="term-table__val"><a href="tel:{{ user.phone }}">{{ user.phone }}</a></td></tr>
|
|
44
|
-
{% endif %}
|
|
45
|
-
{% if user.githubUrl != blank %}
|
|
46
|
-
<tr><td class="term-table__key">github</td><td class="term-table__eq">=</td><td class="term-table__val"><a href="{{ user.githubUrl }}" target="_blank" rel="noopener">{{ user.githubUrl | stripProtocol }}</a></td></tr>
|
|
47
|
-
{% endif %}
|
|
48
|
-
{% if user.linkedinUrl != blank %}
|
|
49
|
-
<tr><td class="term-table__key">linkedin</td><td class="term-table__eq">=</td><td class="term-table__val"><a href="{{ user.linkedinUrl }}" target="_blank" rel="noopener">{{ user.linkedinUrl | stripProtocol }}</a></td></tr>
|
|
50
|
-
{% endif %}
|
|
51
|
-
{% if user.twitterHandle != blank %}
|
|
52
|
-
<tr><td class="term-table__key">twitter</td><td class="term-table__eq">=</td><td class="term-table__val"><a href="https://twitter.com/{{ user.twitterHandle }}" target="_blank" rel="noopener">@{{ user.twitterHandle }}</a></td></tr>
|
|
53
|
-
{% endif %}
|
|
54
|
-
{% if user.websiteUrl != blank %}
|
|
55
|
-
<tr><td class="term-table__key">web</td><td class="term-table__eq">=</td><td class="term-table__val"><a href="{{ user.websiteUrl }}" target="_blank" rel="noopener">{{ user.websiteUrl | stripProtocol }}</a></td></tr>
|
|
56
|
-
{% endif %}
|
|
35
|
+
<tr data-portfolio-field="location"{% unless user.location %} data-portfolio-empty="true"{% endunless %}><td class="term-table__key">location</td><td class="term-table__eq">=</td><td class="term-table__val">{{ user.location }}</td></tr>
|
|
36
|
+
<tr data-portfolio-field="email"{% unless user.email %} data-portfolio-empty="true"{% endunless %}><td class="term-table__key">email</td><td class="term-table__eq">=</td><td class="term-table__val"><a href="{% if user.email %}mailto:{{ user.email }}{% endif %}"><span data-portfolio-text>{{ user.email }}</span></a></td></tr>
|
|
37
|
+
<tr data-portfolio-field="phone"{% unless user.phone %} data-portfolio-empty="true"{% endunless %}><td class="term-table__key">phone</td><td class="term-table__eq">=</td><td class="term-table__val"><a href="{% if user.phone %}tel:{{ user.phone }}{% endif %}"><span data-portfolio-text>{{ user.phone }}</span></a></td></tr>
|
|
38
|
+
<tr data-portfolio-field="githubUrl"{% unless user.githubUrl %} data-portfolio-empty="true"{% endunless %}><td class="term-table__key">github</td><td class="term-table__eq">=</td><td class="term-table__val"><a href="{% if user.githubUrl %}{{ user.githubUrl }}{% endif %}" target="_blank" rel="noopener">{{ user.githubUrl | stripProtocol }}</a></td></tr>
|
|
39
|
+
<tr data-portfolio-field="linkedinUrl"{% unless user.linkedinUrl %} data-portfolio-empty="true"{% endunless %}><td class="term-table__key">linkedin</td><td class="term-table__eq">=</td><td class="term-table__val"><a href="{% if user.linkedinUrl %}{{ user.linkedinUrl }}{% endif %}" target="_blank" rel="noopener">LinkedIn</a></td></tr>
|
|
40
|
+
<tr data-portfolio-field="twitterHandle"{% unless user.twitterHandle %} data-portfolio-empty="true"{% endunless %}><td class="term-table__key">twitter</td><td class="term-table__eq">=</td><td class="term-table__val"><a href="{% if user.twitterHandle %}https://x.com/{{ user.twitterHandle }}{% endif %}" target="_blank" rel="noopener"><span data-portfolio-text>{% if user.twitterHandle %}@{{ user.twitterHandle }}{% endif %}</span></a></td></tr>
|
|
41
|
+
<tr data-portfolio-field="websiteUrl"{% unless user.websiteUrl %} data-portfolio-empty="true"{% endunless %}><td class="term-table__key">web</td><td class="term-table__eq">=</td><td class="term-table__val"><a href="{% if user.websiteUrl %}{{ user.websiteUrl }}{% endif %}" target="_blank" rel="noopener"><span data-portfolio-text>{{ user.websiteUrl | stripProtocol }}</span></a></td></tr>
|
|
57
42
|
{% if user.resumeUrl != blank %}
|
|
58
43
|
<tr><td class="term-table__key">resume</td><td class="term-table__eq">=</td><td class="term-table__val"><a href="{{ user.resumeUrl }}" target="_blank" rel="noopener" download>Resume (PDF)</a></td></tr>
|
|
59
44
|
{% endif %}
|
|
60
45
|
</table>
|
|
61
46
|
</div>
|
|
62
47
|
{% endif %}
|
|
63
|
-
{% endif %}
|
|
64
48
|
|
|
65
49
|
{%- comment -%} Stats as tabular output {%- endcomment -%}
|
|
66
50
|
<div class="term-section">
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
body { background: var(--term-bg); color: var(--term-text); }
|
|
1
2
|
/* ── Terminal template styles ──
|
|
2
3
|
* Green-on-black terminal aesthetic.
|
|
3
4
|
* Monospace font, ASCII elements, command-line feel.
|
|
@@ -490,3 +491,7 @@
|
|
|
490
491
|
flex-wrap: wrap;
|
|
491
492
|
}
|
|
492
493
|
}
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
/* Live-edit empty field hiding */
|
|
497
|
+
[data-portfolio-empty="true"] { display: none; }
|
|
@@ -8,57 +8,45 @@
|
|
|
8
8
|
{%- comment -%} Hero / Profile {%- endcomment -%}
|
|
9
9
|
<section class="vd-section vd-hero" aria-label="Profile">
|
|
10
10
|
{% if user.photoUrl %}
|
|
11
|
-
<img src="{{ user.photoUrl }}" alt="{{ user.displayName }}" class="vd-hero__photo" width="120" height="120">
|
|
11
|
+
<img src="{{ user.photoUrl }}" alt="{{ user.displayName }}" class="vd-hero__photo" width="120" height="120" data-portfolio-field="photoBase64"{% unless user.photoUrl %} data-portfolio-empty="true"{% endunless %}>
|
|
12
12
|
{% elsif user.displayName != blank %}
|
|
13
13
|
<div class="vd-hero__avatar" aria-hidden="true">{{ user.displayName | slice: 0 }}</div>
|
|
14
14
|
{% endif %}
|
|
15
15
|
{% if user.displayName != blank %}
|
|
16
|
-
<h1>{{ user.displayName }}</h1>
|
|
16
|
+
<h1 data-portfolio-field="displayName">{{ user.displayName }}</h1>
|
|
17
17
|
{% endif %}
|
|
18
18
|
{% if user.bio != blank %}
|
|
19
|
-
<p class="vd-hero__bio">{{ user.bio }}</p>
|
|
19
|
+
<p class="vd-hero__bio" data-portfolio-field="bio">{{ user.bio }}</p>
|
|
20
20
|
{% endif %}
|
|
21
21
|
{% if user.location != blank %}
|
|
22
22
|
<p class="vd-hero__location">
|
|
23
23
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg>
|
|
24
|
-
{{ user.location }}
|
|
24
|
+
<span data-portfolio-field="location">{{ user.location }}</span>
|
|
25
25
|
</p>
|
|
26
26
|
{% endif %}
|
|
27
27
|
{% if user.email or user.linkedinUrl or user.githubUrl or user.twitterHandle or user.websiteUrl or user.resumeUrl %}
|
|
28
28
|
<div class="vd-hero__links">
|
|
29
|
-
{% if user.email %}
|
|
30
|
-
<a href="mailto:{{ user.email }}">
|
|
29
|
+
<a href="{% if user.email %}mailto:{{ user.email }}{% endif %}" data-portfolio-field="email"{% unless user.email %} data-portfolio-empty="true"{% endunless %}>
|
|
31
30
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m2 4 10 8 10-8"/></svg>
|
|
32
31
|
{{ user.email }}
|
|
33
32
|
</a>
|
|
34
|
-
{% endif %}
|
|
35
|
-
{% if user.phone %}
|
|
36
|
-
<a href="tel:{{ user.phone }}">
|
|
33
|
+
<a href="{% if user.phone %}tel:{{ user.phone }}{% endif %}" data-portfolio-field="phone"{% unless user.phone %} data-portfolio-empty="true"{% endunless %}>
|
|
37
34
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.67A2 2 0 014.11 2h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L8.09 9.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 16.92z"/></svg>
|
|
38
35
|
{{ user.phone }}
|
|
39
36
|
</a>
|
|
40
|
-
{% endif %}
|
|
41
|
-
{% if user.linkedinUrl %}
|
|
42
|
-
<a href="{{ user.linkedinUrl }}">
|
|
37
|
+
<a href="{% if user.linkedinUrl %}{{ user.linkedinUrl }}{% endif %}" data-portfolio-field="linkedinUrl"{% unless user.linkedinUrl %} data-portfolio-empty="true"{% endunless %}>
|
|
43
38
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20.5 2h-17A1.5 1.5 0 002 3.5v17A1.5 1.5 0 003.5 22h17a1.5 1.5 0 001.5-1.5v-17A1.5 1.5 0 0020.5 2zM8 19H5v-9h3zM6.5 8.25A1.75 1.75 0 118.3 6.5a1.78 1.78 0 01-1.8 1.75zM19 19h-3v-4.74c0-1.42-.6-1.93-1.38-1.93A1.74 1.74 0 0013 14.19V19h-3v-9h2.9v1.3a3.11 3.11 0 012.7-1.4c1.55 0 3.36.86 3.36 3.66z"/></svg>
|
|
44
39
|
LinkedIn
|
|
45
40
|
</a>
|
|
46
|
-
{% endif %}
|
|
47
|
-
{% if user.githubUrl %}
|
|
48
|
-
<a href="{{ user.githubUrl }}">
|
|
41
|
+
<a href="{% if user.githubUrl %}{{ user.githubUrl }}{% endif %}" data-portfolio-field="githubUrl"{% unless user.githubUrl %} data-portfolio-empty="true"{% endunless %}>
|
|
49
42
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
|
|
50
43
|
GitHub
|
|
51
44
|
</a>
|
|
52
|
-
{% endif %}
|
|
53
|
-
{% if user.twitterHandle %}
|
|
54
|
-
<a href="https://x.com/{{ user.twitterHandle }}">
|
|
45
|
+
<a href="{% if user.twitterHandle %}https://x.com/{{ user.twitterHandle }}{% endif %}" data-portfolio-field="twitterHandle"{% unless user.twitterHandle %} data-portfolio-empty="true"{% endunless %}>
|
|
55
46
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
|
|
56
47
|
@{{ user.twitterHandle }}
|
|
57
48
|
</a>
|
|
58
|
-
{% endif %}
|
|
59
|
-
{% if user.websiteUrl %}
|
|
60
|
-
<a href="{{ user.websiteUrl }}">{{ user.websiteUrl | stripProtocol }}</a>
|
|
61
|
-
{% endif %}
|
|
49
|
+
<a href="{% if user.websiteUrl %}{{ user.websiteUrl }}{% endif %}" data-portfolio-field="websiteUrl"{% unless user.websiteUrl %} data-portfolio-empty="true"{% endunless %}>{{ user.websiteUrl | stripProtocol }}</a>
|
|
62
50
|
{% if user.resumeUrl %}
|
|
63
51
|
<a href="{{ user.resumeUrl }}">
|
|
64
52
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><polyline points="9 15 12 18 15 15"/></svg>
|
|
@@ -94,7 +82,7 @@
|
|
|
94
82
|
<div class="vd-stat-card">
|
|
95
83
|
{% assign totalCombined = totalDurationMinutes | plus: totalAgentDurationMinutes %}
|
|
96
84
|
{% if totalCombined > 0 %}
|
|
97
|
-
{% assign humanPct = totalDurationMinutes | times: 100 | divided_by: totalCombined %}
|
|
85
|
+
{% assign humanPct = totalDurationMinutes | times: 100 | divided_by: totalCombined | round %}
|
|
98
86
|
{% assign agentPct = 100 | minus: humanPct %}
|
|
99
87
|
<div style="display: flex; align-items: center; gap: 4px; margin-bottom: 6px;">
|
|
100
88
|
<div style="flex: {{ humanPct }}; height: 6px; border-radius: 3px; background: rgba(0,0,0,0.12);"></div>
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
<div class="vd-stat-card">
|
|
97
97
|
{% assign totalCombined = project.totalDurationMinutes | plus: project.totalAgentDurationMinutes %}
|
|
98
98
|
{% if totalCombined > 0 %}
|
|
99
|
-
{% assign humanPct = project.totalDurationMinutes | times: 100 | divided_by: totalCombined %}
|
|
99
|
+
{% assign humanPct = project.totalDurationMinutes | times: 100 | divided_by: totalCombined | round %}
|
|
100
100
|
{% assign agentPct = 100 | minus: humanPct %}
|
|
101
101
|
<div style="display: flex; align-items: center; gap: 4px; margin-bottom: 6px;">
|
|
102
102
|
<div style="flex: {{ humanPct }}; height: 6px; border-radius: 3px; background: rgba(0,0,0,0.12);"></div>
|
|
@@ -7,38 +7,26 @@
|
|
|
7
7
|
<section class="zen-section zen-fade" aria-label="Profile">
|
|
8
8
|
<div class="zen-header-profile">
|
|
9
9
|
{% if user.photoUrl %}
|
|
10
|
-
<img src="{{ user.photoUrl }}" alt="{{ user.displayName }}" class="zen-header-photo">
|
|
10
|
+
<img src="{{ user.photoUrl }}" alt="{{ user.displayName }}" class="zen-header-photo" data-portfolio-field="photoBase64"{% unless user.photoUrl %} data-portfolio-empty="true"{% endunless %}>
|
|
11
11
|
{% endif %}
|
|
12
12
|
<div class="zen-header-info">
|
|
13
13
|
{% if user.displayName != blank %}
|
|
14
|
-
<h1 class="zen-display zen-header-name">{{ user.displayName }}</h1>
|
|
14
|
+
<h1 class="zen-display zen-header-name" data-portfolio-field="displayName">{{ user.displayName }}</h1>
|
|
15
15
|
{% endif %}
|
|
16
16
|
{% if user.bio != blank %}
|
|
17
|
-
<p class="zen-header-bio">{{ user.bio }}</p>
|
|
17
|
+
<p class="zen-header-bio" data-portfolio-field="bio">{{ user.bio }}</p>
|
|
18
18
|
{% endif %}
|
|
19
19
|
{% if user.location != blank %}
|
|
20
|
-
<p class="zen-header-location">{{ user.location }}</p>
|
|
20
|
+
<p class="zen-header-location" data-portfolio-field="location">{{ user.location }}</p>
|
|
21
21
|
{% endif %}
|
|
22
22
|
{% if user.email != blank or user.linkedinUrl != blank or user.githubUrl != blank or user.twitterHandle != blank or user.websiteUrl != blank %}
|
|
23
23
|
<ul class="zen-header-contact" aria-label="Contact and social links">
|
|
24
|
-
{% if user.email
|
|
25
|
-
<li><a href="
|
|
26
|
-
{% endif %}
|
|
27
|
-
{% if user.
|
|
28
|
-
<li><a href="
|
|
29
|
-
{% endif %}
|
|
30
|
-
{% if user.linkedinUrl != blank %}
|
|
31
|
-
<li><a href="{{ user.linkedinUrl }}"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M20.5 2h-17A1.5 1.5 0 002 3.5v17A1.5 1.5 0 003.5 22h17a1.5 1.5 0 001.5-1.5v-17A1.5 1.5 0 0020.5 2zM8 19H5v-9h3zM6.5 8.25A1.75 1.75 0 118.3 6.5a1.78 1.78 0 01-1.8 1.75zM19 19h-3v-4.74c0-1.42-.6-1.93-1.38-1.93A1.74 1.74 0 0013 14.19V19h-3v-9h2.9v1.3a3.11 3.11 0 012.7-1.4c1.55 0 3.36.86 3.36 3.66z"/></svg>LinkedIn</a></li>
|
|
32
|
-
{% endif %}
|
|
33
|
-
{% if user.githubUrl != blank %}
|
|
34
|
-
<li><a href="{{ user.githubUrl }}"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>GitHub</a></li>
|
|
35
|
-
{% endif %}
|
|
36
|
-
{% if user.twitterHandle != blank %}
|
|
37
|
-
<li><a href="https://x.com/{{ user.twitterHandle }}"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>@{{ user.twitterHandle }}</a></li>
|
|
38
|
-
{% endif %}
|
|
39
|
-
{% if user.websiteUrl != blank %}
|
|
40
|
-
<li><a href="{{ user.websiteUrl }}"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>{{ user.websiteUrl | stripProtocol }}</a></li>
|
|
41
|
-
{% endif %}
|
|
24
|
+
<li data-portfolio-field="email"{% unless user.email %} data-portfolio-empty="true"{% endunless %}><a href="{% if user.email %}mailto:{{ user.email }}{% endif %}"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m2 4 10 8 10-8"/></svg>{{ user.email }}</a></li>
|
|
25
|
+
<li data-portfolio-field="phone"{% unless user.phone %} data-portfolio-empty="true"{% endunless %}><a href="{% if user.phone %}tel:{{ user.phone }}{% endif %}"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/></svg>{{ user.phone }}</a></li>
|
|
26
|
+
<li data-portfolio-field="linkedinUrl"{% unless user.linkedinUrl %} data-portfolio-empty="true"{% endunless %}><a href="{% if user.linkedinUrl %}{{ user.linkedinUrl }}{% endif %}"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M20.5 2h-17A1.5 1.5 0 002 3.5v17A1.5 1.5 0 003.5 22h17a1.5 1.5 0 001.5-1.5v-17A1.5 1.5 0 0020.5 2zM8 19H5v-9h3zM6.5 8.25A1.75 1.75 0 118.3 6.5a1.78 1.78 0 01-1.8 1.75zM19 19h-3v-4.74c0-1.42-.6-1.93-1.38-1.93A1.74 1.74 0 0013 14.19V19h-3v-9h2.9v1.3a3.11 3.11 0 012.7-1.4c1.55 0 3.36.86 3.36 3.66z"/></svg>LinkedIn</a></li>
|
|
27
|
+
<li data-portfolio-field="githubUrl"{% unless user.githubUrl %} data-portfolio-empty="true"{% endunless %}><a href="{% if user.githubUrl %}{{ user.githubUrl }}{% endif %}"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>GitHub</a></li>
|
|
28
|
+
<li data-portfolio-field="twitterHandle"{% unless user.twitterHandle %} data-portfolio-empty="true"{% endunless %}><a href="{% if user.twitterHandle %}https://x.com/{{ user.twitterHandle }}{% endif %}"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>@{{ user.twitterHandle }}</a></li>
|
|
29
|
+
<li data-portfolio-field="websiteUrl"{% unless user.websiteUrl %} data-portfolio-empty="true"{% endunless %}><a href="{% if user.websiteUrl %}{{ user.websiteUrl }}{% endif %}"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>{{ user.websiteUrl | stripProtocol }}</a></li>
|
|
42
30
|
</ul>
|
|
43
31
|
{% endif %}
|
|
44
32
|
{% if user.resumeUrl != blank %}
|
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
|
@@ -427,6 +427,7 @@ export function createRouteContext(sessionsBasePath, dbPath) {
|
|
|
427
427
|
uploadedSessionCount: published?.uploadedSessions?.length ?? 0,
|
|
428
428
|
uploadedSessions: published?.uploadedSessions ?? [],
|
|
429
429
|
enhancedAt: enhanceCache?.enhancedAt ?? null,
|
|
430
|
+
enhancedSessionCount: enhanceCache?.selectedSessionIds?.length ?? 0,
|
|
430
431
|
totalAgentDuration: agentRow.total,
|
|
431
432
|
totalInputTokens: dbStats.totalInputTokens,
|
|
432
433
|
totalOutputTokens: dbStats.totalOutputTokens,
|
|
@@ -469,6 +470,7 @@ export function createRouteContext(sessionsBasePath, dbPath) {
|
|
|
469
470
|
uploadedSessionCount: published?.uploadedSessions?.length ?? 0,
|
|
470
471
|
uploadedSessions: published?.uploadedSessions ?? [],
|
|
471
472
|
enhancedAt: enhanceCache?.enhancedAt ?? null,
|
|
473
|
+
enhancedSessionCount: enhanceCache?.selectedSessionIds?.length ?? 0,
|
|
472
474
|
totalAgentDuration,
|
|
473
475
|
totalInputTokens: 0,
|
|
474
476
|
totalOutputTokens: 0,
|
|
@@ -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
|
@@ -5,6 +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
9
|
export function createEnhanceRouter(ctx) {
|
|
9
10
|
const router = Router();
|
|
10
11
|
// Triage endpoint -- AI selects which sessions are worth showcasing (SSE stream)
|
|
@@ -81,6 +82,43 @@ export function createEnhanceRouter(ctx) {
|
|
|
81
82
|
});
|
|
82
83
|
}
|
|
83
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
|
+
});
|
|
84
122
|
// Delete locally-saved enhanced data
|
|
85
123
|
router.delete('/api/sessions/:id/enhanced', (_req, res) => {
|
|
86
124
|
const { id } = _req.params;
|
|
@@ -235,6 +273,8 @@ export function createEnhanceRouter(ctx) {
|
|
|
235
273
|
if (!proj)
|
|
236
274
|
return;
|
|
237
275
|
saveProjectEnhanceResult(proj.dirName, selectedSessionIds, result, undefined, { title, repoUrl, projectUrl, screenshotBase64 });
|
|
276
|
+
// Project title/narrative/skills appear in portfolio listing — bust cache.
|
|
277
|
+
invalidatePortfolioPreviewCache();
|
|
238
278
|
res.json({ saved: true, enhancedAt: new Date().toISOString() });
|
|
239
279
|
}
|
|
240
280
|
catch (err) {
|