@mneme-ai/xray 2.150.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/README.md +71 -0
- package/dist/battery/age.d.ts +3 -0
- package/dist/battery/age.d.ts.map +1 -0
- package/dist/battery/age.js +65 -0
- package/dist/battery/age.js.map +1 -0
- package/dist/battery/busfactor.d.ts +3 -0
- package/dist/battery/busfactor.d.ts.map +1 -0
- package/dist/battery/busfactor.js +92 -0
- package/dist/battery/busfactor.js.map +1 -0
- package/dist/battery/complexity.d.ts +3 -0
- package/dist/battery/complexity.d.ts.map +1 -0
- package/dist/battery/complexity.js +50 -0
- package/dist/battery/complexity.js.map +1 -0
- package/dist/battery/deps.d.ts +15 -0
- package/dist/battery/deps.d.ts.map +1 -0
- package/dist/battery/deps.js +107 -0
- package/dist/battery/deps.js.map +1 -0
- package/dist/battery/hotspots.d.ts +3 -0
- package/dist/battery/hotspots.d.ts.map +1 -0
- package/dist/battery/hotspots.js +61 -0
- package/dist/battery/hotspots.js.map +1 -0
- package/dist/battery/secrets.d.ts +3 -0
- package/dist/battery/secrets.d.ts.map +1 -0
- package/dist/battery/secrets.js +64 -0
- package/dist/battery/secrets.js.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +76 -0
- package/dist/bin.js.map +1 -0
- package/dist/clone.d.ts +13 -0
- package/dist/clone.d.ts.map +1 -0
- package/dist/clone.js +42 -0
- package/dist/clone.js.map +1 -0
- package/dist/cosmic.d.ts +35 -0
- package/dist/cosmic.d.ts.map +1 -0
- package/dist/cosmic.js +122 -0
- package/dist/cosmic.js.map +1 -0
- package/dist/engine.d.ts +8 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +138 -0
- package/dist/engine.js.map +1 -0
- package/dist/gauntlet.d.ts +9 -0
- package/dist/gauntlet.d.ts.map +1 -0
- package/dist/gauntlet.js +47 -0
- package/dist/gauntlet.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/privacy.d.ts +12 -0
- package/dist/privacy.d.ts.map +1 -0
- package/dist/privacy.js +43 -0
- package/dist/privacy.js.map +1 -0
- package/dist/publish.d.ts +9 -0
- package/dist/publish.d.ts.map +1 -0
- package/dist/publish.js +28 -0
- package/dist/publish.js.map +1 -0
- package/dist/server.d.ts +29 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +482 -0
- package/dist/server.js.map +1 -0
- package/dist/sign.d.ts +7 -0
- package/dist/sign.d.ts.map +1 -0
- package/dist/sign.js +33 -0
- package/dist/sign.js.map +1 -0
- package/dist/types.d.ts +148 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +21 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +111 -0
- package/dist/util.js.map +1 -0
- package/package.json +55 -0
- package/public/card.js +45 -0
- package/public/cosmic.html +74 -0
- package/public/favicon.svg +1 -0
- package/public/index.html +294 -0
- package/public/report.html +76 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Mneme · Repo X-Ray</title>
|
|
7
|
+
<link rel="icon" href="/favicon.svg" />
|
|
8
|
+
<style>
|
|
9
|
+
:root{
|
|
10
|
+
--ink:#0a0a0a; --sub:#6b7280; --line:#ececec; --bg:#ffffff; --soft:#fafafa;
|
|
11
|
+
--a:#4f46e5; --green:#16a34a; --amber:#d97706; --red:#dc2626;
|
|
12
|
+
--radius:14px;
|
|
13
|
+
}
|
|
14
|
+
*{box-sizing:border-box}
|
|
15
|
+
html,body{margin:0;background:var(--bg);color:var(--ink);
|
|
16
|
+
font:16px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,Roboto,sans-serif;
|
|
17
|
+
-webkit-font-smoothing:antialiased}
|
|
18
|
+
.wrap{max-width:760px;margin:0 auto;padding:0 22px}
|
|
19
|
+
header{padding:74px 0 34px;text-align:center}
|
|
20
|
+
.mark{font-size:13px;letter-spacing:.18em;text-transform:uppercase;color:var(--sub)}
|
|
21
|
+
h1{font-size:46px;line-height:1.08;font-weight:680;margin:14px 0 10px;letter-spacing:-.02em}
|
|
22
|
+
.lede{color:var(--sub);font-size:18px;max-width:540px;margin:0 auto}
|
|
23
|
+
form{display:flex;gap:10px;margin:34px 0 8px}
|
|
24
|
+
input{flex:1;padding:15px 17px;border:1px solid var(--line);border-radius:var(--radius);
|
|
25
|
+
font-size:16px;outline:none;transition:border-color .15s}
|
|
26
|
+
input:focus{border-color:var(--ink)}
|
|
27
|
+
button{padding:15px 22px;border:0;border-radius:var(--radius);background:var(--ink);color:#fff;
|
|
28
|
+
font-size:16px;font-weight:560;cursor:pointer;transition:opacity .15s}
|
|
29
|
+
button:disabled{opacity:.45;cursor:default}
|
|
30
|
+
.hint{color:var(--sub);font-size:13.5px;text-align:center}
|
|
31
|
+
.hint b{color:var(--ink);font-weight:560}
|
|
32
|
+
.err{color:var(--red);text-align:center;margin-top:18px;font-size:14.5px}
|
|
33
|
+
/* card */
|
|
34
|
+
.card{border:1px solid var(--line);border-radius:18px;margin-top:30px;overflow:hidden;
|
|
35
|
+
box-shadow:0 1px 2px rgba(0,0,0,.03),0 8px 30px rgba(0,0,0,.04);animation:rise .3s ease}
|
|
36
|
+
@keyframes rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
|
|
37
|
+
.card .top{display:flex;align-items:center;gap:20px;padding:26px 28px;border-bottom:1px solid var(--line)}
|
|
38
|
+
.grade{width:78px;height:78px;border-radius:18px;display:grid;place-items:center;
|
|
39
|
+
font-size:40px;font-weight:720;color:#fff;flex:none}
|
|
40
|
+
.g-A{background:var(--green)} .g-B{background:#65a30d} .g-C{background:var(--amber)}
|
|
41
|
+
.g-D{background:#ea580c} .g-F{background:var(--red)}
|
|
42
|
+
.top .repo{font-size:21px;font-weight:620;letter-spacing:-.01em;word-break:break-all}
|
|
43
|
+
.top .head{color:var(--sub);font-size:14.5px;margin-top:2px}
|
|
44
|
+
.rows{padding:8px 28px 14px}
|
|
45
|
+
.row{display:flex;gap:14px;padding:15px 0;border-bottom:1px solid var(--soft);align-items:baseline}
|
|
46
|
+
.row:last-child{border-bottom:0}
|
|
47
|
+
.row .k{font-size:13px;letter-spacing:.04em;text-transform:uppercase;color:var(--sub);width:128px;flex:none}
|
|
48
|
+
.row .v{font-size:15.5px}
|
|
49
|
+
.row .v .big{font-weight:640}
|
|
50
|
+
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
|
|
51
|
+
.chip{font-size:12.5px;padding:3px 9px;border-radius:999px;background:var(--soft);border:1px solid var(--line);color:#374151}
|
|
52
|
+
.chip.bad{background:#fef2f2;border-color:#fee2e2;color:var(--red)}
|
|
53
|
+
.chip.warn{background:#fffbeb;border-color:#fef3c7;color:var(--amber)}
|
|
54
|
+
.foot{display:flex;align-items:center;justify-content:space-between;gap:12px;
|
|
55
|
+
padding:16px 28px;background:var(--soft);font-size:13px;color:var(--sub);flex-wrap:wrap}
|
|
56
|
+
.verified{display:inline-flex;align-items:center;gap:7px;color:var(--green);font-weight:560}
|
|
57
|
+
.dot{width:8px;height:8px;border-radius:50%;background:var(--green)}
|
|
58
|
+
code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px}
|
|
59
|
+
/* board */
|
|
60
|
+
.board{margin:54px 0 40px}
|
|
61
|
+
.board h3{font-size:13px;letter-spacing:.14em;text-transform:uppercase;color:var(--sub);font-weight:560}
|
|
62
|
+
.bitem{display:flex;align-items:center;gap:12px;padding:11px 0;border-bottom:1px solid var(--soft);font-size:14.5px}
|
|
63
|
+
.bg{width:24px;height:24px;border-radius:7px;display:grid;place-items:center;color:#fff;font-size:12px;font-weight:700;flex:none}
|
|
64
|
+
.bitem .nm{font-weight:540}.bitem .hd{color:var(--sub);margin-left:auto;font-size:13px}
|
|
65
|
+
footer{padding:30px 0 60px;text-align:center;color:var(--sub);font-size:13px}
|
|
66
|
+
.pill{display:inline-block;margin-top:14px;font-size:12.5px;color:var(--sub);background:var(--soft);
|
|
67
|
+
border:1px solid var(--line);border-radius:999px;padding:5px 13px}
|
|
68
|
+
.steps{display:flex;flex-wrap:wrap;gap:8px 18px;justify-content:center;margin:18px auto 0;max-width:640px;
|
|
69
|
+
color:var(--sub);font-size:13px}
|
|
70
|
+
.steps span{display:inline-flex;align-items:center;gap:7px}
|
|
71
|
+
.steps b{display:inline-grid;place-items:center;width:18px;height:18px;border-radius:50%;background:var(--ink);
|
|
72
|
+
color:#fff;font-size:11px;font-weight:700}
|
|
73
|
+
.keybox{max-width:560px;margin:20px auto 0;font-size:13px;color:var(--sub)}
|
|
74
|
+
.keybox summary{cursor:pointer;text-align:center;list-style:none}
|
|
75
|
+
.keybox summary::-webkit-details-marker{display:none}
|
|
76
|
+
.keyrow{display:flex;gap:8px;margin-top:12px}
|
|
77
|
+
.keyrow input{flex:1;padding:11px 13px;border:1px solid var(--line);border-radius:10px;font-size:14px}
|
|
78
|
+
.keyrow button{padding:11px 16px;border:0;border-radius:10px;background:var(--ink);color:#fff;cursor:pointer;font-size:14px}
|
|
79
|
+
.kstate{align-self:center;color:var(--green);font-size:12.5px;white-space:nowrap}
|
|
80
|
+
.tabs{display:flex;gap:8px;margin-bottom:6px}
|
|
81
|
+
.tab{background:none;border:0;padding:4px 0;margin-right:16px;font-size:13px;letter-spacing:.14em;
|
|
82
|
+
text-transform:uppercase;color:var(--sub);cursor:pointer;border-bottom:2px solid transparent}
|
|
83
|
+
.tab.on{color:var(--ink);border-bottom-color:var(--ink)}
|
|
84
|
+
.listbox{max-height:430px;overflow:auto;border:1px solid var(--line);border-radius:14px}
|
|
85
|
+
.bitem{display:flex;align-items:center;gap:12px;padding:13px 16px;border-bottom:1px solid var(--soft);font-size:14.5px;cursor:pointer}
|
|
86
|
+
.bitem:last-child{border-bottom:0}
|
|
87
|
+
.bitem:hover{background:var(--soft)}
|
|
88
|
+
.bitem .bg{width:24px;height:24px;border-radius:7px;display:grid;place-items:center;color:#fff;font-size:12px;font-weight:700;flex:none}
|
|
89
|
+
.bitem .nm{font-weight:560;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
90
|
+
.bitem .dates{display:flex;gap:14px;color:var(--sub);font-size:12.5px;white-space:nowrap}
|
|
91
|
+
.bitem .dates b{color:var(--ink);font-weight:560}
|
|
92
|
+
.bitem .cnt{color:var(--sub);font-size:12.5px;white-space:nowrap;min-width:56px;text-align:right}
|
|
93
|
+
.bitem .arr{color:var(--sub)}
|
|
94
|
+
.bitem.muted{cursor:default;color:var(--sub)}
|
|
95
|
+
.bitem.muted:hover{background:none}
|
|
96
|
+
.lock{margin-right:4px}
|
|
97
|
+
.listfoot{display:flex;justify-content:space-between;align-items:center;padding:12px 4px 0;font-size:13px}
|
|
98
|
+
.moreb{padding:8px 16px;border:1px solid var(--line);background:#fff;border-radius:9px;cursor:pointer;font-size:13px}
|
|
99
|
+
.moreb:hover{border-color:var(--ink)}
|
|
100
|
+
@media (max-width:680px){ .bitem .dates{display:none} .bitem .cnt{min-width:auto} }
|
|
101
|
+
.share{display:flex;flex-wrap:wrap;align-items:center;gap:10px;padding:16px 28px;border-top:1px solid var(--line)}
|
|
102
|
+
.badgeimg{height:20px}
|
|
103
|
+
.sbtn{padding:8px 13px;border:1px solid var(--ink);background:var(--ink);color:#fff;border-radius:9px;
|
|
104
|
+
font-size:13px;cursor:pointer;text-decoration:none}
|
|
105
|
+
.sbtn.ghost{background:#fff;color:var(--ink)}
|
|
106
|
+
.spin{display:inline-block;width:15px;height:15px;border:2px solid #fff;border-top-color:transparent;
|
|
107
|
+
border-radius:50%;animation:s .7s linear infinite;vertical-align:-2px;margin-right:7px}
|
|
108
|
+
@keyframes s{to{transform:rotate(360deg)}}
|
|
109
|
+
.muted{color:var(--sub)}
|
|
110
|
+
/* crisper section separation */
|
|
111
|
+
.card .top{background:linear-gradient(#fff,#fcfcfd)}
|
|
112
|
+
.row .k{font-weight:560}
|
|
113
|
+
/* ---- mobile / cross-browser responsive ---- */
|
|
114
|
+
@media (max-width:680px){
|
|
115
|
+
.wrap{padding:0 16px}
|
|
116
|
+
header{padding:48px 0 26px}
|
|
117
|
+
h1{font-size:32px}
|
|
118
|
+
.lede{font-size:16px}
|
|
119
|
+
form{flex-direction:column}
|
|
120
|
+
button{width:100%}
|
|
121
|
+
.steps{gap:8px 12px;font-size:12.5px}
|
|
122
|
+
.card .top{flex-direction:row;gap:14px;padding:18px}
|
|
123
|
+
.grade{width:56px;height:56px;font-size:28px;border-radius:14px}
|
|
124
|
+
.top .repo{font-size:17px}
|
|
125
|
+
.rows{padding:4px 18px 10px}
|
|
126
|
+
.row{flex-direction:column;gap:4px;padding:13px 0}
|
|
127
|
+
.row .k{width:auto}
|
|
128
|
+
.foot,.share{padding:14px 18px}
|
|
129
|
+
.share{gap:8px}
|
|
130
|
+
.sbtn{flex:1;text-align:center}
|
|
131
|
+
}
|
|
132
|
+
@media (max-width:380px){ h1{font-size:27px} .grade{width:48px;height:48px;font-size:24px} }
|
|
133
|
+
</style>
|
|
134
|
+
</head>
|
|
135
|
+
<body>
|
|
136
|
+
<div class="wrap">
|
|
137
|
+
<header>
|
|
138
|
+
<div class="mark">Mneme · Repo X-Ray</div>
|
|
139
|
+
<h1>See the truth about any repo.</h1>
|
|
140
|
+
<p class="lede">Paste a public repo. Get a signed, reproducible X-Ray — dependencies, secrets, bus factor, vitality, complexity. No source leaves the machine. No AI guesses anything.</p>
|
|
141
|
+
<form id="f">
|
|
142
|
+
<input id="u" placeholder="https://github.com/owner/repo" autocomplete="off" spellcheck="false" />
|
|
143
|
+
<button id="go" type="submit">X-Ray</button>
|
|
144
|
+
</form>
|
|
145
|
+
<p class="hint">Every number is <b>reproducible</b> from git / AST / npm metadata and sealed with an <b>offline-verifiable</b> signature.</p>
|
|
146
|
+
<div class="steps">
|
|
147
|
+
<span><b>1</b> Paste a public repo URL</span>
|
|
148
|
+
<span><b>2</b> Get a signed grade in seconds</span>
|
|
149
|
+
<span><b>3</b> Private repo? run <code>npx mneme-xray ./path --publish</code> — source never leaves your machine</span>
|
|
150
|
+
</div>
|
|
151
|
+
<details class="keybox">
|
|
152
|
+
<summary>Have an X-Ray key? Sign in to keep your reports</summary>
|
|
153
|
+
<div class="keyrow">
|
|
154
|
+
<input id="key" placeholder="your key (any secret string you choose)" autocomplete="off" />
|
|
155
|
+
<button id="savekey" type="button">Save</button>
|
|
156
|
+
<span id="keystate" class="kstate"></span>
|
|
157
|
+
</div>
|
|
158
|
+
</details>
|
|
159
|
+
<p class="err" id="err" style="display:none"></p>
|
|
160
|
+
</header>
|
|
161
|
+
|
|
162
|
+
<div id="out"></div>
|
|
163
|
+
|
|
164
|
+
<div class="board">
|
|
165
|
+
<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>
|
|
166
|
+
<div id="board"></div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
<footer>Mneme — the trust & cost layer for code. Deterministic. Signed. Local-first.</footer>
|
|
170
|
+
|
|
171
|
+
<script src="/card.js"></script>
|
|
172
|
+
<script>
|
|
173
|
+
const gC = g => "g-" + (("ABCDEF".includes(g)) ? g : "C");
|
|
174
|
+
const esc = s => String(s==null?"":s).replace(/[&<>]/g, c => ({"&":"&","<":"<",">":">"}[c]));
|
|
175
|
+
|
|
176
|
+
function render(signed){
|
|
177
|
+
document.getElementById("out").innerHTML = window.MnemeXRay.xrayCardHTML(signed, { share: true });
|
|
178
|
+
mountShare(signed.report);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// share row: permalink + an embeddable badge (the viral loop)
|
|
182
|
+
function mountShare(r){
|
|
183
|
+
const el = document.getElementById("share"); if(!el) return;
|
|
184
|
+
const origin = location.origin;
|
|
185
|
+
const link = `${origin}/r/${r.fingerprint}`;
|
|
186
|
+
let badgeTarget = r.fingerprint;
|
|
187
|
+
if (r.subject.kind === "git-url"){
|
|
188
|
+
const m = String(r.subject.ref).replace(/\.git$/,"").match(/(github|gitlab|bitbucket)\.[a-z]+\/([^/]+\/[^/]+)/);
|
|
189
|
+
if (m) badgeTarget = `${m[1]}/${m[2]}`;
|
|
190
|
+
}
|
|
191
|
+
const badge = `${origin}/badge/${badgeTarget}.svg`;
|
|
192
|
+
const md = `[](${link})`;
|
|
193
|
+
el.innerHTML = `
|
|
194
|
+
<img class="badgeimg" src="${badge}" alt="X-Ray badge"/>
|
|
195
|
+
<button class="sbtn" data-copy="${esc(md)}">Copy badge markdown</button>
|
|
196
|
+
<button class="sbtn ghost" data-copy="${esc(link)}">Copy permalink</button>
|
|
197
|
+
<a class="sbtn ghost" href="${link}" target="_blank" rel="noopener">Open ↗</a>`;
|
|
198
|
+
el.querySelectorAll("[data-copy]").forEach(b=>b.addEventListener("click",async()=>{
|
|
199
|
+
try{ await navigator.clipboard.writeText(b.dataset.copy); const t=b.textContent; b.textContent="Copied ✓"; setTimeout(()=>b.textContent=t,1200);}catch{}
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---- key / profile (optional sign-in; the key is a secret string the user chooses) ----
|
|
204
|
+
const getKey = () => localStorage.getItem("xrayKey") || "";
|
|
205
|
+
async function profileId(token){
|
|
206
|
+
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode("mneme-xray-profile:"+token));
|
|
207
|
+
return [...new Uint8Array(buf)].map(b=>b.toString(16).padStart(2,"0")).join("").slice(0,16);
|
|
208
|
+
}
|
|
209
|
+
function refreshKeyState(){
|
|
210
|
+
const k=getKey(), st=document.getElementById("keystate"), tab=document.getElementById("tabMine");
|
|
211
|
+
document.getElementById("key").value = k;
|
|
212
|
+
st.textContent = k ? "signed in ✓" : "";
|
|
213
|
+
tab.style.display = k ? "inline-block" : "none";
|
|
214
|
+
}
|
|
215
|
+
document.getElementById("savekey").addEventListener("click", ()=>{
|
|
216
|
+
const k=document.getElementById("key").value.trim();
|
|
217
|
+
if(k) localStorage.setItem("xrayKey",k); else localStorage.removeItem("xrayKey");
|
|
218
|
+
refreshKeyState(); loadList();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
let tab="board", offset=0, loaded=[];
|
|
222
|
+
const PAGE=20;
|
|
223
|
+
function fmtDate(s){ if(!s) return "—"; const d=new Date(s); return isNaN(d)?"—":d.toLocaleDateString(undefined,{year:"numeric",month:"short",day:"numeric"}); }
|
|
224
|
+
function rowHTML(b){
|
|
225
|
+
const lock = b.visibility==="private" ? '<span class="lock" title="private — only you can open this">🔒</span>' : "";
|
|
226
|
+
const scans = b.count>1 ? `${b.count} scans` : `1 scan`;
|
|
227
|
+
return `<div class="bitem" data-fp="${esc(b.fingerprint)}" data-priv="${b.visibility==="private"?1:0}">
|
|
228
|
+
<span class="bg ${gC(b.grade)}">${esc(b.grade)}</span>
|
|
229
|
+
<span class="nm">${lock}${esc(b.repoName)}</span>
|
|
230
|
+
<span class="dates"><span>first <b>${fmtDate(b.firstAt)}</b></span><span>latest <b>${fmtDate(b.lastAt)}</b></span></span>
|
|
231
|
+
<span class="cnt">${scans}</span><span class="arr">↗</span></div>`;
|
|
232
|
+
}
|
|
233
|
+
async function fetchPage(off){
|
|
234
|
+
if(tab==="mine"){
|
|
235
|
+
const k=getKey(); if(!k) return {items:[],total:0};
|
|
236
|
+
const id=await profileId(k);
|
|
237
|
+
return await (await fetch(`/api/profile/${id}?offset=${off}&limit=${PAGE}`)).json();
|
|
238
|
+
}
|
|
239
|
+
return await (await fetch(`/api/board?offset=${off}&limit=${PAGE}`)).json();
|
|
240
|
+
}
|
|
241
|
+
async function loadList(reset=true){
|
|
242
|
+
const el=document.getElementById("board");
|
|
243
|
+
if(reset){ offset=0; loaded=[]; el.innerHTML='<div class="bitem muted">Loading…</div>'; }
|
|
244
|
+
try{
|
|
245
|
+
const data=await fetchPage(offset);
|
|
246
|
+
loaded=loaded.concat(data.items||[]);
|
|
247
|
+
if(loaded.length===0){
|
|
248
|
+
el.innerHTML=`<div class="bitem muted">${tab==="mine"?"No reports yet. Run an X-Ray while signed in, or <code>mneme-xray ./repo --publish</code> a private repo.":"No reports yet — be the first."}</div>`;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const more = (data.offset+data.limit) < data.total;
|
|
252
|
+
el.innerHTML = `<div class="listbox">${loaded.map(rowHTML).join("")}</div>`
|
|
253
|
+
+ `<div class="listfoot"><span class="muted">${loaded.length} of ${data.total} repos</span>${more?'<button class="moreb" id="moreb">Load more</button>':""}</div>`;
|
|
254
|
+
el.querySelectorAll(".bitem[data-fp]").forEach(n=>n.addEventListener("click",()=>openReport(n.dataset.fp, n.dataset.priv==="1")));
|
|
255
|
+
const mb=document.getElementById("moreb"); if(mb) mb.addEventListener("click",()=>{ offset+=PAGE; loadList(false); });
|
|
256
|
+
}catch{ el.innerHTML='<div class="bitem muted">Could not load.</div>'; }
|
|
257
|
+
}
|
|
258
|
+
async function openReport(fp, isPrivate){
|
|
259
|
+
if(!fp) return;
|
|
260
|
+
try{
|
|
261
|
+
const headers={}; const k=getKey();
|
|
262
|
+
if(isPrivate && k) headers.authorization="Bearer "+k; // private: authenticated fetch, never a public link
|
|
263
|
+
const res=await fetch("/api/report/"+encodeURIComponent(fp),{headers});
|
|
264
|
+
if(!res.ok){ alert(isPrivate?"This is a private report — open it while signed in with the key that created it.":"Report not found."); return; }
|
|
265
|
+
const signed=await res.json();
|
|
266
|
+
if(signed&&signed.report){ render(signed); window.scrollTo({top:0,behavior:"smooth"}); }
|
|
267
|
+
}catch{}
|
|
268
|
+
}
|
|
269
|
+
document.getElementById("tabBoard").addEventListener("click",e=>{tab="board";setTab(e.target);loadList();});
|
|
270
|
+
document.getElementById("tabMine").addEventListener("click",e=>{tab="mine";setTab(e.target);loadList();});
|
|
271
|
+
function setTab(t){document.querySelectorAll(".tab").forEach(x=>x.classList.remove("on"));t.classList.add("on");}
|
|
272
|
+
|
|
273
|
+
document.getElementById("f").addEventListener("submit", async e=>{
|
|
274
|
+
e.preventDefault();
|
|
275
|
+
const u = document.getElementById("u").value.trim();
|
|
276
|
+
const err = document.getElementById("err"), go = document.getElementById("go");
|
|
277
|
+
err.style.display="none"; document.getElementById("out").innerHTML="";
|
|
278
|
+
if(!u) return;
|
|
279
|
+
go.disabled=true; go.innerHTML='<span class="spin"></span>X-raying…';
|
|
280
|
+
try{
|
|
281
|
+
const headers={"content-type":"application/json"};
|
|
282
|
+
const k=getKey(); if(k) headers.authorization="Bearer "+k; // also files under your profile
|
|
283
|
+
const res = await fetch("/api/xray",{method:"POST",headers,body:JSON.stringify({gitUrl:u})});
|
|
284
|
+
const data = await res.json();
|
|
285
|
+
if(!res.ok){ err.textContent = data.error || "failed"; err.style.display="block"; }
|
|
286
|
+
else { render(data); loadList(); }
|
|
287
|
+
}catch(ex){ err.textContent = String(ex); err.style.display="block"; }
|
|
288
|
+
finally{ go.disabled=false; go.textContent="X-Ray"; }
|
|
289
|
+
});
|
|
290
|
+
refreshKeyState();
|
|
291
|
+
loadList();
|
|
292
|
+
</script>
|
|
293
|
+
</body>
|
|
294
|
+
</html>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Mneme · Repo X-Ray</title>
|
|
7
|
+
<link rel="icon" href="/favicon.svg" />
|
|
8
|
+
<!--OGMETA-->
|
|
9
|
+
<style>
|
|
10
|
+
:root{--ink:#0a0a0a;--sub:#6b7280;--line:#ececec;--soft:#fafafa;--green:#16a34a;--amber:#d97706;--red:#dc2626}
|
|
11
|
+
*{box-sizing:border-box}
|
|
12
|
+
body{margin:0;background:#fff;color:var(--ink);font:16px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,Roboto,sans-serif;-webkit-font-smoothing:antialiased}
|
|
13
|
+
.wrap{max-width:760px;margin:0 auto;padding:46px 22px 70px}
|
|
14
|
+
.nav{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}
|
|
15
|
+
.mark{font-size:13px;letter-spacing:.18em;text-transform:uppercase;color:var(--sub)}
|
|
16
|
+
.home{font-size:13px;color:var(--ink);text-decoration:none;border:1px solid var(--line);border-radius:9px;padding:8px 13px}
|
|
17
|
+
.card{border:1px solid var(--line);border-radius:18px;overflow:hidden;box-shadow:0 1px 2px rgba(0,0,0,.03),0 8px 30px rgba(0,0,0,.04)}
|
|
18
|
+
.card .top{display:flex;align-items:center;gap:20px;padding:26px 28px;border-bottom:1px solid var(--line)}
|
|
19
|
+
.grade{width:78px;height:78px;border-radius:18px;display:grid;place-items:center;font-size:40px;font-weight:720;color:#fff;flex:none}
|
|
20
|
+
.g-A{background:var(--green)}.g-B{background:#65a30d}.g-C{background:var(--amber)}.g-D{background:#ea580c}.g-F{background:var(--red)}
|
|
21
|
+
.top .repo{font-size:21px;font-weight:620;word-break:break-all}.top .head{color:var(--sub);font-size:14.5px;margin-top:2px}
|
|
22
|
+
.rows{padding:8px 28px 14px}
|
|
23
|
+
.row{display:flex;gap:14px;padding:15px 0;border-bottom:1px solid var(--soft);align-items:baseline}.row:last-child{border-bottom:0}
|
|
24
|
+
.row .k{font-size:13px;letter-spacing:.04em;text-transform:uppercase;color:var(--sub);width:128px;flex:none}
|
|
25
|
+
.row .v{font-size:15.5px}.row .v .big{font-weight:640}
|
|
26
|
+
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
|
|
27
|
+
.chip{font-size:12.5px;padding:3px 9px;border-radius:999px;background:var(--soft);border:1px solid var(--line);color:#374151}
|
|
28
|
+
.chip.bad{background:#fef2f2;border-color:#fee2e2;color:var(--red)}.chip.warn{background:#fffbeb;border-color:#fef3c7;color:var(--amber)}
|
|
29
|
+
.foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:16px 28px;background:var(--soft);font-size:13px;color:var(--sub);flex-wrap:wrap}
|
|
30
|
+
.verified{display:inline-flex;align-items:center;gap:7px;color:var(--green);font-weight:560}
|
|
31
|
+
.dot{width:8px;height:8px;border-radius:50%;background:var(--green)}
|
|
32
|
+
code{font-family:ui-monospace,Menlo,monospace;font-size:12.5px}
|
|
33
|
+
.share{display:flex;flex-wrap:wrap;align-items:center;gap:10px;padding:16px 28px;border-top:1px solid var(--line)}
|
|
34
|
+
.badgeimg{height:20px}
|
|
35
|
+
.sbtn{padding:8px 13px;border:1px solid var(--ink);background:var(--ink);color:#fff;border-radius:9px;font-size:13px;cursor:pointer;text-decoration:none}
|
|
36
|
+
.sbtn.ghost{background:#fff;color:var(--ink)}
|
|
37
|
+
.miss{color:var(--sub);text-align:center;padding:60px 0}
|
|
38
|
+
.vbadge{margin-top:16px;font-size:13px;color:var(--sub);text-align:center}
|
|
39
|
+
</style>
|
|
40
|
+
</head>
|
|
41
|
+
<body>
|
|
42
|
+
<div class="wrap">
|
|
43
|
+
<div class="nav"><span class="mark">Mneme · Repo X-Ray</span><a class="home" href="/">X-ray another repo →</a></div>
|
|
44
|
+
<div id="out"><div class="miss">Loading…</div></div>
|
|
45
|
+
<div class="vbadge" id="vb"></div>
|
|
46
|
+
</div>
|
|
47
|
+
<script src="/card.js"></script>
|
|
48
|
+
<script>
|
|
49
|
+
const esc = s => String(s==null?"":s).replace(/[&<>]/g, c=>({"&":"&","<":"<",">":">"}[c]));
|
|
50
|
+
const fp = decodeURIComponent(location.pathname.replace(/^\/r\//,"").replace(/\/$/,""));
|
|
51
|
+
function mountShare(r){
|
|
52
|
+
const el=document.getElementById("share"); if(!el) return;
|
|
53
|
+
const link=`${location.origin}/r/${r.fingerprint}`;
|
|
54
|
+
let t=r.fingerprint;
|
|
55
|
+
if(r.subject.kind==="git-url"){const m=String(r.subject.ref).replace(/\.git$/,"").match(/(github|gitlab|bitbucket)\.[a-z]+\/([^/]+\/[^/]+)/);if(m)t=`${m[1]}/${m[2]}`;}
|
|
56
|
+
const badge=`${location.origin}/badge/${t}.svg`, md=`[](${link})`;
|
|
57
|
+
el.innerHTML=`<img class="badgeimg" src="${badge}"/><button class="sbtn" data-c="${esc(md)}">Copy badge markdown</button><button class="sbtn ghost" data-c="${esc(link)}">Copy permalink</button>`;
|
|
58
|
+
el.querySelectorAll("[data-c]").forEach(b=>b.addEventListener("click",async()=>{try{await navigator.clipboard.writeText(b.dataset.c);const x=b.textContent;b.textContent="Copied ✓";setTimeout(()=>b.textContent=x,1200);}catch{}}));
|
|
59
|
+
}
|
|
60
|
+
(async()=>{
|
|
61
|
+
try{
|
|
62
|
+
const res=await fetch("/api/report/"+encodeURIComponent(fp));
|
|
63
|
+
if(!res.ok){document.getElementById("out").innerHTML='<div class="miss">Report not found. <a href="/">X-ray a repo →</a></div>';return;}
|
|
64
|
+
const signed=await res.json();
|
|
65
|
+
document.getElementById("out").innerHTML=window.MnemeXRay.xrayCardHTML(signed,{share:true});
|
|
66
|
+
mountShare(signed.report);
|
|
67
|
+
// verify the signature live, in the visitor's browser
|
|
68
|
+
try{
|
|
69
|
+
const v=await (await fetch("/api/verify",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(signed)})).json();
|
|
70
|
+
document.getElementById("vb").textContent = v.valid ? "✓ signature verified" : "✗ signature invalid: "+v.reason;
|
|
71
|
+
}catch{}
|
|
72
|
+
}catch(e){document.getElementById("out").innerHTML='<div class="miss">Error loading report.</div>';}
|
|
73
|
+
})();
|
|
74
|
+
</script>
|
|
75
|
+
</body>
|
|
76
|
+
</html>
|