Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

plan-approval.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

plan-approval.tsxBlame773 lines · 1 contributor
11c3ab6ccanty labs1import type { FC } from "hono/jsx";
2
3export interface PlanStep {
4 n: string;
5 title: string;
6 desc: string;
7}
8
9export interface Plan {
10 id: number | string;
11 spec: string;
12 steps: PlanStep[];
13}
14
15export interface PlanApprovalProps {
16 plan?: Plan;
17 blastRadius?: any;
18 costEstimate?: any;
19 user?: any;
20}
21
22const DEFAULT_STEPS = [
23 {
24 n: "01",
25 title: "Add plan-lapse state to the schema",
26 detail:
27 "New enum value 'lapsed' + lapsedAt timestamp. Additive migration, no backfill needed.",
28 files: "drizzle/0108_plan_lapse.sql · src/db/schema.ts",
29 },
30 {
31 n: "02",
32 title: "Enforce read-only on lapsed orgs",
33 detail:
34 "Git push and web writes rejected with a clear message; clone, fetch, and browsing stay open. Internal orgs short-circuit before any check.",
35 files: "src/middleware/plan-guard.ts · src/git/protocol.ts",
36 },
37 {
38 n: "03",
39 title: "Warning emails at 7 days and 1 day",
40 detail:
41 "Two templated emails via the existing notify pipeline, deduped per org, logged to the audit trail.",
42 files: "src/lib/notify.ts · src/lib/autopilot.ts",
43 },
44 {
45 n: "04",
46 title: "Admin visibility",
47 detail:
48 "Lapsed orgs surface in the customers table with days-remaining, next to the existing exempt/current states.",
49 files: "src/routes/admin.tsx",
50 },
51 {
52 n: "05",
53 title: "Tests",
54 detail:
55 "12 new tests: guard behavior, internal-org exemption, email dedupe, migration idempotence.",
56 files: "src/__tests__/plan-guard.test.ts",
57 },
58];
59
60const DEFAULT_SPEC =
61 "When a customer's plan lapses, keep their repos read-only instead of blocking clone access. Send one warning email at 7 days and one at 1 day. Internal orgs are never affected.";
62
63const STEP_COUNT = DEFAULT_STEPS.length;
64
65const inlineJs = `
66(function () {
67 var manual = {};
68 var approved = false;
69 var editing = false;
70 var STEP_COUNT = ${STEP_COUNT};
71
72 function getManualCount() {
73 var n = 0;
74 for (var k in manual) { if (manual[k]) n++; }
75 return n;
76 }
77
78 function updateUI() {
79 var manualCount = getManualCount();
80 var agentSteps = STEP_COUNT - manualCount;
81
82 for (var i = 0; i < STEP_COUNT; i++) {
83 var btn = document.getElementById('pa-step-btn-' + i);
84 if (!btn) continue;
85 if (manual[i]) {
86 btn.textContent = 'Yours ✓';
87 btn.style.border = '1px solid rgba(67,83,201,0.35)';
88 btn.style.background = 'rgba(67,83,201,0.06)';
89 btn.style.color = '#4353c9';
90 } else {
91 btn.textContent = "I’ll do this";
92 btn.style.border = '1px solid rgba(22,24,29,0.12)';
93 btn.style.background = '#ffffff';
94 btn.style.color = '#6b7080';
95 }
96 }
97
98 var filesTouched = document.getElementById('pa-files-touched');
99 if (filesTouched) filesTouched.textContent = String(9 - manualCount);
100
101 var yourTime = document.getElementById('pa-your-time');
102 if (yourTime) {
103 yourTime.textContent =
104 manualCount > 0
105 ? 'review + ' + manualCount + ' manual step' + (manualCount > 1 ? 's' : '')
106 : '≈ 10 min review';
107 }
108
109 var approveBtn = document.getElementById('pa-approve-btn');
110 if (approveBtn) {
111 if (approved) {
112 approveBtn.textContent = 'Plan approved — agent starting';
113 approveBtn.style.background = '#1e7f5c';
114 } else if (manualCount > 0) {
115 approveBtn.textContent = 'Approve plan (' + agentSteps + ' agent step' + (agentSteps !== 1 ? 's' : '') + ')';
116 approveBtn.style.background = '#16181d';
117 } else {
118 approveBtn.textContent = 'Approve plan';
119 approveBtn.style.background = '#16181d';
120 }
121 }
122 }
123
124 for (var i = 0; i < STEP_COUNT; i++) {
125 (function (idx) {
126 var btn = document.getElementById('pa-step-btn-' + idx);
127 if (btn) {
128 btn.addEventListener('click', function () {
129 manual[idx] = !manual[idx];
130 updateUI();
131 });
132 }
133 })(i);
134 }
135
136 var approveBtn = document.getElementById('pa-approve-btn');
137 if (approveBtn) {
138 approveBtn.addEventListener('click', function () {
139 if (!approved) {
140 approved = true;
141 updateUI();
142 }
143 });
144 }
145
146 var editBtn = document.getElementById('pa-edit-btn');
147 if (editBtn) {
148 editBtn.addEventListener('click', function () {
149 editing = !editing;
150 editBtn.textContent = editing
151 ? "Editing — tap ‘I’ll do this’ on any step"
152 : 'Edit the plan';
153 });
154 }
155})();
156`;
157
158export const PlanApprovalView: FC<PlanApprovalProps> = (props) => {
159 const plan = props.plan;
160 const spec = plan?.spec ?? DEFAULT_SPEC;
161 const planId = plan?.id ?? 31;
162 const steps = DEFAULT_STEPS;
163
164 return (
165 <>
166 <style dangerouslySetInnerHTML={{ __html: css }} />
167 <div class="pa-root">
168 <header class="pa-header">
169 <a href="/" class="pa-logo">
170 <span class="pa-logo-mark"></span>
171 gluecron
172 </a>
173 <nav class="pa-breadcrumb">
174 <span class="pa-breadcrumb-item">ccantynz-alt</span>
175 <span class="pa-breadcrumb-sep">/</span>
176 <span class="pa-breadcrumb-item">gluecron</span>
177 <span class="pa-breadcrumb-sep">/</span>
178 <span class="pa-breadcrumb-item pa-breadcrumb-active">Specs</span>
179 </nav>
180 <div class="pa-header-right">
181 <span class="pa-avatar">C</span>
182 </div>
183 </header>
184
185 <main class="pa-main">
186 <div class="pa-page-header">
187 <div class="pa-eyebrow">Spec №{planId} · Plan review</div>
188 <h1 class="pa-title">Approve the plan, not just the PR.</h1>
189 <p class="pa-subtitle">
190 Before a single line is written, here is exactly what the agent intends to do — the
191 blast radius, the cost, and how to undo it. Approve it, edit it, or take any piece of
192 it manually.
193 </p>
194 </div>
195
196 <div class="pa-grid">
197 {/* Left: the plan */}
198 <section class="pa-left">
199 {/* Spec quote card */}
200 <div class="pa-spec-card">
201 <div class="pa-spec-label">Your spec — as written</div>
202 <p class="pa-spec-quote">"{spec}"</p>
203 </div>
204
205 {/* Plan steps */}
206 <h2 class="pa-steps-heading">The plan · {steps.length} steps</h2>
207 <div class="pa-steps-list">
208 {steps.map((step, i) => (
209 <div class="pa-step-row" id={"pa-step-row-" + i}>
210 <span class="pa-step-num">{step.n}</span>
211 <div class="pa-step-body">
212 <div class="pa-step-title">{step.title}</div>
213 <div class="pa-step-detail">{step.detail}</div>
214 <div class="pa-step-files">{step.files}</div>
215 </div>
216 <button
217 type="button"
218 id={"pa-step-btn-" + i}
219 class="pa-step-btn"
220 >
221 I'll do this
222 </button>
223 </div>
224 ))}
225 </div>
226 <p class="pa-steps-note">
227 Steps marked{" "}
228 <strong class="pa-steps-note-em">I'll do this</strong> are left out of the agent's
229 branch — the PR arrives with those files untouched and a checklist for you.
230 </p>
231 </section>
232
233 {/* Right: aside */}
234 <aside class="pa-aside">
235 {/* Blast radius */}
236 <div class="pa-section">
237 <h3 class="pa-section-label">Blast radius</h3>
238 <div class="pa-card">
239 <div class="pa-card-rows">
240 <div class="pa-card-row">
241 <span class="pa-card-key">Files touched</span>
242 <span class="pa-card-val pa-mono" id="pa-files-touched">
243 9
244 </span>
245 </div>
246 <div class="pa-card-row">
247 <span class="pa-card-key">Repos affected</span>
248 <span class="pa-card-val pa-mono">1 · gluecron</span>
249 </div>
250 <div class="pa-card-row">
251 <span class="pa-card-key">DB migration</span>
252 <span class="pa-card-val pa-mono pa-amber">1 · additive only</span>
253 </div>
254 <div class="pa-card-row">
255 <span class="pa-card-key">Public API change</span>
256 <span class="pa-card-val pa-mono pa-green">none</span>
257 </div>
258 <div class="pa-card-row">
259 <span class="pa-card-key">Tests to write</span>
260 <span class="pa-card-val pa-mono">12</span>
261 </div>
262 </div>
263 <div class="pa-divider"></div>
264 <p class="pa-card-note">
265 <strong class="pa-card-note-em">Downstream:</strong> billing cron, email
266 digests, and the admin customers table read the plan-state column this plan
267 modifies.
268 </p>
269 </div>
270 </div>
271
272 {/* Cost before code */}
273 <div class="pa-section">
274 <h3 class="pa-section-label">Cost before code</h3>
275 <div class="pa-card">
276 <div class="pa-card-rows">
277 <div class="pa-card-row">
278 <span class="pa-card-key">Estimated AI spend</span>
279 <span class="pa-card-val pa-mono">$1.10 – $1.80</span>
280 </div>
281 <div class="pa-card-row">
282 <span class="pa-card-key">Model routing</span>
283 <span class="pa-card-val pa-mono">local first</span>
284 </div>
285 <div class="pa-card-row">
286 <span class="pa-card-key">Sandbox time</span>
287 <span class="pa-card-val pa-mono">≈ 25 min</span>
288 </div>
289 <div class="pa-card-row">
290 <span class="pa-card-key">Your time needed</span>
291 <span class="pa-card-val pa-mono" id="pa-your-time">
292 ≈ 10 min review
293 </span>
294 </div>
295 </div>
296 </div>
297 </div>
298
299 {/* How this reverses */}
300 <div class="pa-section">
301 <h3 class="pa-section-label">How this reverses</h3>
302 <div class="pa-card">
303 <p class="pa-revert-text">
304 Single revert commit restores all code paths. The migration is additive, so
305 rollback needs no data repair. Warning emails already sent are logged but cannot
306 be unsent — flagged as this plan's only irreversible effect.
307 </p>
308 </div>
309 </div>
310
311 {/* Actions */}
312 <div class="pa-actions">
313 <button type="button" id="pa-approve-btn" class="pa-btn-primary">
314 Approve plan
315 </button>
316 <button type="button" id="pa-edit-btn" class="pa-btn-secondary">
317 Edit the plan
318 </button>
319 <p class="pa-disclaimer">Nothing is written until you approve.</p>
320 </div>
321 </aside>
322 </div>
323 </main>
324 </div>
325 <script dangerouslySetInnerHTML={{ __html: inlineJs }} />
326 </>
327 );
328};
329
330export default PlanApprovalView;
331
332const css = `
333@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;450;500;600;700&family=Inter+Tight:wght@500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
334
335*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
336
337.pa-root {
338 font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
339 font-size: 14px;
340 line-height: 1.55;
341 letter-spacing: -0.008em;
342 color: #16181d;
343 background: #fcfcfd;
344 min-height: 100vh;
345 display: flex;
346 flex-direction: column;
347 -webkit-font-smoothing: antialiased;
348 text-rendering: optimizeLegibility;
349}
350
351/* ── Header ─────────────────────────────────────────────── */
352
353.pa-header {
354 height: 56px;
355 border-bottom: 1px solid rgba(22,24,29,0.07);
356 background: #fcfcfd;
357 display: flex;
358 align-items: center;
359 padding: 0 28px;
360 gap: 20px;
361 position: sticky;
362 top: 0;
363 z-index: 50;
364}
365
366.pa-logo {
367 display: inline-flex;
368 align-items: center;
369 gap: 9px;
370 font-family: 'Inter Tight', sans-serif;
371 font-weight: 600;
372 font-size: 15px;
373 letter-spacing: -0.02em;
374 color: #16181d;
375 text-decoration: none;
376 flex-shrink: 0;
377}
378
379.pa-logo-mark {
380 width: 16px;
381 height: 16px;
382 border-radius: 5px;
383 background: #4353c9;
384 display: inline-block;
385 flex-shrink: 0;
386}
387
388.pa-breadcrumb {
389 display: flex;
390 align-items: center;
391 gap: 4px;
392 font-size: 13px;
393 color: #6b7080;
394}
395
396.pa-breadcrumb-item {
397 padding: 5px 8px;
398}
399
400.pa-breadcrumb-sep {
401 color: #c4c6cf;
402}
403
404.pa-breadcrumb-active {
405 color: #16181d;
406 font-weight: 500;
407}
408
409.pa-header-right {
410 margin-left: auto;
411 display: flex;
412 align-items: center;
413 gap: 14px;
414}
415
416.pa-avatar {
417 width: 28px;
418 height: 28px;
419 border-radius: 50%;
420 background: #16181d;
421 color: #fff;
422 font-size: 11px;
423 font-weight: 600;
424 display: inline-flex;
425 align-items: center;
426 justify-content: center;
427 flex-shrink: 0;
428}
429
430/* ── Main ───────────────────────────────────────────────── */
431
432.pa-main {
433 flex: 1;
434 max-width: 1180px;
435 width: 100%;
436 margin: 0 auto;
437 padding: 44px 32px 80px;
438}
439
440/* ── Page header ────────────────────────────────────────── */
441
442.pa-page-header {
443 max-width: 720px;
444 margin-bottom: 40px;
445}
446
447.pa-eyebrow {
448 font-family: 'JetBrains Mono', monospace;
449 font-size: 11px;
450 letter-spacing: 0.12em;
451 text-transform: uppercase;
452 color: #8a8d99;
453 margin-bottom: 10px;
454}
455
456.pa-title {
457 font-family: 'Inter Tight', sans-serif;
458 font-size: 28px;
459 font-weight: 600;
460 letter-spacing: -0.025em;
461 line-height: 1.15;
462 color: #111318;
463 margin: 0 0 10px;
464}
465
466.pa-subtitle {
467 font-size: 14px;
468 color: #6b7080;
469 line-height: 1.6;
470}
471
472/* ── Two-column grid ────────────────────────────────────── */
473
474.pa-grid {
475 display: grid;
476 grid-template-columns: minmax(0, 1.5fr) minmax(300px, 1fr);
477 gap: 44px;
478 align-items: start;
479}
480
481/* ── Left column ────────────────────────────────────────── */
482
483.pa-left {}
484
485/* Spec quote card */
486
487.pa-spec-card {
488 border: 1px solid rgba(22,24,29,0.07);
489 border-left: 3px solid #4353c9;
490 border-radius: 12px;
491 background: #ffffff;
492 padding: 20px 24px;
493 margin-bottom: 28px;
494}
495
496.pa-spec-label {
497 font-family: 'JetBrains Mono', monospace;
498 font-size: 11px;
499 letter-spacing: 0.1em;
500 text-transform: uppercase;
501 color: #8a8d99;
502 font-weight: 500;
503 margin-bottom: 10px;
504}
505
506.pa-spec-quote {
507 font-size: 14px;
508 color: #16181d;
509 line-height: 1.65;
510 font-style: italic;
511}
512
513/* Plan steps */
514
515.pa-steps-heading {
516 font-family: 'Inter Tight', sans-serif;
517 font-size: 15px;
518 font-weight: 600;
519 letter-spacing: -0.015em;
520 color: #16181d;
521 margin: 0 0 16px;
522}
523
524.pa-steps-list {
525 display: flex;
526 flex-direction: column;
527 margin-bottom: 8px;
528}
529
530.pa-step-row {
531 display: grid;
532 grid-template-columns: 24px minmax(0, 1fr) auto;
533 gap: 14px;
534 padding: 14px 4px;
535 border-bottom: 1px solid rgba(22,24,29,0.06);
536 align-items: start;
537}
538
539.pa-step-num {
540 font-family: 'JetBrains Mono', monospace;
541 font-size: 12px;
542 color: #8a8d99;
543 padding-top: 2px;
544 line-height: 1.55;
545}
546
547.pa-step-body {
548 min-width: 0;
549}
550
551.pa-step-title {
552 font-size: 14px;
553 font-weight: 500;
554 color: #16181d;
555 line-height: 1.4;
556}
557
558.pa-step-detail {
559 font-size: 12.5px;
560 color: #6b7080;
561 margin-top: 2px;
562 line-height: 1.5;
563}
564
565.pa-step-files {
566 font-family: 'JetBrains Mono', monospace;
567 font-size: 11.5px;
568 color: #8a8d99;
569 margin-top: 6px;
570 line-height: 1.5;
571}
572
573.pa-step-btn {
574 padding: 5px 12px;
575 border-radius: 7px;
576 font-size: 12px;
577 font-weight: 500;
578 border: 1px solid rgba(22,24,29,0.12);
579 background: #ffffff;
580 color: #6b7080;
581 cursor: pointer;
582 font-family: inherit;
583 white-space: nowrap;
584 transition: border-color 0.12s, background 0.12s, color 0.12s;
585 flex-shrink: 0;
586 align-self: start;
587 margin-top: 2px;
588}
589
590.pa-step-btn:hover {
591 border-color: rgba(22,24,29,0.22);
592}
593
594.pa-steps-note {
595 font-size: 12.5px;
596 color: #8a8d99;
597 margin: 10px 0 0;
598 line-height: 1.6;
599}
600
601.pa-steps-note-em {
602 color: #16181d;
603 font-weight: 500;
604}
605
606/* ── Right aside ────────────────────────────────────────── */
607
608.pa-aside {
609 display: flex;
610 flex-direction: column;
611 gap: 32px;
612 position: sticky;
613 top: 92px;
614}
615
616.pa-section {}
617
618.pa-section-label {
619 font-family: 'JetBrains Mono', monospace;
620 font-size: 11px;
621 letter-spacing: 0.12em;
622 text-transform: uppercase;
623 color: #8a8d99;
624 font-weight: 500;
625 margin: 0 0 14px;
626 display: block;
627}
628
629.pa-card {
630 border: 1px solid rgba(22,24,29,0.07);
631 border-radius: 12px;
632 background: #ffffff;
633 padding: 20px;
634}
635
636.pa-card-rows {
637 display: flex;
638 flex-direction: column;
639 gap: 12px;
640}
641
642.pa-card-row {
643 display: flex;
644 justify-content: space-between;
645 align-items: baseline;
646 font-size: 13px;
647 gap: 12px;
648}
649
650.pa-card-key {
651 color: #6b7080;
652 flex-shrink: 0;
653}
654
655.pa-card-val {
656 color: #16181d;
657 text-align: right;
658}
659
660.pa-mono {
661 font-family: 'JetBrains Mono', monospace;
662 font-variant-numeric: tabular-nums;
663}
664
665.pa-amber {
666 color: #b45309;
667}
668
669.pa-green {
670 color: #1e7f5c;
671}
672
673.pa-divider {
674 height: 1px;
675 background: rgba(22,24,29,0.06);
676 margin: 16px 0;
677}
678
679.pa-card-note {
680 font-size: 12.5px;
681 color: #6b7080;
682 line-height: 1.6;
683}
684
685.pa-card-note-em {
686 color: #16181d;
687 font-weight: 500;
688}
689
690.pa-revert-text {
691 font-size: 12.5px;
692 color: #6b7080;
693 line-height: 1.65;
694}
695
696/* ── Actions ────────────────────────────────────────────── */
697
698.pa-actions {
699 display: flex;
700 flex-direction: column;
701 gap: 10px;
702}
703
704.pa-btn-primary {
705 padding: 10px 16px;
706 border-radius: 9px;
707 font-size: 13.5px;
708 font-weight: 500;
709 border: none;
710 background: #16181d;
711 color: #fff;
712 cursor: pointer;
713 font-family: inherit;
714 white-space: nowrap;
715 transition: background 0.18s;
716 text-align: center;
717 display: block;
718 width: 100%;
719}
720
721.pa-btn-primary:hover {
722 background: #2a2d38;
723}
724
725.pa-btn-secondary {
726 padding: 10px 16px;
727 border-radius: 9px;
728 font-size: 13.5px;
729 font-weight: 500;
730 border: 1px solid rgba(22,24,29,0.12);
731 background: #ffffff;
732 color: #16181d;
733 cursor: pointer;
734 font-family: inherit;
735 white-space: nowrap;
736 transition: border-color 0.12s;
737 text-align: center;
738 display: block;
739 width: 100%;
740}
741
742.pa-btn-secondary:hover {
743 border-color: rgba(22,24,29,0.24);
744}
745
746.pa-disclaimer {
747 font-size: 12px;
748 color: #8a8d99;
749 margin: 2px 0 0;
750 text-align: center;
751 line-height: 1.5;
752}
753
754/* ── Responsive ─────────────────────────────────────────── */
755
756@media (max-width: 820px) {
757 .pa-grid {
758 grid-template-columns: 1fr;
759 }
760 .pa-aside {
761 position: static;
762 }
763}
764
765@media (max-width: 600px) {
766 .pa-main {
767 padding: 32px 20px 60px;
768 }
769 .pa-header {
770 padding: 0 20px;
771 }
772}
773`;