@mneme-ai/xray 2.163.0 → 2.173.0
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/clone.d.ts +2 -7
- package/dist/clone.d.ts.map +1 -1
- package/dist/clone.js +15 -2
- package/dist/clone.js.map +1 -1
- package/dist/engine.js +2 -2
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +98 -1
- package/dist/server.js.map +1 -1
- package/dist/track.d.ts +59 -0
- package/dist/track.d.ts.map +1 -0
- package/dist/track.js +182 -0
- package/dist/track.js.map +1 -0
- package/dist/tracker_server.d.ts +94 -0
- package/dist/tracker_server.d.ts.map +1 -0
- package/dist/tracker_server.js +235 -0
- package/dist/tracker_server.js.map +1 -0
- package/dist/triage.d.ts +66 -0
- package/dist/triage.d.ts.map +1 -0
- package/dist/triage.js +171 -0
- package/dist/triage.js.map +1 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/public/card.js +40 -4
- package/public/index.html +235 -19
- package/public/local-scan.js +104 -0
- package/public/report.html +14 -2
package/public/index.html
CHANGED
|
@@ -66,11 +66,25 @@
|
|
|
66
66
|
.g-C{background:linear-gradient(135deg,#eaa83a,#c9821a)} .g-D{background:linear-gradient(135deg,#f0742e,#d4571a)}
|
|
67
67
|
.g-F{background:linear-gradient(135deg,#f43f5e,#be123c)}
|
|
68
68
|
.top .repo{font-size:22px;font-weight:640;letter-spacing:-.02em;color:var(--ink);word-break:break-all;line-height:1.2}
|
|
69
|
+
.top .repobr{font-size:13px;font-weight:600;color:var(--a);background:var(--a-soft);border-radius:6px;padding:1px 7px;vertical-align:middle}
|
|
70
|
+
.top .repourl{display:inline-block;font-size:12.5px;color:var(--a);text-decoration:none;margin-top:3px;word-break:break-all;font-family:ui-monospace,Menlo,monospace}
|
|
71
|
+
.top .repourl:hover{text-decoration:underline}
|
|
69
72
|
.top .head{color:var(--sub);font-size:14px;margin-top:4px}
|
|
70
73
|
.trustbar{display:flex;align-items:center;gap:14px;padding:13px 30px;background:#f0fdf4;border-bottom:1px solid #dcfce7;flex-wrap:wrap}
|
|
71
74
|
.hgauge{display:inline-flex;align-items:center;gap:8px;font-weight:680;color:#15803d;font-size:14px;white-space:nowrap}
|
|
72
75
|
.hdot{width:10px;height:10px;border-radius:50%;background:#16a34a;box-shadow:0 0 0 4px rgba(22,163,74,.16)}
|
|
73
76
|
.htext{font-size:12.5px;color:#3f6b4e;line-height:1.5}.htext b{color:#14532d;font-weight:640}
|
|
77
|
+
.membrane{display:flex;border-bottom:1px solid #eef0f2;background:#fafbfc;flex-wrap:wrap}
|
|
78
|
+
.mp{flex:1;min-width:180px;padding:11px 18px;border-right:1px solid #eef0f2;display:flex;flex-direction:column;gap:3px}.mp:last-child{border-right:0}
|
|
79
|
+
.mpk{font-size:10.5px;letter-spacing:.06em;color:#8a909a;font-weight:700}
|
|
80
|
+
.mpv{font-size:12.5px;color:#2b2f36;font-weight:560;display:flex;align-items:center;gap:7px}
|
|
81
|
+
.triage{padding:14px 30px 4px}
|
|
82
|
+
.triage-h{font-size:13px;font-weight:680;color:#b45309;margin-bottom:9px}.triage-sub{font-weight:400;color:#9aa0a6;font-size:11.5px}
|
|
83
|
+
.triage.clearall{padding:13px 30px;color:#15803d;font-weight:560;font-size:13px;background:#f0fdf4;border-bottom:1px solid #dcfce7}
|
|
84
|
+
.ti{display:flex;gap:11px;align-items:flex-start;padding:8px 0;border-top:1px solid #f1f2f4}
|
|
85
|
+
.ti-sev{flex:0 0 auto;font-size:10px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;padding:3px 8px;border-radius:6px;min-width:62px;text-align:center}
|
|
86
|
+
.ti.critical .ti-sev{background:#fee2e2;color:#b91c1c}.ti.warn .ti-sev{background:#fef3c7;color:#b45309}.ti.info .ti-sev{background:#e0e7ff;color:#4338ca}
|
|
87
|
+
.ti-body{flex:1;min-width:0}.ti-f{font-size:13px;color:#1f2329}.ti-p{font-size:11px;color:#9aa0a6;margin-top:2px;font-family:ui-monospace,Menlo,monospace}
|
|
74
88
|
.rows{padding:6px 30px 16px}
|
|
75
89
|
.row{display:flex;gap:18px;padding:17px 0;border-bottom:1px solid var(--line2);align-items:baseline}
|
|
76
90
|
.row:last-child{border-bottom:0}
|
|
@@ -81,7 +95,8 @@
|
|
|
81
95
|
.row .v .big{font-weight:660;color:var(--ink);font-size:16px}
|
|
82
96
|
.muted{color:var(--sub)}
|
|
83
97
|
.spark{font-family:ui-monospace,Menlo,monospace;letter-spacing:1px;color:var(--a)}
|
|
84
|
-
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
|
|
98
|
+
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px;max-height:148px;overflow-y:auto}
|
|
99
|
+
.chips::-webkit-scrollbar{width:8px}.chips::-webkit-scrollbar-thumb{background:#d7dbe0;border-radius:8px}
|
|
85
100
|
.chip{font-size:12px;padding:4px 10px;border-radius:8px;background:var(--soft);border:1px solid var(--line);color:#5c616b;font-weight:450}
|
|
86
101
|
.chip.bad{background:#fff1f3;border-color:#ffe0e6;color:var(--red)}
|
|
87
102
|
.chip.warn{background:#fff8ed;border-color:#fde6cc;color:#b45309}
|
|
@@ -151,14 +166,43 @@
|
|
|
151
166
|
.paste-hint{align-self:center;font-size:12.5px;color:var(--sub)}
|
|
152
167
|
.paste-hint b{color:var(--ink2);font-weight:560}
|
|
153
168
|
/* local bridge panel */
|
|
154
|
-
.localbox{max-width:560px;margin:22px auto 0;border:1px solid var(--line);border-radius:var(--rs);padding:18px 20px;background:var(--soft);display:
|
|
155
|
-
.localbox.on{display:block}
|
|
169
|
+
.localbox{max-width:560px;margin:22px auto 0;border:1px solid var(--line);border-radius:var(--rs);padding:18px 20px;background:var(--soft);display:block;text-align:left}
|
|
156
170
|
.localbox .lh{display:flex;align-items:center;gap:9px;font-size:13.5px;font-weight:560;color:var(--ink2)}
|
|
157
|
-
.localdot{width:9px;height:9px;border-radius:50%;background
|
|
171
|
+
.localdot{width:9px;height:9px;border-radius:50%;background:#c8ccd2;box-shadow:0 0 0 3px rgba(140,140,150,.12)}
|
|
172
|
+
.localbox.connected .localdot{background:var(--green);box-shadow:0 0 0 3px rgba(22,163,74,.14)}
|
|
173
|
+
.pickbtn{height:44px;padding:0 18px;border:1.5px solid var(--a);border-radius:10px;background:var(--a);color:#fff;cursor:pointer;font-size:13.5px;font-weight:600}
|
|
174
|
+
.cmdrow{display:flex;gap:8px;align-items:center;margin-top:8px;flex-wrap:wrap}
|
|
175
|
+
.cmdrow code{background:#0b0b0f;color:#e6e6ea;border-radius:8px;padding:8px 12px;font-size:12.5px;font-family:ui-monospace,Menlo,monospace}
|
|
176
|
+
.orline{display:flex;align-items:center;gap:10px;margin:14px 0;color:var(--sub);font-size:12px}
|
|
177
|
+
.orline::before,.orline::after{content:"";flex:1;height:1px;background:var(--line)}
|
|
158
178
|
.localrow{display:flex;gap:8px;margin-top:12px}
|
|
159
179
|
.localrow input{flex:1;height:44px;padding:0 14px;border:1px solid var(--line);border-radius:10px;font-size:13.5px;font-family:ui-monospace,Menlo,monospace}
|
|
160
180
|
.localrow button{height:44px;padding:0 16px;border:0;border-radius:10px;background:var(--ink);color:#fff;cursor:pointer;font-size:13.5px}
|
|
161
|
-
.localhint{font-size:12px;color:var(--sub);margin-top:9px}
|
|
181
|
+
.localhint{font-size:12px;color:var(--sub);margin-top:9px;line-height:1.5}
|
|
182
|
+
.bridgebox{margin-top:14px;border-top:1px solid var(--line);padding-top:12px}
|
|
183
|
+
.bridgebox summary{cursor:pointer;font-size:12.5px;color:var(--a);font-weight:560;list-style:none}
|
|
184
|
+
.lscard{margin-top:12px;border:1px solid var(--line);border-radius:12px;background:#fff;padding:14px 16px;text-align:left}
|
|
185
|
+
.lscard .lsh{font-size:14px;color:var(--ink);margin-bottom:8px}
|
|
186
|
+
.lscard .lsrow{display:flex;gap:12px;padding:7px 0;border-top:1px solid var(--line2);font-size:13px}
|
|
187
|
+
.lscard .lsk{width:110px;color:var(--sub);font-weight:560;flex-shrink:0}
|
|
188
|
+
.lscard .lsv{color:var(--ink2)} .lscard .lsv.bad{color:var(--red)} .lscard .lsv.ok{color:var(--green)}
|
|
189
|
+
.lscard .lshits{margin:6px 0;font-size:12px;color:var(--red);font-family:ui-monospace,Menlo,monospace;line-height:1.6}
|
|
190
|
+
.lscard .lsok{margin:6px 0;font-size:12.5px;color:var(--green)}
|
|
191
|
+
.lscard .pill{display:inline-block;font-size:11px;background:var(--soft);border:1px solid var(--line);border-radius:20px;padding:1px 8px;color:var(--ink2)}
|
|
192
|
+
.lscard .lsnote{margin-top:10px;font-size:11.5px;color:var(--sub);line-height:1.5;border-top:1px solid var(--line2);padding-top:9px}
|
|
193
|
+
/* SHOWCASE — selling points + grand graphic at the bottom */
|
|
194
|
+
.showcase{max-width:980px;margin:64px auto 0;text-align:center;padding:0 20px}
|
|
195
|
+
.showcase .sctitle{font-size:30px;font-weight:700;letter-spacing:-.02em;color:var(--ink);margin:0 0 10px;line-height:1.2}
|
|
196
|
+
.showcase .sctitle .hl{color:var(--a)}
|
|
197
|
+
.showcase .scsub{font-size:15px;color:var(--sub);max-width:640px;margin:0 auto 30px;line-height:1.6}
|
|
198
|
+
.showcase .sc3{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;text-align:left}
|
|
199
|
+
@media(max-width:760px){.showcase .sc3{grid-template-columns:1fr}}
|
|
200
|
+
.showcase .sccard{background:var(--soft);border:1px solid var(--line);border-radius:16px;padding:20px}
|
|
201
|
+
.showcase .scico{font-size:26px;margin-bottom:8px}
|
|
202
|
+
.showcase .sccard b{display:block;font-size:15px;color:var(--ink);margin-bottom:6px}
|
|
203
|
+
.showcase .sccard span{font-size:13px;color:var(--sub);line-height:1.55}
|
|
204
|
+
.showcase .bigart{margin-top:8px;display:flex;justify-content:center;opacity:.92}
|
|
205
|
+
@media(max-width:760px){.showcase .bigart svg{width:300px;height:auto}}
|
|
162
206
|
.picker{display:none;margin-top:12px;border:1px solid var(--line);border-radius:10px;background:#fff;overflow:hidden}
|
|
163
207
|
.picker.on{display:block}
|
|
164
208
|
.pkbar{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid var(--line2);font-size:12.5px}
|
|
@@ -169,6 +213,33 @@
|
|
|
169
213
|
.pkrow:last-child{border-bottom:0}.pkrow:hover{background:var(--soft)}
|
|
170
214
|
.pkrow .ic{width:18px;text-align:center}
|
|
171
215
|
.pkrow .repobadge{margin-left:auto;font-size:10.5px;color:var(--a);background:var(--a-soft);border-radius:6px;padding:2px 7px;font-weight:600}
|
|
216
|
+
/* ── REAL-TIME TRACKING panel — clean + clear ── */
|
|
217
|
+
.track{max-width:760px;margin:18px auto 0;background:var(--soft);border:1px solid var(--line);border-top:2px solid var(--a);border-radius:var(--rs);padding:16px 18px;box-shadow:var(--sh-sm)}
|
|
218
|
+
.track .thead{display:flex;align-items:center;gap:8px;font-size:15px;color:var(--ink)}
|
|
219
|
+
.track .ticon{font-size:17px}
|
|
220
|
+
.track .tnew{font-size:10px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--a);background:var(--a-soft);border-radius:6px;padding:2px 7px}
|
|
221
|
+
.track .trow{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:12px}
|
|
222
|
+
.track .tlabel{font-size:13px;color:var(--sub);font-weight:560}
|
|
223
|
+
.track select{height:38px;border:1px solid var(--line);border-radius:9px;padding:0 12px;font-size:13.5px;background:#fff;color:var(--ink2);max-width:260px}
|
|
224
|
+
.track .tbtn{height:38px;padding:0 16px;border-radius:9px;border:1.5px solid var(--a);background:var(--a);color:#fff;font-weight:600;font-size:13.5px;cursor:pointer}
|
|
225
|
+
.track .tbtn.off{background:#fff;color:var(--a)}
|
|
226
|
+
.track .tbtn.stop{border-color:var(--red);background:#fff;color:var(--red)}
|
|
227
|
+
.track .live{display:inline-flex;align-items:center;gap:7px;font-size:13px;color:var(--green);font-weight:600}
|
|
228
|
+
.track .dot{width:9px;height:9px;border-radius:50%;background:var(--green);box-shadow:0 0 0 0 rgba(22,163,74,.5);animation:pulse 1.8s infinite}
|
|
229
|
+
@keyframes pulse{0%{box-shadow:0 0 0 0 rgba(22,163,74,.45)}70%{box-shadow:0 0 0 8px rgba(22,163,74,0)}100%{box-shadow:0 0 0 0 rgba(22,163,74,0)}}
|
|
230
|
+
.track .drift{margin-top:12px;border-radius:10px;padding:10px 14px;font-size:13.5px;font-weight:560;display:none}
|
|
231
|
+
.track .drift.degraded{background:#fff1f3;border:1px solid #fecdd6;color:var(--red)}
|
|
232
|
+
.track .drift.improved{background:#f0fdf4;border:1px solid #bbf7d0;color:var(--green)}
|
|
233
|
+
.track .drift.changed,.track .drift.stable{background:var(--a-soft);border:1px solid #dfe2ff;color:var(--a)}
|
|
234
|
+
.track .tl{margin-top:10px;display:flex;gap:6px;align-items:center;flex-wrap:wrap;font-family:ui-monospace,Menlo,monospace;font-size:12px}
|
|
235
|
+
.track .tl .pill{padding:2px 8px;border-radius:20px;border:1px solid var(--line);background:#fff;color:var(--ink2)}
|
|
236
|
+
.track .tl .pill.deg{border-color:#fecdd6;color:var(--red)} .track .tl .pill.imp{border-color:#bbf7d0;color:var(--green)}
|
|
237
|
+
.track .thelp{margin-top:8px;font-size:12px;color:var(--sub);line-height:1.5}
|
|
238
|
+
.track .tfeat{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:12px}
|
|
239
|
+
@media(max-width:620px){.track .tfeat{grid-template-columns:1fr}}
|
|
240
|
+
.track .tf{background:#fff;border:1px solid var(--line);border-radius:10px;padding:10px 12px;font-size:12px}
|
|
241
|
+
.track .tf b{display:block;font-size:12.5px;color:var(--ink);margin-bottom:3px}
|
|
242
|
+
.track .tf span{color:var(--sub);line-height:1.45}
|
|
172
243
|
footer{padding:40px 0 70px;text-align:center;color:#aab0b8;font-size:13px}
|
|
173
244
|
.spin{display:inline-block;width:15px;height:15px;border:2px solid currentColor;border-top-color:transparent;
|
|
174
245
|
border-radius:50%;animation:s .7s linear infinite;vertical-align:-2px;margin-right:7px;opacity:.9}
|
|
@@ -198,8 +269,8 @@
|
|
|
198
269
|
</style>
|
|
199
270
|
</head>
|
|
200
271
|
<body>
|
|
201
|
-
<!-- grand meteor-impact illustration —
|
|
202
|
-
<div class="comet comet-a" aria-hidden="true">
|
|
272
|
+
<!-- grand meteor-impact illustration — moved to the bottom showcase -->
|
|
273
|
+
<div class="comet comet-a" aria-hidden="true" style="display:none">
|
|
203
274
|
<svg viewBox="0 0 360 420" width="360" height="420">
|
|
204
275
|
<defs>
|
|
205
276
|
<linearGradient id="mtail" x1="1" y1="0" x2="0" y2="1">
|
|
@@ -258,10 +329,10 @@
|
|
|
258
329
|
<span><b>X-Ray</b> — instant health report: dependencies, secrets, risk & who-to-ask.</span>
|
|
259
330
|
<span><b>📦 AI Pack</b> — bundle the whole repo into one text to paste into any AI chat.</span>
|
|
260
331
|
</div>
|
|
261
|
-
<p class="hint">Every number is <b>reproducible</b> from git, code & package metadata and sealed with an <b>offline-verifiable</b> signature. <span class="muted">Private folder?
|
|
332
|
+
<p class="hint">Every number is <b>reproducible</b> from git, code & package metadata and sealed with an <b>offline-verifiable</b> signature. <span class="muted">Private repo or a local folder with no public URL? Run <code>npx @mneme-ai/xray bridge</code> on your machine — a <b>Scan a local folder</b> box appears here + your code never leaves your computer.</span></p>
|
|
262
333
|
<details class="keybox">
|
|
263
334
|
<summary>🔖 Optional — save your reports</summary>
|
|
264
|
-
<div class="keyhelp">A “key” is
|
|
335
|
+
<div class="keyhelp">A “key” is just a password you make up (no sign-up). <b>How it works:</b> ① type a key + Save · ② X-Ray any repo above — it's filed under your key automatically · ③ open <b>My repos</b> below to find it anytime, on any device with the same key.</div>
|
|
265
336
|
<div class="keyrow">
|
|
266
337
|
<input id="key" placeholder="make up a password — e.g. my-secret-123" autocomplete="off" />
|
|
267
338
|
<button id="savekey" type="button">Save</button>
|
|
@@ -270,39 +341,149 @@
|
|
|
270
341
|
<div id="keymsg" class="keymsg" style="display:none"></div>
|
|
271
342
|
</details>
|
|
272
343
|
<div class="localbox" id="localbox">
|
|
273
|
-
<div class="lh"><span class="localdot"></span>
|
|
344
|
+
<div class="lh"><span class="localdot" id="localdot"></span><b>Or scan a folder on this computer</b> <span class="muted" id="localhead">— nothing is uploaded</span></div>
|
|
274
345
|
<div class="localrow">
|
|
275
|
-
<
|
|
276
|
-
<
|
|
277
|
-
<button id="localgo" type="button">Scan</button>
|
|
278
|
-
<button id="localpack" type="button" class="ghostbtn">📦 Pack</button>
|
|
346
|
+
<button id="pickfolder" type="button" class="pickbtn">📂 Choose a folder…</button>
|
|
347
|
+
<span class="muted" id="picknote">No install, no terminal — your browser asks which folder, the scan runs right here (secrets · dependencies · size).</span>
|
|
279
348
|
</div>
|
|
280
|
-
<div
|
|
281
|
-
<
|
|
349
|
+
<div id="localout"></div>
|
|
350
|
+
<details class="bridgebox">
|
|
351
|
+
<summary>Want the FULL report on a local repo (git history, bus factor, vitality)?</summary>
|
|
352
|
+
<div class="localhint">Run this once on your machine — a tiny local agent reads git + files locally and shows the <b>signed</b> result here. Nothing is uploaded.
|
|
353
|
+
<div class="cmdrow"><code>npx @mneme-ai/xray bridge</code><button id="copybridge" type="button" class="ghostbtn">Copy</button></div>
|
|
354
|
+
</div>
|
|
355
|
+
<div class="localrow" id="bridgerow" style="display:none">
|
|
356
|
+
<input id="localpath" placeholder="/absolute/path/to/your/repo" autocomplete="off" spellcheck="false" />
|
|
357
|
+
<button id="localbrowse" type="button" class="ghostbtn">📂 Browse</button>
|
|
358
|
+
<button id="localgo" type="button">Scan</button>
|
|
359
|
+
<button id="localpack" type="button" class="ghostbtn">📦 Pack</button>
|
|
360
|
+
</div>
|
|
361
|
+
<div class="picker" id="picker"></div>
|
|
362
|
+
</details>
|
|
282
363
|
</div>
|
|
283
364
|
<p class="err" id="err" style="display:none"></p>
|
|
284
365
|
</header>
|
|
285
366
|
|
|
286
367
|
<div id="out"></div>
|
|
287
368
|
<div id="packout"></div>
|
|
369
|
+
<div id="track" class="track" style="display:none"></div>
|
|
288
370
|
|
|
289
371
|
<div class="board">
|
|
290
372
|
<div class="tabs"><button id="tabBoard" class="tab on">Recently X-rayed</button><button id="tabMine" class="tab" style="display:none">My repos</button></div>
|
|
291
373
|
<p class="boardnote" id="boardnote">🌍 <b>Public</b> — recent X-Rays of <b>public</b> repos, visible to everyone. Private repos never appear here.</p>
|
|
292
374
|
<div id="board"></div>
|
|
293
375
|
</div>
|
|
376
|
+
|
|
377
|
+
<!-- SHOWCASE — the selling points (real: this is what Live tracking does) + the grand graphic -->
|
|
378
|
+
<section class="showcase">
|
|
379
|
+
<h2 class="sctitle">From a one-shot check to an <span class="hl">AI Auditor for your team</span></h2>
|
|
380
|
+
<p class="scsub">Turn on Live tracking and X-Ray watches a branch 24/7 — every push, human or AI, is re-scanned and the drift surfaces here. No re-click.</p>
|
|
381
|
+
<div class="sc3">
|
|
382
|
+
<div class="sccard"><div class="scico">🛡</div><b>AI Code Drift Detection</b><span>The moment anyone — or an AI — pushes, catch a newly-leaked secret, a destructive CI command, or a grade drop. Malicious or off-architecture code is flagged on arrival.</span></div>
|
|
383
|
+
<div class="sccard"><div class="scico">🔁</div><b>Continuous Autonomous Audit</b><span>Re-checked on every push (or every 30s) — a 24/7 security & health audit that runs itself. Nobody has to remember to click.</span></div>
|
|
384
|
+
<div class="sccard"><div class="scico">🕰</div><b>Time-Machine Indexing</b><span>A signed timeline of how code-health drifted, commit by commit — see exactly when a risk entered and who pushed it.</span></div>
|
|
385
|
+
</div>
|
|
386
|
+
<div class="bigart" aria-hidden="true">
|
|
387
|
+
<svg viewBox="0 0 360 420" width="440" height="513">
|
|
388
|
+
<defs>
|
|
389
|
+
<linearGradient id="mtail2" x1="1" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#0b0b0f" stop-opacity=".5"/><stop offset="1" stop-color="#0b0b0f" stop-opacity="0"/></linearGradient>
|
|
390
|
+
<radialGradient id="dust2" cx="50%" cy="60%"><stop offset="0" stop-color="#5b5bf6" stop-opacity=".18"/><stop offset="1" stop-color="#5b5bf6" stop-opacity="0"/></radialGradient>
|
|
391
|
+
<linearGradient id="rock2" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#2b2c4a"/><stop offset="1" stop-color="#0b0b0f"/></linearGradient>
|
|
392
|
+
</defs>
|
|
393
|
+
<path d="M-20 322 Q90 300 150 330 Q175 344 200 330 Q280 300 380 320" fill="none" stroke="#5b5bf6" stroke-width="2" opacity=".4"/>
|
|
394
|
+
<ellipse cx="172" cy="332" rx="96" ry="24" fill="url(#dust2)"/>
|
|
395
|
+
<path d="M300 24 Q236 96 196 250 Q244 110 296 40 Z" fill="url(#mtail2)"/>
|
|
396
|
+
<g transform="translate(196 250)">
|
|
397
|
+
<polygon points="-34,-6 -18,-34 14,-40 36,-18 30,18 2,34 -28,22" fill="url(#rock2)"/>
|
|
398
|
+
<g stroke="#5b5bf6" stroke-width="1.3" opacity=".6" fill="none"><path d="M-18 -34 L2 -6 L36 -18"/><path d="M2 -6 L-28 22"/><path d="M2 -6 L2 34"/><path d="M2 -6 L30 18"/></g>
|
|
399
|
+
<polygon points="-18,-34 14,-40 2,-6" fill="#34356a"/>
|
|
400
|
+
</g>
|
|
401
|
+
<g>
|
|
402
|
+
<polygon points="86,178 110,150 128,170 116,210 92,206" fill="url(#rock2)"/><polygon points="248,170 276,150 296,184 272,206 250,194" fill="url(#rock2)"/>
|
|
403
|
+
<polygon points="150,128 168,104 186,124 178,156 154,152" fill="url(#rock2)"/>
|
|
404
|
+
<polygon points="44,256 64,242 74,266 56,278" fill="#0b0b0f"/><polygon points="300,250 318,238 326,262 306,272" fill="#0b0b0f"/>
|
|
405
|
+
<polygon points="208,150 222,134 234,150 226,170" fill="#0b0b0f" opacity=".82"/>
|
|
406
|
+
</g>
|
|
407
|
+
<g fill="#5b5bf6" opacity=".5"><polygon points="130,96 140,90 142,102 132,104"/><polygon points="232,108 242,102 244,114 234,116"/><polygon points="70,150 79,145 81,156 71,158"/><polygon points="286,138 295,133 297,144 287,146"/></g>
|
|
408
|
+
</svg>
|
|
409
|
+
</div>
|
|
410
|
+
</section>
|
|
294
411
|
</div>
|
|
295
412
|
<footer>Mneme — the trust & cost layer for code. Deterministic · Signed · Local-first.<br/><span id="copyr">© Mneme</span></footer>
|
|
296
413
|
|
|
297
414
|
<script src="/card.js"></script>
|
|
415
|
+
<script src="/local-scan.js"></script>
|
|
298
416
|
<script>
|
|
299
417
|
const gC = g => "g-" + (("ABCDEF".includes(g)) ? g : "C");
|
|
300
418
|
const esc = s => String(s==null?"":s).replace(/[&<>]/g, c => ({"&":"&","<":"<",">":">"}[c]));
|
|
301
419
|
|
|
302
|
-
function
|
|
420
|
+
function renderCard(signed){
|
|
303
421
|
document.getElementById("out").innerHTML = window.MnemeXRay.xrayCardHTML(signed, { share: true });
|
|
304
422
|
mountShare(signed.report);
|
|
305
423
|
}
|
|
424
|
+
function render(signed){
|
|
425
|
+
renderCard(signed);
|
|
426
|
+
mountTracking(signed.report);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ── REAL-TIME TRACKING (branch-aware · poll+webhook · live SSE · drift) ──
|
|
430
|
+
const brank = n => /^(main)$/.test(n)?0 : /^(master)$/.test(n)?1 : /^(develop|dev)$/.test(n)?2 : 9;
|
|
431
|
+
function mountTracking(r){
|
|
432
|
+
const panel = document.getElementById("track");
|
|
433
|
+
if(!r || r.subject.kind !== "git-url"){ panel.style.display="none"; return; }
|
|
434
|
+
const gitUrl = r.subject.ref;
|
|
435
|
+
panel.style.display="block";
|
|
436
|
+
panel.innerHTML = `<div class="thead"><span class="ticon">🛰</span><b>Live tracking</b><span class="tnew">new</span></div>
|
|
437
|
+
<div class="thelp">Turn this one-shot X-Ray into an <b>AI Auditor for your team</b>. Pick a branch and press Track live — when you, a teammate, or an AI pushes, this report <b>re-scans itself</b> and shows what changed. No re-click.</div>
|
|
438
|
+
<div class="tfeat">
|
|
439
|
+
<div class="tf"><b>🛡 AI Code Drift</b><span>a new pushed secret, a destructive CI command, a grade drop — flagged the moment it lands</span></div>
|
|
440
|
+
<div class="tf"><b>🔁 Continuous audit</b><span>re-checked on every push (or every 30s) — 24/7, no one has to click</span></div>
|
|
441
|
+
<div class="tf"><b>🕰 Time-Machine</b><span>a timeline of how code-health drifted, commit by commit</span></div>
|
|
442
|
+
</div>
|
|
443
|
+
<div class="trow">
|
|
444
|
+
<span class="tlabel">Branch</span>
|
|
445
|
+
<select id="tbranch"><option value="">loading branches…</option></select>
|
|
446
|
+
<button id="ttoggle" class="tbtn off">● Track live</button>
|
|
447
|
+
<span id="tstate" class="tlabel"></span>
|
|
448
|
+
</div>
|
|
449
|
+
<div id="tdrift" class="drift"></div>
|
|
450
|
+
<div id="ttl" class="tl"></div>
|
|
451
|
+
<div id="tnote" class="thelp" style="display:none"></div>`;
|
|
452
|
+
fetch("/api/branches?url="+encodeURIComponent(gitUrl)).then(x=>x.json()).then(d=>{
|
|
453
|
+
const sel=document.getElementById("tbranch"); if(!sel) return;
|
|
454
|
+
const names=((d&&d.branches)||[]).map(b=>b.name);
|
|
455
|
+
if(!names.length){ sel.innerHTML='<option value="">default branch</option>'; return; }
|
|
456
|
+
names.sort((a,b)=>brank(a)-brank(b)||a.localeCompare(b));
|
|
457
|
+
sel.innerHTML=names.map(n=>`<option value="${esc(n)}"${n===r.subject.branch?" selected":""}>${esc(n)}</option>`).join("");
|
|
458
|
+
}).catch(()=>{ const sel=document.getElementById("tbranch"); if(sel) sel.innerHTML='<option value="">default branch</option>'; });
|
|
459
|
+
|
|
460
|
+
let es=null; const history=[];
|
|
461
|
+
const toggle=document.getElementById("ttoggle");
|
|
462
|
+
const stateEl=()=>document.getElementById("tstate");
|
|
463
|
+
const noteEl=()=>document.getElementById("tnote");
|
|
464
|
+
function stop(){ if(es){es.close();es=null;} toggle.className="tbtn off"; toggle.textContent="● Track live"; stateEl().textContent="stopped"; const n=noteEl(); if(n) n.style.display="none"; }
|
|
465
|
+
function showDrift(d){ const el=document.getElementById("tdrift"); if(!el||!d) return; el.className="drift "+d.drift; el.style.display="block"; el.textContent=((d.highlights&&d.highlights[0])||("changed → grade "+d.gradeTo))+" · just now"; }
|
|
466
|
+
function pushTL(d){ history.push(d); const el=document.getElementById("ttl"); if(!el) return; el.innerHTML='<span class="tlabel">Timeline</span> '+history.slice(-14).map(h=>{const c=h.drift==="degraded"?"deg":h.drift==="improved"?"imp":""; return '<span class="pill '+c+'">'+esc(h.gradeTo)+(h.newSecretLeaks>0?" 🔴":"")+'</span>';}).join(" "); }
|
|
467
|
+
function startSSE(id, branch){
|
|
468
|
+
es=new EventSource("/api/track/"+id+"/stream");
|
|
469
|
+
toggle.className="tbtn stop"; toggle.textContent="Stop"; toggle.disabled=false;
|
|
470
|
+
stateEl().innerHTML='<span class="live"><span class="dot"></span>LIVE · '+esc(branch||"default")+'</span>';
|
|
471
|
+
const n=noteEl(); if(n){ n.style.display="block"; n.innerHTML='✓ <b>Now monitoring '+esc(branch||"the default branch")+'.</b> Leave this tab open to watch changes appear here live. The monitor keeps running on the server even if you close the tab — come back and X-Ray this same repo to reconnect. <span class="muted">Switching repo or branch starts a separate monitor; a refresh reconnects this view.</span>'; }
|
|
472
|
+
es.addEventListener("update", ev=>{ try{ const p=JSON.parse(ev.data); renderCard(p.signed); showDrift(p.delta); pushTL(p.delta);}catch{} });
|
|
473
|
+
// EventSource auto-reconnects on error; keep the LIVE state.
|
|
474
|
+
}
|
|
475
|
+
toggle.addEventListener("click", async ()=>{
|
|
476
|
+
if(es){ stop(); return; }
|
|
477
|
+
const branch=document.getElementById("tbranch").value||undefined;
|
|
478
|
+
toggle.disabled=true; toggle.textContent="starting…";
|
|
479
|
+
try{
|
|
480
|
+
const res=await fetch("/api/track",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({gitUrl,branch})});
|
|
481
|
+
const data=await res.json();
|
|
482
|
+
if(!res.ok){ stateEl().textContent=data.error||"failed"; toggle.disabled=false; toggle.textContent="● Track live"; return; }
|
|
483
|
+
startSSE(data.trackId, branch);
|
|
484
|
+
}catch(e){ stateEl().textContent=String(e); toggle.disabled=false; toggle.textContent="● Track live"; }
|
|
485
|
+
});
|
|
486
|
+
}
|
|
306
487
|
|
|
307
488
|
// share row: permalink + an embeddable badge (the viral loop)
|
|
308
489
|
function mountShare(r){
|
|
@@ -453,9 +634,44 @@ async function detectBridge(){
|
|
|
453
634
|
try{
|
|
454
635
|
const ctrl=new AbortController(); const t=setTimeout(()=>ctrl.abort(),1200);
|
|
455
636
|
const r=await fetch(BRIDGE+"/bridge/ping",{signal:ctrl.signal}); clearTimeout(t);
|
|
456
|
-
if(r.ok){
|
|
457
|
-
|
|
637
|
+
if(r.ok){ // full-report bridge is up → light up the green dot + the bridge row
|
|
638
|
+
document.getElementById("localbox").classList.add("connected");
|
|
639
|
+
const h=document.getElementById("localhead"); if(h) h.textContent="— local agent connected · full signed report available";
|
|
640
|
+
const br=document.getElementById("bridgerow"); if(br) br.style.display="flex";
|
|
641
|
+
}
|
|
642
|
+
}catch{ /* no local agent — the zero-install Choose-folder path still works */ }
|
|
643
|
+
}
|
|
644
|
+
// ---- ZERO-INSTALL local scan (File System Access API) — no terminal, no upload ----
|
|
645
|
+
const copyBtn=document.getElementById("copybridge");
|
|
646
|
+
if(copyBtn) copyBtn.addEventListener("click",async()=>{ try{ await navigator.clipboard.writeText("npx @mneme-ai/xray bridge"); const o=copyBtn.textContent; copyBtn.textContent="Copied ✓"; setTimeout(()=>copyBtn.textContent=o,1200);}catch{} });
|
|
647
|
+
function renderLocalScan(r){
|
|
648
|
+
const s=r.summary, out=document.getElementById("localout"); if(!out) return;
|
|
649
|
+
const sec=s.secrets, secCls=sec.totalFindings>0?"bad":"ok";
|
|
650
|
+
const langs=s.langs.map(([e,n])=>`<span class="pill">${esc(e)} ${n}</span>`).join(" ");
|
|
651
|
+
out.innerHTML=`<div class="lscard">
|
|
652
|
+
<div class="lsh"><b>📂 ${esc(r.folder)}</b> <span class="muted">— scanned in your browser · ${r.files} files · nothing uploaded</span></div>
|
|
653
|
+
<div class="lsrow"><span class="lsk">Secrets</span><span class="lsv ${secCls}"><b>${sec.totalFindings}</b> in production code${sec.excludedTestHits?` · ${sec.excludedTestHits} in tests (excluded)`:""}</span></div>
|
|
654
|
+
${sec.hits.length?`<div class="lshits">${sec.hits.slice(0,6).map(h=>`<div>🔴 ${esc(h.kind)} — ${esc(h.file)}:${h.line}</div>`).join("")}</div>`:`<div class="lsok">✓ no leaked credentials in production code</div>`}
|
|
655
|
+
<div class="lsrow"><span class="lsk">Dependencies</span><span class="lsv"><b>${s.deps.total}</b> total · ${s.deps.deps} deps · ${s.deps.devDeps} dev</span></div>
|
|
656
|
+
<div class="lsrow"><span class="lsk">Size</span><span class="lsv"><b>${s.filesScanned}</b> source files · ${s.loc.toLocaleString()} lines</span></div>
|
|
657
|
+
<div class="lsrow"><span class="lsk">Languages</span><span class="lsv">${langs||"—"}</span></div>
|
|
658
|
+
<div class="lsnote">Quick local scan — secrets · dependencies · size, computed <b>in your browser</b> (unsigned, no upload). The full <b>signed</b> report with git history, bus factor & vitality needs a public URL above or the bridge.</div>
|
|
659
|
+
</div>`;
|
|
458
660
|
}
|
|
661
|
+
const pick=document.getElementById("pickfolder");
|
|
662
|
+
if(pick) pick.addEventListener("click", async ()=>{
|
|
663
|
+
const note=document.getElementById("picknote");
|
|
664
|
+
if(!window.MnemeLocalScan || !window.MnemeLocalScan.supported){
|
|
665
|
+
if(note) note.textContent="Your browser does not support in-page folder access (try Chrome or Edge), or use the FULL report option below.";
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
pick.disabled=true; const o=pick.textContent; pick.innerHTML='<span class="spin"></span>Scanning locally…';
|
|
669
|
+
try{
|
|
670
|
+
const r=await window.MnemeLocalScan.pickAndScan();
|
|
671
|
+
renderLocalScan(r);
|
|
672
|
+
}catch(e){ if(String(e&&e.name)!=="AbortError" && String(e&&e.message)!=="UNSUPPORTED" && note) note.textContent="Couldn't read that folder: "+String(e&&e.message||e); }
|
|
673
|
+
finally{ pick.disabled=false; pick.textContent=o; }
|
|
674
|
+
});
|
|
459
675
|
document.getElementById("localgo").addEventListener("click", async ()=>{
|
|
460
676
|
const p=document.getElementById("localpath").value.trim();
|
|
461
677
|
const err=document.getElementById("err"), btn=document.getElementById("localgo");
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-browser LOCAL FOLDER scan — zero install, no terminal, NOTHING uploaded.
|
|
3
|
+
*
|
|
4
|
+
* A website cannot read your disk on its own (browser security). The File System
|
|
5
|
+
* Access API is the honest exception: the user clicks "Choose folder", the OS
|
|
6
|
+
* shows a native picker, and ONLY the folder they grant is readable — and only by
|
|
7
|
+
* code running in their own tab. We read the files in the browser, run the
|
|
8
|
+
* deterministic file-content analyzers (secrets · dependencies · size), and render
|
|
9
|
+
* the result locally. The bytes never leave the machine.
|
|
10
|
+
*
|
|
11
|
+
* Honest scope: git-history signals (bus factor · vitality · hotspots · coupling)
|
|
12
|
+
* need git, which a browser cannot run — those need the public URL or the bridge.
|
|
13
|
+
* This is the instant, no-install "what's in this folder" scan.
|
|
14
|
+
*/
|
|
15
|
+
(function (g) {
|
|
16
|
+
// ── pure, deterministic analyzers (unit-testable, no DOM) ──────────────────
|
|
17
|
+
const SECRET_PATTERNS = [
|
|
18
|
+
{ kind: "AWS access key", re: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
19
|
+
{ kind: "AWS secret key", re: /\b[A-Za-z0-9/+]{40}\b(?=.*aws|.*secret)/i },
|
|
20
|
+
{ kind: "GitHub token", re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/ },
|
|
21
|
+
{ kind: "OpenAI key", re: /\bsk-[A-Za-z0-9]{20,}\b/ },
|
|
22
|
+
{ kind: "Anthropic key", re: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/ },
|
|
23
|
+
{ kind: "Slack token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/ },
|
|
24
|
+
{ kind: "Google API key", re: /\bAIza[0-9A-Za-z_-]{35}\b/ },
|
|
25
|
+
{ kind: "private key block", re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/ },
|
|
26
|
+
{ kind: "JWT", re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ },
|
|
27
|
+
];
|
|
28
|
+
// test/fixture/doc files are sample data, not leaks — excluded from the count
|
|
29
|
+
const TEST_RE = /(^|\/)(test|tests|__tests__|fixtures?|examples?|docs?|spec|mocks?)(\/|$)|\.(test|spec)\.|\.md$|\.lock$/i;
|
|
30
|
+
const TEXT_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|c|h|cpp|cc|rb|php|cs|kt|swift|scala|sh|bash|env|json|ya?ml|toml|ini|cfg|xml|gradle|properties|tf|sql|vue|svelte)$/i;
|
|
31
|
+
const SKIP_DIR = /(^|\/)(node_modules|\.git|dist|build|vendor|\.next|coverage|__pycache__|\.venv|target)(\/|$)/;
|
|
32
|
+
|
|
33
|
+
function scanSecretsText(text, relPath) {
|
|
34
|
+
const isTest = TEST_RE.test(relPath || "");
|
|
35
|
+
const hits = [];
|
|
36
|
+
const lines = String(text).split("\n");
|
|
37
|
+
for (let i = 0; i < lines.length; i++) {
|
|
38
|
+
for (const p of SECRET_PATTERNS) {
|
|
39
|
+
if (p.re.test(lines[i])) { hits.push({ kind: p.kind, file: relPath, line: i + 1, isTest }); }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return hits;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseDeps(pkgJsonText) {
|
|
46
|
+
try {
|
|
47
|
+
const p = JSON.parse(pkgJsonText);
|
|
48
|
+
const d = Object.keys(p.dependencies || {});
|
|
49
|
+
const dev = Object.keys(p.devDependencies || {});
|
|
50
|
+
return { total: d.length + dev.length, deps: d.length, devDeps: dev.length, names: [...d, ...dev].slice(0, 60) };
|
|
51
|
+
} catch { return { total: 0, deps: 0, devDeps: 0, names: [] }; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Compose a deterministic local report from already-read files. Pure. */
|
|
55
|
+
function summarize(files) {
|
|
56
|
+
// files: [{ rel, text }]
|
|
57
|
+
let loc = 0, scanned = 0, testHits = 0;
|
|
58
|
+
const prodHits = [];
|
|
59
|
+
const langs = {};
|
|
60
|
+
let deps = { total: 0, deps: 0, devDeps: 0, names: [] };
|
|
61
|
+
for (const f of files) {
|
|
62
|
+
if (SKIP_DIR.test(f.rel)) continue;
|
|
63
|
+
if (/(^|\/)package\.json$/.test(f.rel) && !/node_modules/.test(f.rel)) deps = parseDeps(f.text);
|
|
64
|
+
if (!TEXT_EXT.test(f.rel)) continue;
|
|
65
|
+
scanned++;
|
|
66
|
+
loc += f.text.split("\n").length;
|
|
67
|
+
const ext = (f.rel.match(/\.([a-z0-9]+)$/i) || [, "?"])[1].toLowerCase();
|
|
68
|
+
langs[ext] = (langs[ext] || 0) + 1;
|
|
69
|
+
for (const h of scanSecretsText(f.text, f.rel)) { if (h.isTest) testHits++; else prodHits.push(h); }
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
filesScanned: scanned, loc, deps,
|
|
73
|
+
secrets: { totalFindings: prodHits.length, excludedTestHits: testHits, hits: prodHits.slice(0, 20) },
|
|
74
|
+
langs: Object.entries(langs).sort((a, b) => b[1] - a[1]).slice(0, 8),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
g.MnemeLocalScan = { scanSecretsText, parseDeps, summarize, _patterns: SECRET_PATTERNS };
|
|
79
|
+
|
|
80
|
+
// ── File System Access glue (browser-only; needs a user gesture + HTTPS) ────
|
|
81
|
+
g.MnemeLocalScan.supported = typeof g.showDirectoryPicker === "function";
|
|
82
|
+
|
|
83
|
+
async function readDir(dirHandle, prefix, out, cap) {
|
|
84
|
+
for await (const [name, handle] of dirHandle.entries()) {
|
|
85
|
+
const rel = prefix ? prefix + "/" + name : name;
|
|
86
|
+
if (SKIP_DIR.test(rel) || name.startsWith(".")) continue;
|
|
87
|
+
if (out.length >= cap) return;
|
|
88
|
+
if (handle.kind === "directory") { await readDir(handle, rel, out, cap); }
|
|
89
|
+
else if (TEXT_EXT.test(rel) || /(^|\/)package\.json$/.test(rel)) {
|
|
90
|
+
try { const file = await handle.getFile(); if (file.size <= 2 * 1024 * 1024) out.push({ rel, text: await file.text() }); } catch { /* skip unreadable */ }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Open the OS folder picker, read text files in-browser, return the summary +
|
|
96
|
+
* the folder name. Throws on user-cancel (caller catches). cap bounds the walk. */
|
|
97
|
+
g.MnemeLocalScan.pickAndScan = async function pickAndScan(cap = 4000) {
|
|
98
|
+
if (!g.MnemeLocalScan.supported) throw new Error("UNSUPPORTED");
|
|
99
|
+
const dir = await g.showDirectoryPicker(); // native picker — user grants ONE folder
|
|
100
|
+
const files = [];
|
|
101
|
+
await readDir(dir, "", files, cap);
|
|
102
|
+
return { folder: dir.name || "folder", files: files.length, summary: summarize(files) };
|
|
103
|
+
};
|
|
104
|
+
})(typeof window !== "undefined" ? window : globalThis);
|
package/public/report.html
CHANGED
|
@@ -22,11 +22,22 @@
|
|
|
22
22
|
.grade{width:72px;height:72px;border-radius:16px;display:grid;place-items:center;font-size:36px;font-weight:740;color:#fff;flex:none;box-shadow:inset 0 -2px 6px rgba(0,0,0,.12)}
|
|
23
23
|
.g-A{background:linear-gradient(135deg,#1fb255,#15863f)}.g-B{background:linear-gradient(135deg,#7bb736,#5c8f1e)}
|
|
24
24
|
.g-C{background:linear-gradient(135deg,#eaa83a,#c9821a)}.g-D{background:linear-gradient(135deg,#f0742e,#d4571a)}.g-F{background:linear-gradient(135deg,#f43f5e,#be123c)}
|
|
25
|
-
.top .repo{font-size:22px;font-weight:640;letter-spacing:-.02em;color:var(--ink);word-break:break-all;line-height:1.2}.top .head{color:var(--sub);font-size:14px;margin-top:4px}
|
|
25
|
+
.top .repo{font-size:22px;font-weight:640;letter-spacing:-.02em;color:var(--ink);word-break:break-all;line-height:1.2}.top .repobr{font-size:13px;font-weight:600;color:var(--a);background:var(--a-soft);border-radius:6px;padding:1px 7px;vertical-align:middle}.top .repourl{display:inline-block;font-size:12.5px;color:var(--a);text-decoration:none;margin-top:3px;word-break:break-all;font-family:ui-monospace,Menlo,monospace}.top .repourl:hover{text-decoration:underline}.top .head{color:var(--sub);font-size:14px;margin-top:4px}
|
|
26
26
|
.trustbar{display:flex;align-items:center;gap:14px;padding:13px 30px;background:#f0fdf4;border-bottom:1px solid #dcfce7;flex-wrap:wrap}
|
|
27
27
|
.hgauge{display:inline-flex;align-items:center;gap:8px;font-weight:680;color:#15803d;font-size:14px}
|
|
28
28
|
.hdot{width:10px;height:10px;border-radius:50%;background:#16a34a;box-shadow:0 0 0 4px rgba(22,163,74,.16)}
|
|
29
29
|
.htext{font-size:12.5px;color:#3f6b4e;line-height:1.5}.htext b{color:#14532d;font-weight:640}
|
|
30
|
+
.membrane{display:flex;border-bottom:1px solid #eef0f2;background:#fafbfc}
|
|
31
|
+
.mp{flex:1;padding:11px 18px;border-right:1px solid #eef0f2;display:flex;flex-direction:column;gap:3px}.mp:last-child{border-right:0}
|
|
32
|
+
.mpk{font-size:10.5px;letter-spacing:.06em;color:#8a909a;font-weight:700}
|
|
33
|
+
.mpv{font-size:12.5px;color:#2b2f36;font-weight:560;display:flex;align-items:center;gap:7px}
|
|
34
|
+
.triage{padding:14px 30px 4px}
|
|
35
|
+
.triage-h{font-size:13px;font-weight:680;color:#b45309;margin-bottom:9px}.triage-sub{font-weight:400;color:#9aa0a6;font-size:11.5px}
|
|
36
|
+
.triage.clearall{padding:13px 30px;color:#15803d;font-weight:560;font-size:13px;background:#f0fdf4;border-bottom:1px solid #dcfce7}
|
|
37
|
+
.ti{display:flex;gap:11px;align-items:flex-start;padding:8px 0;border-top:1px solid #f1f2f4}
|
|
38
|
+
.ti-sev{flex:0 0 auto;font-size:10px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;padding:3px 8px;border-radius:6px;min-width:62px;text-align:center}
|
|
39
|
+
.ti.critical .ti-sev{background:#fee2e2;color:#b91c1c}.ti.warn .ti-sev{background:#fef3c7;color:#b45309}.ti.info .ti-sev{background:#e0e7ff;color:#4338ca}
|
|
40
|
+
.ti-body{flex:1;min-width:0}.ti-f{font-size:13px;color:#1f2329}.ti-p{font-size:11px;color:#9aa0a6;margin-top:2px;font-family:ui-monospace,Menlo,monospace}
|
|
30
41
|
.rows{padding:6px 30px 16px}
|
|
31
42
|
.row{display:flex;gap:18px;padding:17px 0;border-bottom:1px solid var(--line2);align-items:baseline}.row:last-child{border-bottom:0}
|
|
32
43
|
.row .k{width:138px;flex:none;display:flex;flex-direction:column;gap:4px;font-size:12.5px;letter-spacing:.05em;text-transform:uppercase;color:var(--ink);font-weight:700}
|
|
@@ -34,7 +45,8 @@
|
|
|
34
45
|
.spark{font-family:ui-monospace,Menlo,monospace;letter-spacing:1px;color:var(--a)}
|
|
35
46
|
.muted{color:var(--sub)}
|
|
36
47
|
.row .v{font-size:15px;color:var(--ink2)}.row .v .big{font-weight:660;color:var(--ink);font-size:16px}
|
|
37
|
-
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
|
|
48
|
+
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px;max-height:148px;overflow-y:auto}
|
|
49
|
+
.chips::-webkit-scrollbar{width:8px}.chips::-webkit-scrollbar-thumb{background:#d7dbe0;border-radius:8px}
|
|
38
50
|
.chip{font-size:12px;padding:4px 10px;border-radius:8px;background:var(--soft);border:1px solid var(--line);color:#5c616b}
|
|
39
51
|
.chip.bad{background:#fff1f3;border-color:#ffe0e6;color:var(--red)}.chip.warn{background:#fff8ed;border-color:#fde6cc;color:#b45309}
|
|
40
52
|
.foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:16px 30px;background:var(--soft);font-size:13px;color:var(--sub);flex-wrap:wrap}
|