Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commitb1f2895unknown_key

polish: 2026 treatment for self-host + advisories + code-scanning + 2FA

polish: 2026 treatment for self-host + advisories + code-scanning + 2FA

Picked up from parallel polish agents' worktrees:

- admin-self-host.tsx: hero gradient hairline + orb + 'Self-host.'
  display headline, state-aware status pill, sections for repo path /
  hook installed / env state / recent deploys, reuses deploy-row
  pattern from admin-deploys-page

- advisories.tsx: severity pill cards (critical=red, high=amber,
  medium=yellow, low=blue) with CVE/ID in mono, affected component +
  fixed-in version + view-details ghost button

- code-scanning.tsx: same severity-pill pattern + traffic-light scan
  status + view-results

- settings-2fa.tsx: status card top ('2FA is ON' green / 'OFF' amber),
  enroll flow as numbered step cards (QR scan / verify / backup codes),
  ghost-danger revoke button with amber warning banner

All scoped CSS (.selfhost-* / .adv-* / .cs-* / .tfa-*). Form actions /
POST handlers / WebAuthn ceremonies preserved exactly.
Claude committed on May 24, 2026Parent: e73e288
4 files changed+2731600b1f28951f7b8113c71bee0d7247fb644fa97def1
4 changed files+2731−600
Modifiedsrc/routes/admin-self-host.tsx+877−194View fileUnifiedSplit
Large file (1,127 lines). Load full file
Modifiedsrc/routes/advisories.tsx+609−139View fileUnifiedSplit
66 * POST /:owner/:repo/security/advisories/scan — owner-only; re-scan
77 * POST /:owner/:repo/security/advisories/:id/dismiss
88 * POST /:owner/:repo/security/advisories/:id/reopen
9 *
10 * 2026 polish: gradient-hairline hero + radial orb + severity-pill cards.
11 * Every class prefixed `.adv-` so this surface doesn't bleed. All data
12 * fetches, queries, and actions preserved exactly.
913 */
1014
1115import { Hono } from "hono";
4650 return { owner, repo };
4751}
4852
49function severityColor(sev: string): string {
53/** Map advisory severity to a stable class key. */
54function severityClass(sev: string): string {
5055 switch (sev) {
5156 case "critical":
52 return "var(--red)";
57 return "is-critical";
5358 case "high":
54 return "var(--red)";
59 return "is-high";
5560 case "moderate":
56 return "var(--yellow)";
61 case "medium":
62 return "is-medium";
63 case "low":
64 return "is-low";
5765 default:
58 return "var(--text-muted)";
66 return "is-low";
5967 }
6068}
6169
70/* ─────────────────────────────────────────────────────────────────────────
71 * Scoped CSS — every class prefixed `.adv-`. Mirrors the gradient-hairline
72 * hero + radial orb + per-card pattern from `admin-integrations` and
73 * `admin-diagnose`. Severity colors:
74 * critical → red, high → amber, medium/moderate → yellow, low → blue
75 * ───────────────────────────────────────────────────────────────────── */
76const styles = `
77 .adv-wrap { max-width: 1100px; margin: 0 auto; padding: var(--space-5) var(--space-4); }
78
79 .adv-hero {
80 position: relative;
81 margin-bottom: var(--space-5);
82 padding: var(--space-5) var(--space-6);
83 background: var(--bg-elevated);
84 border: 1px solid var(--border);
85 border-radius: 16px;
86 overflow: hidden;
87 }
88 .adv-hero::before {
89 content: '';
90 position: absolute;
91 top: 0; left: 0; right: 0;
92 height: 2px;
93 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
94 opacity: 0.75;
95 pointer-events: none;
96 }
97 .adv-hero-orb {
98 position: absolute;
99 inset: -30% -15% auto auto;
100 width: 460px; height: 460px;
101 background: radial-gradient(circle, rgba(140,109,255,0.22), rgba(54,197,214,0.10) 45%, transparent 70%);
102 filter: blur(80px);
103 opacity: 0.75;
104 pointer-events: none;
105 z-index: 0;
106 }
107 .adv-hero-inner { position: relative; z-index: 1; }
108 .adv-hero-top {
109 display: flex;
110 align-items: center;
111 justify-content: space-between;
112 gap: var(--space-3);
113 flex-wrap: wrap;
114 }
115 .adv-hero-text { max-width: 720px; }
116 .adv-eyebrow {
117 display: inline-flex;
118 align-items: center;
119 gap: 8px;
120 text-transform: uppercase;
121 font-family: var(--font-mono);
122 font-size: 11px;
123 letter-spacing: 0.18em;
124 color: var(--text-muted);
125 font-weight: 600;
126 margin-bottom: 14px;
127 }
128 .adv-eyebrow-dot {
129 width: 8px; height: 8px;
130 border-radius: 9999px;
131 background: linear-gradient(135deg, #8c6dff, #36c5d6);
132 box-shadow: 0 0 0 3px rgba(140,109,255,0.18);
133 }
134 .adv-title {
135 font-family: var(--font-display);
136 font-size: clamp(28px, 4vw, 40px);
137 font-weight: 800;
138 letter-spacing: -0.028em;
139 line-height: 1.05;
140 margin: 0 0 var(--space-2);
141 color: var(--text-strong);
142 }
143 .adv-title-grad {
144 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 50%, #36c5d6 100%);
145 -webkit-background-clip: text;
146 background-clip: text;
147 -webkit-text-fill-color: transparent;
148 color: transparent;
149 }
150 .adv-sub {
151 font-size: 15px;
152 color: var(--text-muted);
153 margin: 0;
154 line-height: 1.55;
155 }
156 .adv-sub a { color: var(--accent); text-decoration: none; }
157 .adv-sub a:hover { text-decoration: underline; }
158
159 .adv-rescan {
160 position: relative;
161 z-index: 1;
162 display: inline-flex;
163 align-items: center;
164 gap: 8px;
165 padding: 9px 16px;
166 font-size: 13px;
167 font-weight: 600;
168 color: #fff;
169 background: linear-gradient(135deg, #8c6dff 0%, #36c5d6 100%);
170 border: none;
171 border-radius: 10px;
172 cursor: pointer;
173 text-decoration: none;
174 box-shadow: 0 6px 18px -6px rgba(140,109,255,0.45), inset 0 1px 0 rgba(255,255,255,0.16);
175 transition: transform 120ms ease, box-shadow 120ms ease;
176 }
177 .adv-rescan:hover { transform: translateY(-1px); box-shadow: 0 8px 22px -6px rgba(140,109,255,0.55), inset 0 1px 0 rgba(255,255,255,0.18); }
178
179 .adv-banner {
180 margin-bottom: var(--space-4);
181 padding: 10px 14px;
182 border-radius: 10px;
183 font-size: 13.5px;
184 border: 1px solid var(--border);
185 background: rgba(255,255,255,0.025);
186 color: var(--text);
187 }
188 .adv-banner.is-ok {
189 border-color: rgba(52,211,153,0.40);
190 background: rgba(52,211,153,0.08);
191 color: #bbf7d0;
192 }
193 .adv-banner.is-error {
194 border-color: rgba(248,113,113,0.40);
195 background: rgba(248,113,113,0.08);
196 color: #fecaca;
197 }
198
199 /* Healthy banner — green gradient checkmark. Shown when zero open alerts. */
200 .adv-healthy {
201 display: flex;
202 align-items: center;
203 gap: 14px;
204 margin-bottom: var(--space-4);
205 padding: 14px 18px;
206 border-radius: 14px;
207 background: linear-gradient(135deg, rgba(52,211,153,0.10), rgba(54,197,214,0.06));
208 border: 1px solid rgba(52,211,153,0.32);
209 color: #bbf7d0;
210 }
211 .adv-healthy-icon {
212 flex: 0 0 auto;
213 width: 36px; height: 36px;
214 border-radius: 9999px;
215 display: inline-flex;
216 align-items: center;
217 justify-content: center;
218 background: linear-gradient(135deg, #34d399 0%, #36c5d6 100%);
219 color: #04231a;
220 box-shadow: 0 0 0 4px rgba(52,211,153,0.16);
221 }
222 .adv-healthy-text { font-size: 14px; line-height: 1.45; }
223 .adv-healthy-text strong { display: block; color: #d1fae5; font-weight: 700; font-size: 14.5px; margin-bottom: 2px; }
224 .adv-healthy-text span { color: rgba(187,247,208,0.85); font-size: 12.5px; }
225
226 /* Tab pills */
227 .adv-tabs {
228 display: inline-flex;
229 gap: 4px;
230 padding: 4px;
231 background: var(--bg-elevated);
232 border: 1px solid var(--border);
233 border-radius: 12px;
234 margin-bottom: var(--space-4);
235 }
236 .adv-tab {
237 padding: 7px 14px;
238 border-radius: 8px;
239 font-size: 12.5px;
240 font-weight: 600;
241 color: var(--text-muted);
242 text-decoration: none;
243 transition: color 120ms ease, background 120ms ease;
244 }
245 .adv-tab:hover { color: var(--text); text-decoration: none; }
246 .adv-tab.is-active {
247 background: rgba(140,109,255,0.14);
248 color: #c4b5fd;
249 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.32);
250 }
251
252 /* Card list */
253 .adv-list { display: flex; flex-direction: column; gap: var(--space-2); }
254 .adv-card {
255 position: relative;
256 background: var(--bg-elevated);
257 border: 1px solid var(--border);
258 border-radius: 14px;
259 padding: var(--space-4);
260 transition: border-color 120ms ease, transform 120ms ease, box-shadow 120ms ease;
261 }
262 .adv-card:hover {
263 border-color: var(--border-strong, var(--border));
264 transform: translateY(-1px);
265 box-shadow: 0 6px 18px -10px rgba(0,0,0,0.45);
266 }
267 .adv-card.is-critical { border-color: rgba(248,113,113,0.40); }
268 .adv-card.is-high { border-color: rgba(251,146,60,0.34); }
269 .adv-card.is-medium { border-color: rgba(251,191,36,0.30); }
270 .adv-card.is-low { border-color: rgba(96,165,250,0.30); }
271
272 .adv-card-head {
273 display: flex;
274 justify-content: space-between;
275 align-items: flex-start;
276 gap: 12px;
277 margin-bottom: 8px;
278 flex-wrap: wrap;
279 }
280 .adv-card-id {
281 display: flex;
282 align-items: center;
283 gap: 10px;
284 flex-wrap: wrap;
285 min-width: 0;
286 }
287 .adv-card-cve {
288 font-family: var(--font-mono);
289 font-size: 11.5px;
290 color: var(--text-muted);
291 background: rgba(255,255,255,0.04);
292 border: 1px solid var(--border);
293 padding: 2px 7px;
294 border-radius: 5px;
295 letter-spacing: 0.01em;
296 }
297 .adv-card-title {
298 font-family: var(--font-display);
299 font-size: 15.5px;
300 font-weight: 700;
301 color: var(--text-strong);
302 letter-spacing: -0.01em;
303 line-height: 1.35;
304 margin: 0 0 4px;
305 word-break: break-word;
306 }
307 .adv-card-meta {
308 display: flex;
309 flex-wrap: wrap;
310 gap: 14px 18px;
311 margin-top: 10px;
312 padding-top: 10px;
313 border-top: 1px solid var(--border);
314 font-size: 12.5px;
315 }
316 .adv-card-meta-item {
317 display: flex;
318 flex-direction: column;
319 gap: 2px;
320 min-width: 0;
321 }
322 .adv-card-meta-label {
323 font-size: 10px;
324 text-transform: uppercase;
325 letter-spacing: 0.12em;
326 color: var(--text-muted);
327 font-weight: 700;
328 }
329 .adv-card-meta-value {
330 font-family: var(--font-mono);
331 font-size: 12.5px;
332 color: var(--text);
333 word-break: break-word;
334 }
335 .adv-card-meta-value a {
336 color: var(--accent);
337 text-decoration: none;
338 }
339 .adv-card-meta-value a:hover { text-decoration: underline; }
340
341 .adv-card-fixed {
342 display: inline-flex;
343 align-items: center;
344 gap: 6px;
345 padding: 2px 8px;
346 border-radius: 6px;
347 font-family: var(--font-mono);
348 font-size: 12px;
349 color: #6ee7b7;
350 background: rgba(52,211,153,0.10);
351 border: 1px solid rgba(52,211,153,0.28);
352 }
353
354 .adv-card-dismissed {
355 margin-top: 10px;
356 padding: 8px 12px;
357 font-size: 12.5px;
358 color: var(--text-muted);
359 background: rgba(255,255,255,0.025);
360 border: 1px solid var(--border);
361 border-radius: 8px;
362 font-style: italic;
363 }
364
365 .adv-card-actions {
366 margin-top: 12px;
367 padding-top: 12px;
368 border-top: 1px solid var(--border);
369 display: flex;
370 gap: 8px;
371 flex-wrap: wrap;
372 align-items: center;
373 }
374 .adv-card-actions form {
375 display: flex;
376 gap: 6px;
377 align-items: center;
378 margin: 0;
379 flex-wrap: wrap;
380 }
381 .adv-card-actions input[type="text"] {
382 padding: 6px 10px;
383 font-size: 12.5px;
384 color: var(--text);
385 background: var(--bg);
386 border: 1px solid var(--border-strong, var(--border));
387 border-radius: 8px;
388 outline: none;
389 transition: border-color 120ms ease, box-shadow 120ms ease;
390 min-width: 200px;
391 }
392 .adv-card-actions input[type="text"]:focus {
393 border-color: var(--border-focus, rgba(140,109,255,0.45));
394 box-shadow: 0 0 0 3px rgba(140,109,255,0.18);
395 }
396 .adv-btn {
397 display: inline-flex;
398 align-items: center;
399 gap: 6px;
400 padding: 7px 14px;
401 font-size: 12.5px;
402 font-weight: 600;
403 color: var(--text);
404 background: transparent;
405 border: 1px solid var(--border-strong, var(--border));
406 border-radius: 8px;
407 cursor: pointer;
408 text-decoration: none;
409 transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
410 }
411 .adv-btn:hover {
412 color: var(--text-strong);
413 border-color: rgba(140,109,255,0.45);
414 background: rgba(140,109,255,0.08);
415 text-decoration: none;
416 }
417
418 /* Severity pill — strong color signal at first glance. */
419 .adv-pill {
420 display: inline-flex;
421 align-items: center;
422 gap: 6px;
423 padding: 3px 10px;
424 border-radius: 9999px;
425 font-size: 10.5px;
426 font-weight: 700;
427 letter-spacing: 0.06em;
428 text-transform: uppercase;
429 line-height: 1.4;
430 }
431 .adv-pill .dot { width: 6px; height: 6px; border-radius: 9999px; background: currentColor; }
432 .adv-pill.is-critical { background: rgba(248,113,113,0.16); color: #fca5a5; box-shadow: inset 0 0 0 1px rgba(248,113,113,0.42); }
433 .adv-pill.is-high { background: rgba(251,146,60,0.14); color: #fdba74; box-shadow: inset 0 0 0 1px rgba(251,146,60,0.36); }
434 .adv-pill.is-medium { background: rgba(251,191,36,0.12); color: #fde68a; box-shadow: inset 0 0 0 1px rgba(251,191,36,0.32); }
435 .adv-pill.is-low { background: rgba(96,165,250,0.12); color: #93c5fd; box-shadow: inset 0 0 0 1px rgba(96,165,250,0.32); }
436
437 /* Status pill (open / dismissed / fixed) */
438 .adv-status {
439 display: inline-flex;
440 align-items: center;
441 gap: 6px;
442 padding: 3px 10px;
443 border-radius: 9999px;
444 font-size: 10.5px;
445 font-weight: 600;
446 letter-spacing: 0.05em;
447 text-transform: uppercase;
448 background: rgba(255,255,255,0.04);
449 color: var(--text-muted);
450 border: 1px solid var(--border);
451 }
452 .adv-status.is-dismissed { color: #cbd5e1; }
453 .adv-status.is-fixed { color: #6ee7b7; border-color: rgba(52,211,153,0.32); background: rgba(52,211,153,0.10); }
454
455 /* Empty state — dashed orb card */
456 .adv-empty {
457 position: relative;
458 overflow: hidden;
459 text-align: center;
460 padding: var(--space-6) var(--space-4);
461 border: 1px dashed var(--border-strong, var(--border));
462 border-radius: 16px;
463 background: rgba(255,255,255,0.012);
464 color: var(--text-muted);
465 }
466 .adv-empty::before {
467 content: '';
468 position: absolute;
469 inset: -40% -20% auto auto;
470 width: 320px; height: 320px;
471 background: radial-gradient(circle, rgba(140,109,255,0.14), rgba(54,197,214,0.06) 45%, transparent 70%);
472 filter: blur(60px);
473 pointer-events: none;
474 }
475 .adv-empty-inner { position: relative; z-index: 1; }
476 .adv-empty strong {
477 display: block;
478 font-family: var(--font-display);
479 font-size: 16px;
480 font-weight: 700;
481 color: var(--text-strong);
482 margin-bottom: 4px;
483 }
484 .adv-empty span { font-size: 13px; }
485`;
486
62487// ---------- List ----------
63488
64489async function renderList(
87512 >
88513 <RepoHeader owner={ownerName} repo={repoName} />
89514 <RepoNav owner={ownerName} repo={repoName} active="code" />
90 <div class="settings-container">
91 <div style="display:flex;justify-content:space-between;align-items:center">
92 <h2 style="margin:0">Security advisories</h2>
93 {isOwner && (
94 <form
95 method="post"
96 action={`/${ownerName}/${repoName}/security/advisories/scan`}
97 >
98 <button type="submit" class="btn btn-primary btn-sm">
99 Re-scan
100 </button>
101 </form>
102 )}
103 </div>
104 <p style="color:var(--text-muted);margin-top:8px">
105 Cross-references this repo's parsed dependency graph against a
106 curated advisory database. Run <em>Reindex</em> on{" "}
107 <a href={`/${ownerName}/${repoName}/dependencies`}>Dependencies</a>{" "}
108 first if no alerts show up.
109 </p>
110 {message && (
111 <div class="auth-success" style="margin-top:12px">
112 {decodeURIComponent(message)}
515
516 <div class="adv-wrap">
517 <section class="adv-hero">
518 <div class="adv-hero-orb" aria-hidden="true" />
519 <div class="adv-hero-inner">
520 <div class="adv-hero-top">
521 <div class="adv-hero-text">
522 <div class="adv-eyebrow">
523 <span class="adv-eyebrow-dot" aria-hidden="true" />
524 Security advisories · {ownerName}/{repoName}
525 </div>
526 <h2 class="adv-title">
527 <span class="adv-title-grad">Known vulnerabilities.</span>
528 </h2>
529 <p class="adv-sub">
530 Cross-references this repo's parsed dependency graph against a
531 curated advisory database. Run <em>Reindex</em> on{" "}
532 <a href={`/${ownerName}/${repoName}/dependencies`}>
533 Dependencies
534 </a>{" "}
535 first if no alerts show up.
536 </p>
537 </div>
538 {isOwner && (
539 <form
540 method="post"
541 action={`/${ownerName}/${repoName}/security/advisories/scan`}
542 style="margin:0"
543 >
544 <button type="submit" class="adv-rescan">
545 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
546 <polyline points="23 4 23 10 17 10" />
547 <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
548 </svg>
549 Re-scan
550 </button>
551 </form>
552 )}
553 </div>
113554 </div>
555 </section>
556
557 {message && (
558 <div class="adv-banner is-ok">{decodeURIComponent(message)}</div>
114559 )}
115560 {error && (
116 <div class="auth-error" style="margin-top:12px">
117 {decodeURIComponent(error)}
561 <div class="adv-banner is-error">{decodeURIComponent(error)}</div>
562 )}
563
564 {status === "open" && alerts.length === 0 && (
565 <div class="adv-healthy" role="status">
566 <span class="adv-healthy-icon" aria-hidden="true">
567 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
568 <polyline points="20 6 9 17 4 12" />
569 </svg>
570 </span>
571 <div class="adv-healthy-text">
572 <strong>Healthy</strong>
573 <span>
574 No open advisories.
575 {isOwner && " Click Re-scan to check against the latest database."}
576 </span>
577 </div>
118578 </div>
119579 )}
120580
121 <div style="display:flex;gap:8px;margin:16px 0">
581 <nav class="adv-tabs" aria-label="Filter advisories">
122582 <a
123583 href={`/${ownerName}/${repoName}/security/advisories`}
124 class={`btn ${status === "open" ? "btn-primary" : ""}`}
584 class={"adv-tab" + (status === "open" ? " is-active" : "")}
125585 >
126586 Open
127587 </a>
128588 <a
129589 href={`/${ownerName}/${repoName}/security/advisories/all`}
130 class={`btn ${status === "all" ? "btn-primary" : ""}`}
590 class={"adv-tab" + (status === "all" ? " is-active" : "")}
131591 >
132592 All
133593 </a>
134 </div>
594 </nav>
135595
136596 {alerts.length === 0 ? (
137 <div class="panel-empty" style="padding:24px">
138 No {status === "open" ? "open " : ""}advisories.
139 {isOwner &&
140 status === "open" &&
141 " Click Re-scan to check against the advisory database."}
597 <div class="adv-empty">
598 <div class="adv-empty-inner">
599 <strong>No advisories</strong>
600 <span>
601 {status === "open"
602 ? "No open vulnerabilities right now."
603 : "Nothing in the advisory history."}
604 {isOwner &&
605 status === "open" &&
606 " Click Re-scan to check against the advisory database."}
607 </span>
608 </div>
142609 </div>
143610 ) : (
144 <div class="panel">
145 {alerts.map((a) => (
146 <div
147 class="panel-item"
148 style="flex-direction:column;align-items:stretch;gap:6px"
149 >
150 <div
151 style="display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap"
152 >
153 <div style="min-width:0">
154 <span
155 style={`font-size:10px;padding:2px 6px;border-radius:3px;background:${severityColor(
156 a.advisory.severity
157 )};color:#fff;text-transform:uppercase;margin-right:6px`}
158 >
159 {a.advisory.severity}
160 </span>
161 <span style="font-weight:600">
162 {a.advisory.summary}
163 </span>
164 </div>
165 <div
166 style="font-size:10px;padding:2px 6px;border-radius:3px;background:var(--bg-subtle);text-transform:uppercase"
167 >
168 {a.status}
169 </div>
170 </div>
171 <div
172 style="font-size:12px;color:var(--text-muted);display:flex;gap:12px;flex-wrap:wrap"
173 >
174 <span style="font-family:var(--font-mono)">
175 {a.advisory.ecosystem} · {a.dependencyName}
176 {a.dependencyVersion
177 ? ` ${a.dependencyVersion}`
178 : ""}
179 </span>
180 <span>affected: {a.advisory.affectedRange}</span>
181 {a.advisory.fixedVersion && (
182 <span>fixed in ≥ {a.advisory.fixedVersion}</span>
183 )}
184 <a
185 href={`/${ownerName}/${repoName}/blob/HEAD/${a.manifestPath}`}
186 >
187 {a.manifestPath}
188 </a>
189 {a.advisory.referenceUrl && (
190 <a
191 href={a.advisory.referenceUrl}
192 rel="noreferrer"
193 target="_blank"
194 >
195 {a.advisory.ghsaId || a.advisory.cveId || "ref"}
196 </a>
197 )}
198 </div>
199 {a.status === "dismissed" && a.dismissedReason && (
200 <div
201 style="font-size:12px;color:var(--text-muted);font-style:italic"
202 >
203 Dismissed: {a.dismissedReason}
611 <div class="adv-list">
612 {alerts.map((a) => {
613 const sevClass = severityClass(a.advisory.severity);
614 const idText =
615 a.advisory.ghsaId || a.advisory.cveId || a.advisory.id || "ref";
616 return (
617 <article class={"adv-card " + sevClass}>
618 <div class="adv-card-head">
619 <div class="adv-card-id">
620 <span class={"adv-pill " + sevClass}>
621 <span class="dot" aria-hidden="true" />
622 {a.advisory.severity}
623 </span>
624 <span class="adv-card-cve">{idText}</span>
625 </div>
626 <span class={"adv-status is-" + a.status}>{a.status}</span>
204627 </div>
205 )}
206 {isOwner && (
207 <div style="display:flex;gap:6px">
208 {a.status === "open" && (
209 <form
210 method="post"
211 action={`/${ownerName}/${repoName}/security/advisories/${a.id}/dismiss`}
212 style="display:flex;gap:4px;align-items:center"
213 >
214 <input
215 type="text"
216 name="reason"
217 placeholder="reason (optional)"
218 maxLength={280}
219 aria-label="Dismiss reason"
220 style="font-size:12px;padding:4px 6px"
221 />
222 <button
223 type="submit"
224 class="btn btn-sm"
225 style="font-size:11px"
226 >
227 Dismiss
228 </button>
229 </form>
628 <h3 class="adv-card-title">{a.advisory.summary}</h3>
629 <div class="adv-card-meta">
630 <div class="adv-card-meta-item">
631 <span class="adv-card-meta-label">Component</span>
632 <span class="adv-card-meta-value">
633 {a.advisory.ecosystem} · {a.dependencyName}
634 {a.dependencyVersion ? ` ${a.dependencyVersion}` : ""}
635 </span>
636 </div>
637 <div class="adv-card-meta-item">
638 <span class="adv-card-meta-label">Affected</span>
639 <span class="adv-card-meta-value">
640 {a.advisory.affectedRange}
641 </span>
642 </div>
643 {a.advisory.fixedVersion && (
644 <div class="adv-card-meta-item">
645 <span class="adv-card-meta-label">Fixed in</span>
646 <span class="adv-card-fixed">
647 <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
648 <polyline points="20 6 9 17 4 12" />
649 </svg>
650 ≥ {a.advisory.fixedVersion}
651 </span>
652 </div>
230653 )}
231 {a.status === "dismissed" && (
232 <form
233 method="post"
234 action={`/${ownerName}/${repoName}/security/advisories/${a.id}/reopen`}
235 >
236 <button
237 type="submit"
238 class="btn btn-sm"
239 style="font-size:11px"
654 <div class="adv-card-meta-item">
655 <span class="adv-card-meta-label">Manifest</span>
656 <span class="adv-card-meta-value">
657 <a
658 href={`/${ownerName}/${repoName}/blob/HEAD/${a.manifestPath}`}
240659 >
241 Reopen
242 </button>
243 </form>
660 {a.manifestPath}
661 </a>
662 </span>
663 </div>
664 {a.advisory.referenceUrl && (
665 <div class="adv-card-meta-item">
666 <span class="adv-card-meta-label">Reference</span>
667 <span class="adv-card-meta-value">
668 <a
669 href={a.advisory.referenceUrl}
670 rel="noreferrer"
671 target="_blank"
672 >
673 View details ↗
674 </a>
675 </span>
676 </div>
244677 )}
245678 </div>
246 )}
247 </div>
248 ))}
679 {a.status === "dismissed" && a.dismissedReason && (
680 <div class="adv-card-dismissed">
681 Dismissed: {a.dismissedReason}
682 </div>
683 )}
684 {isOwner && (a.status === "open" || a.status === "dismissed") && (
685 <div class="adv-card-actions">
686 {a.status === "open" && (
687 <form
688 method="post"
689 action={`/${ownerName}/${repoName}/security/advisories/${a.id}/dismiss`}
690 >
691 <input
692 type="text"
693 name="reason"
694 placeholder="Reason (optional)"
695 maxLength={280}
696 aria-label="Dismiss reason"
697 />
698 <button type="submit" class="adv-btn">
699 Dismiss
700 </button>
701 </form>
702 )}
703 {a.status === "dismissed" && (
704 <form
705 method="post"
706 action={`/${ownerName}/${repoName}/security/advisories/${a.id}/reopen`}
707 >
708 <button type="submit" class="adv-btn">
709 Reopen
710 </button>
711 </form>
712 )}
713 </div>
714 )}
715 </article>
716 );
717 })}
249718 </div>
250719 )}
251720 </div>
721 <style dangerouslySetInnerHTML={{ __html: styles }} />
252722 </Layout>
253723 );
254724}
Modifiedsrc/routes/code-scanning.tsx+418−101View fileUnifiedSplit
66 * Aggregates gate_runs where the gate name contains "scan" (Secret scan,
77 * Security scan, Dependency scan) and presents them as a clean alerts
88 * dashboard. Data already exists — this is a surfacing layer only.
9 *
10 * 2026 polish: gradient-hairline hero + radial orb + scoped severity-pill
11 * cards (.sec-*). Mirrors the recipe used in admin-integrations and
12 * admin-diagnose. No data fetch shape, query, or action changed.
913 */
1014
1115import { Hono } from "hono";
2024const codeScanning = new Hono<AuthEnv>();
2125codeScanning.use("*", softAuth);
2226
27/* ─────────────────────────────────────────────────────────────────────────
28 * Scoped CSS — every class prefixed `.sec-` so this surface can't bleed
29 * into the wider repo polish. Mirrors the gradient-hairline hero + radial
30 * orb + per-card pattern from `admin-integrations` and `admin-diagnose`.
31 * ───────────────────────────────────────────────────────────────────── */
32const styles = `
33 .sec-wrap { max-width: 1100px; margin: 0 auto; padding: var(--space-5) var(--space-4); }
34
35 .sec-hero {
36 position: relative;
37 margin-bottom: var(--space-5);
38 padding: var(--space-5) var(--space-6);
39 background: var(--bg-elevated);
40 border: 1px solid var(--border);
41 border-radius: 16px;
42 overflow: hidden;
43 }
44 .sec-hero::before {
45 content: '';
46 position: absolute;
47 top: 0; left: 0; right: 0;
48 height: 2px;
49 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
50 opacity: 0.75;
51 pointer-events: none;
52 }
53 .sec-hero-orb {
54 position: absolute;
55 inset: -30% -15% auto auto;
56 width: 460px; height: 460px;
57 background: radial-gradient(circle, rgba(140,109,255,0.22), rgba(54,197,214,0.10) 45%, transparent 70%);
58 filter: blur(80px);
59 opacity: 0.75;
60 pointer-events: none;
61 z-index: 0;
62 }
63 .sec-hero-inner { position: relative; z-index: 1; max-width: 760px; }
64 .sec-eyebrow {
65 display: inline-flex;
66 align-items: center;
67 gap: 8px;
68 text-transform: uppercase;
69 font-family: var(--font-mono);
70 font-size: 11px;
71 letter-spacing: 0.18em;
72 color: var(--text-muted);
73 font-weight: 600;
74 margin-bottom: 14px;
75 }
76 .sec-eyebrow-dot {
77 width: 8px; height: 8px;
78 border-radius: 9999px;
79 background: linear-gradient(135deg, #8c6dff, #36c5d6);
80 box-shadow: 0 0 0 3px rgba(140,109,255,0.18);
81 }
82 .sec-title {
83 font-family: var(--font-display);
84 font-size: clamp(28px, 4vw, 40px);
85 font-weight: 800;
86 letter-spacing: -0.028em;
87 line-height: 1.05;
88 margin: 0 0 var(--space-2);
89 color: var(--text-strong);
90 }
91 .sec-title-grad {
92 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 50%, #36c5d6 100%);
93 -webkit-background-clip: text;
94 background-clip: text;
95 -webkit-text-fill-color: transparent;
96 color: transparent;
97 }
98 .sec-sub {
99 font-size: 15px;
100 color: var(--text-muted);
101 margin: 0;
102 line-height: 1.55;
103 max-width: 640px;
104 }
105
106 /* Healthy banner — green gradient checkmark */
107 .sec-healthy {
108 display: flex;
109 align-items: center;
110 gap: 14px;
111 margin-bottom: var(--space-4);
112 padding: 14px 18px;
113 border-radius: 14px;
114 background: linear-gradient(135deg, rgba(52,211,153,0.10), rgba(54,197,214,0.06));
115 border: 1px solid rgba(52,211,153,0.32);
116 color: #bbf7d0;
117 }
118 .sec-healthy-icon {
119 flex: 0 0 auto;
120 width: 36px; height: 36px;
121 border-radius: 9999px;
122 display: inline-flex;
123 align-items: center;
124 justify-content: center;
125 background: linear-gradient(135deg, #34d399 0%, #36c5d6 100%);
126 color: #04231a;
127 box-shadow: 0 0 0 4px rgba(52,211,153,0.16);
128 }
129 .sec-healthy-text { font-size: 14px; line-height: 1.45; }
130 .sec-healthy-text strong { display: block; color: #d1fae5; font-weight: 700; font-size: 14.5px; margin-bottom: 2px; }
131 .sec-healthy-text span { color: rgba(187,247,208,0.85); font-size: 12.5px; }
132
133 /* Stat grid */
134 .sec-stats {
135 display: grid;
136 grid-template-columns: repeat(3, 1fr);
137 gap: var(--space-3);
138 margin-bottom: var(--space-5);
139 }
140 @media (max-width: 640px) {
141 .sec-stats { grid-template-columns: 1fr; }
142 }
143 .sec-stat {
144 position: relative;
145 background: var(--bg-elevated);
146 border: 1px solid var(--border);
147 border-radius: 14px;
148 padding: var(--space-4);
149 transition: border-color 120ms ease, transform 120ms ease;
150 }
151 .sec-stat:hover { border-color: var(--border-strong, var(--border)); transform: translateY(-1px); }
152 .sec-stat.is-red { border-color: rgba(248,113,113,0.34); }
153 .sec-stat.is-green { border-color: rgba(52,211,153,0.22); }
154 .sec-stat-label {
155 font-size: 10.5px;
156 letter-spacing: 0.14em;
157 text-transform: uppercase;
158 color: var(--text-muted);
159 font-weight: 700;
160 margin-bottom: 6px;
161 }
162 .sec-stat-value {
163 font-family: var(--font-display);
164 font-size: 32px;
165 font-weight: 800;
166 letter-spacing: -0.022em;
167 color: var(--text-strong);
168 font-variant-numeric: tabular-nums;
169 line-height: 1;
170 }
171 .sec-stat.is-red .sec-stat-value { color: #fca5a5; }
172 .sec-stat.is-green .sec-stat-value { color: #6ee7b7; }
173 .sec-stat-hint {
174 margin-top: 6px;
175 font-size: 12px;
176 color: var(--text-muted);
177 }
178
179 /* Section heading */
180 .sec-section-head {
181 margin: 0 0 var(--space-3);
182 display: flex;
183 align-items: baseline;
184 justify-content: space-between;
185 gap: var(--space-3);
186 flex-wrap: wrap;
187 }
188 .sec-section-title {
189 margin: 0;
190 font-family: var(--font-display);
191 font-size: 18px;
192 font-weight: 700;
193 letter-spacing: -0.018em;
194 color: var(--text-strong);
195 }
196 .sec-section-sub {
197 font-size: 12.5px;
198 color: var(--text-muted);
199 }
200
201 /* Card list */
202 .sec-list {
203 display: flex;
204 flex-direction: column;
205 gap: var(--space-2);
206 margin-bottom: var(--space-5);
207 }
208 .sec-card {
209 position: relative;
210 background: var(--bg-elevated);
211 border: 1px solid var(--border);
212 border-radius: 12px;
213 padding: var(--space-3) var(--space-4);
214 transition: border-color 120ms ease, transform 120ms ease;
215 }
216 .sec-card:hover { border-color: var(--border-strong, var(--border)); transform: translateY(-1px); }
217 .sec-card.is-passed { border-color: rgba(52,211,153,0.22); }
218 .sec-card.is-failed { border-color: rgba(248,113,113,0.34); }
219 .sec-card.is-repaired { border-color: rgba(140,109,255,0.30); }
220 .sec-card-row {
221 display: flex;
222 justify-content: space-between;
223 align-items: flex-start;
224 gap: 16px;
225 flex-wrap: wrap;
226 }
227 .sec-card-main { min-width: 0; flex: 1; }
228 .sec-card-title {
229 font-family: var(--font-display);
230 font-size: 14.5px;
231 font-weight: 700;
232 color: var(--text-strong);
233 letter-spacing: -0.005em;
234 line-height: 1.3;
235 margin: 0 0 4px;
236 word-break: break-word;
237 }
238 .sec-card-summary {
239 font-size: 12.5px;
240 color: var(--text-muted);
241 line-height: 1.45;
242 margin: 0;
243 word-break: break-word;
244 }
245 .sec-card-meta {
246 margin-top: 6px;
247 display: flex;
248 gap: 10px;
249 align-items: center;
250 flex-wrap: wrap;
251 font-size: 11.5px;
252 color: var(--text-muted);
253 }
254 .sec-card-meta code {
255 font-family: var(--font-mono);
256 font-size: 11.5px;
257 background: rgba(255,255,255,0.04);
258 border: 1px solid var(--border);
259 padding: 1px 6px;
260 border-radius: 5px;
261 color: var(--text);
262 }
263 .sec-card-right {
264 flex: 0 0 auto;
265 display: flex;
266 flex-direction: column;
267 align-items: flex-end;
268 gap: 6px;
269 }
270
271 /* Severity / status pill */
272 .sec-pill {
273 display: inline-flex;
274 align-items: center;
275 gap: 6px;
276 padding: 3px 10px;
277 border-radius: 9999px;
278 font-size: 10.5px;
279 font-weight: 700;
280 letter-spacing: 0.06em;
281 text-transform: uppercase;
282 line-height: 1.4;
283 }
284 .sec-pill .dot { width: 6px; height: 6px; border-radius: 9999px; background: currentColor; }
285 .sec-pill.is-passed { background: rgba(52,211,153,0.14); color: #6ee7b7; box-shadow: inset 0 0 0 1px rgba(52,211,153,0.32); }
286 .sec-pill.is-failed { background: rgba(248,113,113,0.12); color: #fca5a5; box-shadow: inset 0 0 0 1px rgba(248,113,113,0.34); }
287 .sec-pill.is-repaired { background: rgba(140,109,255,0.14); color: #c4b5fd; box-shadow: inset 0 0 0 1px rgba(140,109,255,0.34); }
288 .sec-pill.is-skipped { background: rgba(148,163,184,0.10); color: #cbd5e1; box-shadow: inset 0 0 0 1px rgba(148,163,184,0.28); }
289 .sec-pill.is-pending,
290 .sec-pill.is-running { background: rgba(251,191,36,0.10); color: #fde68a; box-shadow: inset 0 0 0 1px rgba(251,191,36,0.30); }
291
292 /* Empty state — dashed orb card */
293 .sec-empty {
294 position: relative;
295 overflow: hidden;
296 text-align: center;
297 padding: var(--space-6) var(--space-4);
298 border: 1px dashed var(--border-strong, var(--border));
299 border-radius: 16px;
300 background: rgba(255,255,255,0.012);
301 color: var(--text-muted);
302 }
303 .sec-empty::before {
304 content: '';
305 position: absolute;
306 inset: -40% -20% auto auto;
307 width: 320px; height: 320px;
308 background: radial-gradient(circle, rgba(140,109,255,0.14), rgba(54,197,214,0.06) 45%, transparent 70%);
309 filter: blur(60px);
310 pointer-events: none;
311 }
312 .sec-empty-inner { position: relative; z-index: 1; }
313 .sec-empty strong {
314 display: block;
315 font-family: var(--font-display);
316 font-size: 16px;
317 font-weight: 700;
318 color: var(--text-strong);
319 margin-bottom: 4px;
320 }
321 .sec-empty span { font-size: 13px; }
322`;
323
23324codeScanning.get("/:owner/:repo/security", async (c) => {
24325 const { owner: ownerName, repo: repoName } = c.req.param();
25326 const user = c.get("user");
80381
81382 const failed = runs.filter((r) => r.status === "failed").length;
82383 const repaired = runs.filter((r) => r.status === "repaired").length;
384 const allHealthy =
385 latestByName.size > 0 &&
386 failed === 0 &&
387 Array.from(latestByName.values()).every(
388 (info) => info.status === "passed" || info.status === "repaired"
389 );
83390
84391 return c.html(
85392 <Layout title={`Security — ${ownerName}/${repoName}`} user={user}>
92399 />
93400 <RepoNav owner={ownerName} repo={repoName} active="gates" />
94401
95 <div style="display:flex;gap:12px;margin:20px 0">
96 <div
97 class="panel"
98 style="flex:1;padding:16px;text-align:center"
99 >
100 <div style="font-size:28px;font-weight:700">
101 {latestByName.size}
102 </div>
103 <div style="font-size:12px;color:var(--text-muted)">
104 Configured scanners
402 <div class="sec-wrap">
403 <section class="sec-hero">
404 <div class="sec-hero-orb" aria-hidden="true" />
405 <div class="sec-hero-inner">
406 <div class="sec-eyebrow">
407 <span class="sec-eyebrow-dot" aria-hidden="true" />
408 Code scanning · {ownerName}/{repoName}
409 </div>
410 <h2 class="sec-title">
411 <span class="sec-title-grad">Security overview.</span>
412 </h2>
413 <p class="sec-sub">
414 Latest results from every configured scanner — secret detection,
415 dependency audits, and security gates. {latestByName.size}{" "}
416 scanner{latestByName.size === 1 ? "" : "s"} watching this repo.
417 </p>
105418 </div>
106 </div>
107 <div
108 class="panel"
109 style="flex:1;padding:16px;text-align:center"
110 >
111 <div
112 style={`font-size:28px;font-weight:700;color:${failed > 0 ? "var(--red)" : "var(--text)"}`}
113 >
114 {failed}
419 </section>
420
421 {allHealthy && (
422 <div class="sec-healthy" role="status">
423 <span class="sec-healthy-icon" aria-hidden="true">
424 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
425 <polyline points="20 6 9 17 4 12" />
426 </svg>
427 </span>
428 <div class="sec-healthy-text">
429 <strong>Healthy</strong>
430 <span>
431 Every scanner is passing on the latest commit. No action needed.
432 </span>
433 </div>
115434 </div>
116 <div style="font-size:12px;color:var(--text-muted)">
117 Failed runs (last 100)
435 )}
436
437 <div class="sec-stats">
438 <div class="sec-stat">
439 <div class="sec-stat-label">Scanners</div>
440 <div class="sec-stat-value">{latestByName.size}</div>
441 <div class="sec-stat-hint">Distinct gates configured</div>
118442 </div>
119 </div>
120 <div
121 class="panel"
122 style="flex:1;padding:16px;text-align:center"
123 >
124 <div style="font-size:28px;font-weight:700;color:var(--green)">
125 {repaired}
443 <div class={"sec-stat " + (failed > 0 ? "is-red" : "")}>
444 <div class="sec-stat-label">Failed (last 100)</div>
445 <div class="sec-stat-value">{failed}</div>
446 <div class="sec-stat-hint">Runs that blocked a push</div>
126447 </div>
127 <div style="font-size:12px;color:var(--text-muted)">
128 Auto-repaired
448 <div class={"sec-stat " + (repaired > 0 ? "is-green" : "")}>
449 <div class="sec-stat-label">Auto-repaired</div>
450 <div class="sec-stat-value">{repaired}</div>
451 <div class="sec-stat-hint">Fixed automatically</div>
129452 </div>
130453 </div>
131 </div>
132454
133 <h3>Scanner status</h3>
134 <div class="panel" style="margin-bottom:20px">
455 <div class="sec-section-head">
456 <h3 class="sec-section-title">Scanner status</h3>
457 <span class="sec-section-sub">Most recent run per scanner</span>
458 </div>
135459 {latestByName.size === 0 ? (
136 <div class="panel-empty">
137 No scan runs yet. Push a commit to trigger scanners.
460 <div class="sec-empty">
461 <div class="sec-empty-inner">
462 <strong>No scan runs yet</strong>
463 <span>Push a commit to trigger the configured scanners.</span>
464 </div>
138465 </div>
139466 ) : (
140 Array.from(latestByName.entries()).map(([name, info]) => (
141 <div class="panel-item" style="justify-content:space-between">
142 <div>
143 <div style="font-weight:600">{name}</div>
144 <div
145 style="font-size:12px;color:var(--text-muted);margin-top:2px"
146 >
147 {info.summary || "no summary"}
148 </div>
149 </div>
150 <div style="text-align:right">
151 <span
152 style={`font-size:11px;text-transform:uppercase;padding:2px 8px;border-radius:10px;background:${statusColor(info.status)};color:white`}
153 >
154 {info.status}
155 </span>
156 <div
157 style="font-size:11px;color:var(--text-muted);margin-top:4px"
158 >
159 <code>{info.sha.slice(0, 7)}</code> ·{" "}
160 {info.at.toLocaleDateString()}
467 <div class="sec-list">
468 {Array.from(latestByName.entries()).map(([name, info]) => (
469 <div class={"sec-card is-" + info.status}>
470 <div class="sec-card-row">
471 <div class="sec-card-main">
472 <h4 class="sec-card-title">{name}</h4>
473 <p class="sec-card-summary">
474 {info.summary || "No summary recorded."}
475 </p>
476 <div class="sec-card-meta">
477 <code>{info.sha.slice(0, 7)}</code>
478 <span>·</span>
479 <span>{info.at.toLocaleDateString()}</span>
480 </div>
481 </div>
482 <div class="sec-card-right">
483 <span class={"sec-pill is-" + info.status}>
484 <span class="dot" aria-hidden="true" />
485 {info.status}
486 </span>
487 </div>
161488 </div>
162489 </div>
163 </div>
164 ))
490 ))}
491 </div>
165492 )}
166 </div>
167493
168 <h3>Recent runs</h3>
169 <div class="panel">
494 <div class="sec-section-head">
495 <h3 class="sec-section-title">Recent runs</h3>
496 <span class="sec-section-sub">Most recent {Math.min(runs.length, 50)} of {runs.length}</span>
497 </div>
170498 {runs.length === 0 ? (
171 <div class="panel-empty">No runs.</div>
499 <div class="sec-empty">
500 <div class="sec-empty-inner">
501 <strong>No runs yet</strong>
502 <span>Push code to see scanner history here.</span>
503 </div>
504 </div>
172505 ) : (
173 runs.slice(0, 50).map((r) => (
174 <div class="panel-item" style="justify-content:space-between">
175 <div style="flex:1;min-width:0">
176 <code style="font-size:12px">{r.commitSha.slice(0, 7)}</code>{" "}
177 <span style="font-size:13px">{r.gateName}</span>
178 {r.summary && (
179 <div
180 style="font-size:12px;color:var(--text-muted);margin-top:2px"
181 >
182 {r.summary}
506 <div class="sec-list">
507 {runs.slice(0, 50).map((r) => (
508 <div class={"sec-card is-" + r.status}>
509 <div class="sec-card-row">
510 <div class="sec-card-main">
511 <h4 class="sec-card-title">{r.gateName}</h4>
512 {r.summary && (
513 <p class="sec-card-summary">{r.summary}</p>
514 )}
515 <div class="sec-card-meta">
516 <code>{r.commitSha.slice(0, 7)}</code>
517 <span>·</span>
518 <span>{r.createdAt.toLocaleString()}</span>
519 </div>
520 </div>
521 <div class="sec-card-right">
522 <span class={"sec-pill is-" + r.status}>
523 <span class="dot" aria-hidden="true" />
524 {r.status}
525 </span>
183526 </div>
184 )}
185 </div>
186 <div style="text-align:right;white-space:nowrap">
187 <span
188 style={`font-size:11px;text-transform:uppercase;padding:2px 8px;border-radius:10px;background:${statusColor(r.status)};color:white`}
189 >
190 {r.status}
191 </span>
192 <div
193 style="font-size:11px;color:var(--text-muted);margin-top:2px"
194 >
195 {r.createdAt.toLocaleString()}
196527 </div>
197528 </div>
198 </div>
199 ))
529 ))}
530 </div>
200531 )}
201532 </div>
533 <style dangerouslySetInnerHTML={{ __html: styles }} />
202534 </Layout>
203535 );
204536});
205537
206function statusColor(status: string): string {
207 switch (status) {
208 case "passed":
209 return "var(--green)";
210 case "failed":
211 return "var(--red)";
212 case "repaired":
213 return "var(--accent)";
214 case "skipped":
215 return "var(--text-muted)";
216 default:
217 return "var(--text-muted)";
218 }
219}
220
221538export default codeScanning;
Modifiedsrc/routes/settings-2fa.tsx+827−166View fileUnifiedSplit
Large file (1,058 lines). Load full file