helixevo 0.2.39 → 0.2.41
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/CHANGELOG.md +26 -0
- package/dashboard/app/changelog/page.tsx +179 -54
- package/dashboard/app/commands/page.tsx +243 -159
- package/dashboard/app/evolution/page.tsx +105 -102
- package/dashboard/app/frontier/page.tsx +103 -100
- package/dashboard/app/globals.css +1105 -403
- package/dashboard/app/guide/page.tsx +48 -4
- package/dashboard/app/layout.tsx +28 -57
- package/dashboard/app/network/client.tsx +453 -269
- package/dashboard/app/network/page.tsx +12 -2
- package/dashboard/app/page.tsx +166 -185
- package/dashboard/app/projects/client.tsx +891 -509
- package/dashboard/app/research/client.tsx +180 -248
- package/dashboard/components/SkillFlowNode.tsx +86 -128
- package/dashboard/components/console-panel.tsx +25 -0
- package/dashboard/components/metric-card.tsx +45 -0
- package/dashboard/components/overview-actions.tsx +29 -40
- package/dashboard/components/page-hero.tsx +44 -0
- package/dashboard/components/quick-actions.tsx +93 -167
- package/dashboard/components/section-frame.tsx +35 -0
- package/dashboard/components/sidebar-nav.tsx +75 -0
- package/dashboard/components/update-banner.tsx +101 -145
- package/dashboard/lib/data.ts +2 -2
- package/dist/cli.js +225 -156
- package/package.json +1 -1
|
@@ -1,128 +1,131 @@
|
|
|
1
1
|
import { loadHistory } from '@/lib/data'
|
|
2
|
+
import { MetricCard } from '@/components/metric-card'
|
|
3
|
+
import { PageHero } from '@/components/page-hero'
|
|
4
|
+
import { SectionFrame } from '@/components/section-frame'
|
|
2
5
|
|
|
3
6
|
export const dynamic = 'force-dynamic'
|
|
4
7
|
|
|
5
8
|
export default function EvolutionPage() {
|
|
6
9
|
const history = loadHistory()
|
|
7
10
|
const iterations = [...history.iterations].reverse()
|
|
8
|
-
const
|
|
9
|
-
const accepted =
|
|
10
|
-
const rejected =
|
|
11
|
+
const totalProposals = iterations.flatMap((iteration) => iteration.proposals)
|
|
12
|
+
const accepted = totalProposals.filter((proposal) => proposal.outcome === 'accepted').length
|
|
13
|
+
const rejected = totalProposals.filter((proposal) => proposal.outcome === 'rejected').length
|
|
14
|
+
const avgAcceptance = totalProposals.length > 0 ? Math.round((accepted / totalProposals.length) * 100) : 0
|
|
11
15
|
|
|
12
16
|
return (
|
|
13
|
-
<div>
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
<div style={{ display: 'grid', gap: 22 }}>
|
|
18
|
+
<PageHero
|
|
19
|
+
eyebrow="Run chronicle"
|
|
20
|
+
title="Evolution Timeline"
|
|
21
|
+
description="Trace each evolution loop from trigger to proposal outcome, with judge scores, regression pressure, and the logic behind what made it into the network."
|
|
22
|
+
chips={[
|
|
23
|
+
{ label: `${iterations.length} runs`, tone: 'purple' },
|
|
24
|
+
{ label: `${accepted} accepted`, tone: 'green' },
|
|
25
|
+
{ label: `${rejected} rejected`, tone: 'red' },
|
|
26
|
+
{ label: `${avgAcceptance}% acceptance rate`, tone: 'blue' },
|
|
27
|
+
]}
|
|
28
|
+
/>
|
|
20
29
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
</div>
|
|
27
|
-
<div className="stat-card">
|
|
28
|
-
<div className="stat-value" style={{ color: 'var(--green)' }}>{accepted}</div>
|
|
29
|
-
<div className="stat-label">Proposals Accepted</div>
|
|
30
|
-
</div>
|
|
31
|
-
<div className="stat-card">
|
|
32
|
-
<div className="stat-value" style={{ color: 'var(--red)' }}>{rejected}</div>
|
|
33
|
-
<div className="stat-label">Proposals Rejected</div>
|
|
34
|
-
</div>
|
|
30
|
+
<div className="grid-4">
|
|
31
|
+
<MetricCard label="Runs" value={iterations.length} sublabel="Completed evolution loops" tone="purple" icon="◎" />
|
|
32
|
+
<MetricCard label="Accepted" value={accepted} sublabel="Promotions that survived judging" tone="green" icon="✓" />
|
|
33
|
+
<MetricCard label="Rejected" value={rejected} sublabel="Proposals blocked by quality gates" tone="red" icon="✕" />
|
|
34
|
+
<MetricCard label="Acceptance rate" value={`${avgAcceptance}%`} sublabel="Across all recorded proposals" tone="blue" icon="↗" />
|
|
35
35
|
</div>
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<div
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
{/* Proposals */}
|
|
68
|
-
{iter.proposals.map(p => (
|
|
69
|
-
<div key={p.id} className="proposal-card" style={{
|
|
70
|
-
borderLeft: `3px solid ${p.outcome === 'accepted' ? 'var(--green)' : 'var(--red)'}`,
|
|
71
|
-
}}>
|
|
72
|
-
<div style={{ padding: '12px 16px' }}>
|
|
73
|
-
{/* Proposal Header */}
|
|
74
|
-
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
|
75
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
76
|
-
<span className={`badge ${p.outcome === 'accepted' ? 'badge-green' : 'badge-red'}`}>
|
|
77
|
-
{p.outcome}
|
|
78
|
-
</span>
|
|
79
|
-
<span style={{ fontSize: 14, fontWeight: 600 }}>{p.targetSkill}</span>
|
|
80
|
-
<span className="badge badge-gray">{p.action}</span>
|
|
37
|
+
<SectionFrame
|
|
38
|
+
eyebrow="Timeline"
|
|
39
|
+
title="Recent evolution runs"
|
|
40
|
+
description="Each run groups the trigger context, clustered failures, and proposal outcomes into a compact chronicle block."
|
|
41
|
+
>
|
|
42
|
+
{iterations.length === 0 ? (
|
|
43
|
+
<div className="empty-state" style={{ padding: 24 }}>
|
|
44
|
+
<div className="empty-state-title">No evolution runs yet</div>
|
|
45
|
+
<div className="empty-state-desc">Run <code>helixevo evolve</code> to start the evolution loop.</div>
|
|
46
|
+
</div>
|
|
47
|
+
) : (
|
|
48
|
+
<div className="chronicle-list">
|
|
49
|
+
{iterations.map((iteration, index) => (
|
|
50
|
+
<div key={`${iteration.id}-${index}`} className="chronicle-item">
|
|
51
|
+
<div className="chronicle-marker" />
|
|
52
|
+
<article className="chronicle-card">
|
|
53
|
+
<div className="chronicle-card-header">
|
|
54
|
+
<div>
|
|
55
|
+
<div className="chronicle-title">{iteration.id}</div>
|
|
56
|
+
<div className="chronicle-meta">
|
|
57
|
+
<span>{new Date(iteration.timestamp).toLocaleString()}</span>
|
|
58
|
+
<span>•</span>
|
|
59
|
+
<span>{iteration.trigger}</span>
|
|
60
|
+
<span>•</span>
|
|
61
|
+
<span>{iteration.failureCount} failures</span>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', justifyContent: 'flex-end' }}>
|
|
65
|
+
<span className="badge badge-green">{iteration.proposals.filter((proposal) => proposal.outcome === 'accepted').length} accepted</span>
|
|
66
|
+
<span className="badge badge-red">{iteration.proposals.filter((proposal) => proposal.outcome === 'rejected').length} rejected</span>
|
|
81
67
|
</div>
|
|
82
68
|
</div>
|
|
83
69
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
70
|
+
<div className="chronicle-body">
|
|
71
|
+
{iteration.failureClusters.length > 0 ? (
|
|
72
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 14 }}>
|
|
73
|
+
{iteration.failureClusters.map((cluster, clusterIndex) => (
|
|
74
|
+
<span key={`${cluster.type}-${clusterIndex}`} className="badge badge-blue">
|
|
75
|
+
{cluster.type} ({cluster.count})
|
|
76
|
+
</span>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
) : null}
|
|
80
|
+
|
|
81
|
+
{iteration.proposals.map((proposal) => (
|
|
82
|
+
<div key={proposal.id} className="proposal-card" style={{ borderLeft: `3px solid ${proposal.outcome === 'accepted' ? 'var(--green)' : 'var(--red)'}` }}>
|
|
83
|
+
<div style={{ padding: '14px 16px' }}>
|
|
84
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12, marginBottom: 10 }}>
|
|
85
|
+
<div>
|
|
86
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
|
87
|
+
<span className={`badge ${proposal.outcome === 'accepted' ? 'badge-green' : 'badge-red'}`}>{proposal.outcome}</span>
|
|
88
|
+
<span style={{ fontSize: 15, fontWeight: 800, letterSpacing: '-0.02em' }}>{proposal.targetSkill}</span>
|
|
89
|
+
<span className="badge badge-gray">{proposal.action}</span>
|
|
90
|
+
</div>
|
|
91
|
+
<div style={{ marginTop: 8, color: 'var(--text-secondary)', fontSize: 12.5, lineHeight: 1.65 }}>
|
|
92
|
+
{proposal.description.slice(0, 260)}
|
|
93
|
+
{proposal.description.length > 260 ? '…' : ''}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
<div style={{ minWidth: 92, textAlign: 'right' }}>
|
|
97
|
+
<div style={{ fontSize: 11, color: 'var(--text-muted)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.08em' }}>Regression</div>
|
|
98
|
+
<div style={{ marginTop: 6, fontSize: 22, fontWeight: 850, lineHeight: 1, color: proposal.regressionResult.total > 0 && proposal.regressionResult.passed === proposal.regressionResult.total ? 'var(--green)' : 'var(--text)' }}>
|
|
99
|
+
{proposal.regressionResult.total > 0 ? `${proposal.regressionResult.passed}/${proposal.regressionResult.total}` : '—'}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
88
103
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 10 }}>
|
|
105
|
+
{[
|
|
106
|
+
{ label: 'Task', score: proposal.judges.taskCompletion.score, color: 'var(--green)' },
|
|
107
|
+
{ label: 'Align', score: proposal.judges.correctionAlignment.score, color: 'var(--blue)' },
|
|
108
|
+
{ label: 'Side-effects', score: proposal.judges.sideEffectCheck.score, color: 'var(--purple)' },
|
|
109
|
+
].map((judge) => (
|
|
110
|
+
<span key={judge.label} className="score-pill" style={{ color: judge.score >= 7 ? judge.color : 'var(--red)' }}>
|
|
111
|
+
{judge.label} {judge.score}/10
|
|
112
|
+
</span>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div style={{ color: 'var(--text-dim)', fontSize: 11.5, lineHeight: 1.55 }}>
|
|
117
|
+
{proposal.outcomeReason}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
99
120
|
</div>
|
|
100
121
|
))}
|
|
101
|
-
{p.regressionResult.total > 0 && (
|
|
102
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12 }}>
|
|
103
|
-
<span style={{ color: 'var(--text-dim)' }}>Regression:</span>
|
|
104
|
-
<b>{p.regressionResult.passed}/{p.regressionResult.total}</b>
|
|
105
|
-
</div>
|
|
106
|
-
)}
|
|
107
122
|
</div>
|
|
108
|
-
|
|
109
|
-
{/* Outcome Reason */}
|
|
110
|
-
<div style={{ fontSize: 11, color: 'var(--text-muted)', lineHeight: 1.4 }}>{p.outcomeReason}</div>
|
|
111
|
-
</div>
|
|
123
|
+
</article>
|
|
112
124
|
</div>
|
|
113
125
|
))}
|
|
114
126
|
</div>
|
|
115
|
-
))}
|
|
116
|
-
|
|
117
|
-
{iterations.length === 0 && (
|
|
118
|
-
<div className="card" style={{ marginLeft: 28 }}>
|
|
119
|
-
<div className="empty-state">
|
|
120
|
-
<div className="empty-state-title">No evolution runs yet</div>
|
|
121
|
-
<div className="empty-state-desc">Run <code>helixevo evolve</code> to start the evolution loop</div>
|
|
122
|
-
</div>
|
|
123
|
-
</div>
|
|
124
127
|
)}
|
|
125
|
-
</
|
|
128
|
+
</SectionFrame>
|
|
126
129
|
</div>
|
|
127
130
|
)
|
|
128
131
|
}
|
|
@@ -1,128 +1,131 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { loadCanaries, loadFrontier } from '@/lib/data'
|
|
2
|
+
import { MetricCard } from '@/components/metric-card'
|
|
3
|
+
import { PageHero } from '@/components/page-hero'
|
|
4
|
+
import { SectionFrame } from '@/components/section-frame'
|
|
2
5
|
|
|
3
6
|
export const dynamic = 'force-dynamic'
|
|
4
7
|
|
|
8
|
+
function scoreTone(value: number): 'green' | 'yellow' | 'red' {
|
|
9
|
+
if (value >= 0.8) return 'green'
|
|
10
|
+
if (value >= 0.6) return 'yellow'
|
|
11
|
+
return 'red'
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
export default function FrontierPage() {
|
|
6
15
|
const frontier = loadFrontier()
|
|
7
16
|
const canaries = loadCanaries()
|
|
17
|
+
const topScore = frontier.programs[0]?.score ?? 0
|
|
8
18
|
|
|
9
19
|
return (
|
|
10
|
-
<div>
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
<div key={`${p.id}-${rank}`} className="card" style={{ overflow: 'hidden' }}>
|
|
22
|
-
<div className="card-body" style={{ position: 'relative' }}>
|
|
23
|
-
{/* Rank Badge */}
|
|
24
|
-
<div style={{
|
|
25
|
-
position: 'absolute', top: 0, right: 0,
|
|
26
|
-
padding: '4px 16px',
|
|
27
|
-
background: rank === 0 ? 'var(--purple)' : rank === 1 ? 'var(--blue)' : 'var(--bg-section)',
|
|
28
|
-
color: rank <= 1 ? '#fff' : 'var(--text-dim)',
|
|
29
|
-
borderBottomLeftRadius: 10, fontSize: 12, fontWeight: 700,
|
|
30
|
-
}}>
|
|
31
|
-
#{rank + 1}
|
|
32
|
-
</div>
|
|
20
|
+
<div style={{ display: 'grid', gap: 22 }}>
|
|
21
|
+
<PageHero
|
|
22
|
+
eyebrow="Candidate board"
|
|
23
|
+
title="Pareto Frontier"
|
|
24
|
+
description="Compare the strongest skill configurations currently competing on the frontier and inspect which canaries are still proving themselves in the wild."
|
|
25
|
+
chips={[
|
|
26
|
+
{ label: `${frontier.programs.length}/${frontier.capacity} frontier slots filled`, tone: 'green' },
|
|
27
|
+
{ label: `${canaries.entries.length} active canaries`, tone: canaries.entries.length > 0 ? 'yellow' : 'neutral' },
|
|
28
|
+
{ label: `Top score ${(topScore * 100).toFixed(0)}`, tone: 'purple' },
|
|
29
|
+
]}
|
|
30
|
+
/>
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
</div>
|
|
40
|
-
</div>
|
|
32
|
+
<div className="grid-3">
|
|
33
|
+
<MetricCard label="Frontier size" value={frontier.programs.length} sublabel={`Capacity ${frontier.capacity}`} tone="purple" icon="▲" />
|
|
34
|
+
<MetricCard label="Top composite" value={`${(topScore * 100).toFixed(0)}`} sublabel="Best current composite score" tone="green" icon="★" />
|
|
35
|
+
<MetricCard label="Canary pipeline" value={canaries.entries.length} sublabel="Configs still under validation" tone={canaries.entries.length > 0 ? 'yellow' : 'blue'} icon="◌" />
|
|
36
|
+
</div>
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
{(
|
|
38
|
+
<SectionFrame
|
|
39
|
+
eyebrow="Leaderboard"
|
|
40
|
+
title="Current frontier programs"
|
|
41
|
+
description="Each candidate is scored across task completion, correction alignment, and side-effect safety before it earns frontier placement."
|
|
42
|
+
>
|
|
43
|
+
{frontier.programs.length === 0 ? (
|
|
44
|
+
<div className="empty-state" style={{ padding: 24 }}>
|
|
45
|
+
<div className="empty-state-title">Frontier empty</div>
|
|
46
|
+
<div className="empty-state-desc">Run <code>helixevo evolve</code> to populate the frontier.</div>
|
|
47
|
+
</div>
|
|
48
|
+
) : (
|
|
49
|
+
<div style={{ display: 'grid', gap: 14 }}>
|
|
50
|
+
{frontier.programs.map((program, rank) => (
|
|
51
|
+
<article key={`${program.id}-${rank}`} className="section-frame section-frame-neutral" style={{ overflow: 'hidden' }}>
|
|
52
|
+
<div className="section-frame-body" style={{ paddingTop: 24 }}>
|
|
53
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 18, marginBottom: 18, flexWrap: 'wrap' }}>
|
|
54
|
+
<div>
|
|
55
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
|
|
56
|
+
<span className={`badge ${rank === 0 ? 'badge-purple' : rank === 1 ? 'badge-blue' : 'badge-gray'}`}>#{rank + 1}</span>
|
|
57
|
+
<span style={{ fontSize: 20, fontWeight: 850, letterSpacing: '-0.03em' }}>{program.id}</span>
|
|
58
|
+
</div>
|
|
59
|
+
<div style={{ marginTop: 8, color: 'var(--text-dim)', fontSize: 12.5 }}>
|
|
60
|
+
gen {program.generation} • created {new Date(program.createdAt).toLocaleDateString()}
|
|
61
|
+
</div>
|
|
64
62
|
</div>
|
|
65
|
-
<div
|
|
66
|
-
<div
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
<div style={{ textAlign: 'right' }}>
|
|
64
|
+
<div style={{ fontSize: 11, color: 'var(--text-muted)', fontWeight: 800, textTransform: 'uppercase', letterSpacing: '0.08em' }}>Composite</div>
|
|
65
|
+
<div style={{ marginTop: 6, fontSize: 34, fontWeight: 850, lineHeight: 1, color: scoreTone(program.score) === 'green' ? 'var(--green)' : scoreTone(program.score) === 'yellow' ? 'var(--yellow)' : 'var(--red)' }}>
|
|
66
|
+
{(program.score * 100).toFixed(0)}
|
|
67
|
+
</div>
|
|
70
68
|
</div>
|
|
71
69
|
</div>
|
|
72
|
-
))}
|
|
73
|
-
</div>
|
|
74
70
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
71
|
+
<div className="grid-4" style={{ marginBottom: 16 }}>
|
|
72
|
+
{[
|
|
73
|
+
{ label: 'Composite', value: program.score, tone: 'purple' as const },
|
|
74
|
+
{ label: 'Task', value: program.scores.taskCompletion, tone: 'green' as const },
|
|
75
|
+
{ label: 'Alignment', value: program.scores.correctionAlignment, tone: 'blue' as const },
|
|
76
|
+
{ label: 'Side-effect free', value: program.scores.sideEffectFree, tone: 'yellow' as const },
|
|
77
|
+
].map((score) => (
|
|
78
|
+
<MetricCard
|
|
79
|
+
key={score.label}
|
|
80
|
+
label={score.label}
|
|
81
|
+
value={(score.value * 100).toFixed(0)}
|
|
82
|
+
sublabel={<div className="score-track" style={{ marginTop: 4 }}><div className="score-fill" style={{ width: `${score.value * 100}%`, background: scoreTone(score.value) === 'green' ? 'var(--green)' : scoreTone(score.value) === 'yellow' ? 'var(--yellow)' : 'var(--red)' }} /></div>}
|
|
83
|
+
tone={score.tone}
|
|
84
|
+
/>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
82
87
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
<div style={{ fontSize: 12.5, color: 'var(--text-secondary)', lineHeight: 1.7 }}>
|
|
89
|
+
{program.changesDescription.slice(0, 260)}
|
|
90
|
+
{program.changesDescription.length > 260 ? '…' : ''}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</article>
|
|
94
|
+
))}
|
|
89
95
|
</div>
|
|
90
96
|
)}
|
|
91
|
-
</
|
|
97
|
+
</SectionFrame>
|
|
92
98
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
<SectionFrame
|
|
100
|
+
eyebrow="Validation"
|
|
101
|
+
title="Active canaries"
|
|
102
|
+
description="Canary deployments remain under observation before they graduate fully into the frontier."
|
|
103
|
+
tone="yellow"
|
|
104
|
+
>
|
|
105
|
+
{canaries.entries.length > 0 ? (
|
|
106
|
+
<div className="summary-list">
|
|
107
|
+
{canaries.entries.map((entry, index) => {
|
|
108
|
+
const daysLeft = Math.max(0, Math.ceil((new Date(entry.expiresAt).getTime() - Date.now()) / (24 * 60 * 60 * 1000)))
|
|
100
109
|
return (
|
|
101
|
-
<div key={
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
106
|
-
<div style={{
|
|
107
|
-
width: 8, height: 8, borderRadius: '50%',
|
|
108
|
-
background: daysLeft > 0 ? 'var(--yellow)' : 'var(--green)',
|
|
109
|
-
}} />
|
|
110
|
-
<span style={{ fontWeight: 500, fontSize: 13 }}>{c.skillSlug}</span>
|
|
111
|
-
<span style={{ color: 'var(--text-muted)', fontSize: 11 }}>{c.version}</span>
|
|
110
|
+
<div key={`${entry.skillSlug}-${index}`} className="summary-row">
|
|
111
|
+
<div className="summary-row-main">
|
|
112
|
+
<div className="summary-row-title">{entry.skillSlug}</div>
|
|
113
|
+
<div className="summary-row-meta">{entry.version} • deployed {new Date(entry.deployedAt).toLocaleDateString()}</div>
|
|
112
114
|
</div>
|
|
113
115
|
<span className={`badge ${daysLeft > 0 ? 'badge-yellow' : 'badge-green'}`}>
|
|
114
116
|
{daysLeft > 0 ? `${daysLeft}d remaining` : 'Ready to promote'}
|
|
115
117
|
</span>
|
|
116
118
|
</div>
|
|
117
119
|
)
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
</div>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
120
|
+
})}
|
|
121
|
+
</div>
|
|
122
|
+
) : (
|
|
123
|
+
<div className="empty-state" style={{ padding: 24 }}>
|
|
124
|
+
<div className="empty-state-title">No active canaries</div>
|
|
125
|
+
<div className="empty-state-desc">When new candidate configurations are staged, they will appear here until their evaluation window ends.</div>
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
</SectionFrame>
|
|
126
129
|
</div>
|
|
127
130
|
)
|
|
128
131
|
}
|