@goplus/agentguard 1.0.9 → 1.0.11
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 +40 -0
- package/package.json +1 -1
- package/skills/agentguard/README.md +31 -0
- package/skills/agentguard/SKILL.md +204 -3
- package/skills/agentguard/scripts/checkup-report.js +1344 -0
|
@@ -0,0 +1,1344 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GoPlus AgentGuard — Checkup Report Generator
|
|
5
|
+
*
|
|
6
|
+
* Reads checkup results as JSON from stdin, generates a self-contained HTML
|
|
7
|
+
* report with lobster mascot and opens it in the default browser.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* echo '{"composite_score":73,...}' | node scripts/checkup-report.js
|
|
11
|
+
*
|
|
12
|
+
* Output: prints the generated HTML file path to stdout.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
16
|
+
import { join, dirname } from 'node:path';
|
|
17
|
+
import { tmpdir, homedir } from 'node:os';
|
|
18
|
+
import { exec } from 'node:child_process';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
// Try to load favicon from agentguard-server or fallback
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
let faviconB64 = '';
|
|
24
|
+
const iconPaths = [
|
|
25
|
+
join(homedir(), 'code/agentguard-server/public/icon-192.png'),
|
|
26
|
+
join(__dirname, '../../assets/icon-192.png'),
|
|
27
|
+
];
|
|
28
|
+
for (const p of iconPaths) {
|
|
29
|
+
if (existsSync(p)) { faviconB64 = readFileSync(p).toString('base64'); break; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let input = '';
|
|
33
|
+
process.stdin.setEncoding('utf8');
|
|
34
|
+
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
35
|
+
process.stdin.on('end', () => {
|
|
36
|
+
try { generateReport(JSON.parse(input)); }
|
|
37
|
+
catch (err) { process.stderr.write(`Error: ${err.message}\n`); process.exit(1); }
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Helpers
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
function getTier(score) {
|
|
45
|
+
const pick = arr => arr[Math.floor(Math.random() * arr.length)];
|
|
46
|
+
if (score >= 90) return { grade: 'S', label: 'JACKED', color: '#00ffa3', quote: pick([
|
|
47
|
+
"Your agent is JACKED! 💪 Nothing gets past these claws!",
|
|
48
|
+
"Built different. This lobster lifts. 🏋️",
|
|
49
|
+
"Solid as a rock — this agent's security is chef's kiss! 🤌",
|
|
50
|
+
"Fort Knox? More like Fort Lobster. 🦞🔒",
|
|
51
|
+
"Peak performance. Your agent bench-presses threats for breakfast. 💪",
|
|
52
|
+
])};
|
|
53
|
+
if (score >= 70) return { grade: 'A', label: 'Healthy', color: '#98cbff', quote: pick([
|
|
54
|
+
"Looking solid! A few tweaks and you'll be unstoppable.",
|
|
55
|
+
"Almost there — one more push and this lobster gets abs! 🦞",
|
|
56
|
+
"Shield's up, claws sharp. Just needs a little polish. 🛡️",
|
|
57
|
+
"Your agent is in good shape — a little tuning and it's S-tier! ✨",
|
|
58
|
+
"Healthy and alert. This lobster runs 5K every morning. 🏃",
|
|
59
|
+
])};
|
|
60
|
+
if (score >= 50) return { grade: 'B', label: 'Tired', color: '#f0a830', quote: pick([
|
|
61
|
+
"Your agent needs a workout... and maybe some coffee. ☕",
|
|
62
|
+
"Sleepy lobster energy. Has potential, just needs motivation. 😴",
|
|
63
|
+
"Running on fumes — time to refuel this crustacean! ⛽",
|
|
64
|
+
"Your agent is binge-watching Netflix instead of patrolling. 📺",
|
|
65
|
+
"This lobster skipped leg day... and arm day... and every day. 🦞💤",
|
|
66
|
+
])};
|
|
67
|
+
return { grade: 'F', label: 'Critical', color: '#ffb4ab', quote: pick([
|
|
68
|
+
"CRITICAL CONDITION! This agent needs emergency care! 🚨",
|
|
69
|
+
"Code red! This lobster is on life support! 🏥",
|
|
70
|
+
"SOS! Your agent just texted 'send help' in morse code. 📡",
|
|
71
|
+
"Mayday mayday! This crustacean is going down! 🆘",
|
|
72
|
+
"Your agent's immune system has left the chat. 💀",
|
|
73
|
+
])};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const DIM_META = {
|
|
77
|
+
code_safety: { icon: 'find_in_page', name: 'Skill & Code Safety', zh: '技能与代码安全' },
|
|
78
|
+
credential_safety: { icon: 'key', name: 'Credential & Secrets', zh: '凭证与密钥安全' },
|
|
79
|
+
network_exposure: { icon: 'lan', name: 'Network & System', zh: '网络与系统暴露' },
|
|
80
|
+
runtime_protection: { icon: 'shield', name: 'Runtime Protection', zh: '运行时防护' },
|
|
81
|
+
web3_safety: { icon: 'token', name: 'Web3 Safety', zh: 'Web3 安全' },
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
85
|
+
|
|
86
|
+
function sevColor(s) {
|
|
87
|
+
s = (s||'').toUpperCase();
|
|
88
|
+
if (s === 'CRITICAL') return '#ffb4ab';
|
|
89
|
+
if (s === 'HIGH') return '#f0a830';
|
|
90
|
+
if (s === 'MEDIUM') return '#98cbff';
|
|
91
|
+
return '#849588';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function dimColor(score) {
|
|
95
|
+
if (score === null) return '#849588';
|
|
96
|
+
if (score >= 90) return '#00ffa3';
|
|
97
|
+
if (score >= 70) return '#00a2fd';
|
|
98
|
+
if (score >= 50) return '#f0a830';
|
|
99
|
+
return '#ffb4ab';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Pixel-art lobster SVG (inline, no external deps) — 5 variants per tier
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
function pixelLobster(grade, color) {
|
|
106
|
+
const R = '#e63946', R2 = '#d62839', R3 = '#c1121f', R4 = '#a30d1a';
|
|
107
|
+
const P1 = '#c4737b', P2 = '#a8636b', P3 = '#8a3040';
|
|
108
|
+
const T1 = '#d4545e', T2 = '#c1454f', T3 = '#a1101a';
|
|
109
|
+
|
|
110
|
+
const styles = `<style>
|
|
111
|
+
@keyframes pxBounce{0%,100%{transform:translateY(0)}40%{transform:translateY(-5px)}}
|
|
112
|
+
@keyframes pxBreath{0%,100%{transform:scaleY(1)}50%{transform:scaleY(1.02)}}
|
|
113
|
+
@keyframes pxSp{0%,100%{opacity:1}50%{opacity:.1}}
|
|
114
|
+
@keyframes pxBl{0%,92%,96%,100%{transform:scaleY(1)}94%{transform:scaleY(.05)}}
|
|
115
|
+
@keyframes pxSw{0%{opacity:.8;transform:translateY(0)}100%{opacity:0;transform:translateY(5px)}}
|
|
116
|
+
@keyframes pxSteam{0%{opacity:.4;transform:translateY(0)}100%{opacity:0;transform:translateY(-4px)}}
|
|
117
|
+
@keyframes pxAlarm{0%,100%{opacity:1}50%{opacity:.1}}
|
|
118
|
+
@keyframes pxFlex{0%,100%{transform:rotate(0)}25%{transform:rotate(-20deg)}75%{transform:rotate(5deg)}}
|
|
119
|
+
@keyframes pxFlexR{0%,100%{transform:rotate(0)}25%{transform:rotate(20deg)}75%{transform:rotate(-5deg)}}
|
|
120
|
+
@keyframes pxWag{0%,100%{transform:rotate(0)}50%{transform:rotate(6deg)}}
|
|
121
|
+
@keyframes pxNod{0%,100%{transform:translateY(0)}50%{transform:translateY(1px)}}
|
|
122
|
+
@keyframes pxCoffee{0%,100%{transform:rotate(0)}30%{transform:rotate(-10deg)}70%{transform:rotate(3deg)}}
|
|
123
|
+
@keyframes pxTremor{0%,100%{transform:translate(0,0)}25%{transform:translate(-1px,0)}50%{transform:translate(1px,-1px)}75%{transform:translate(-1px,1px)}}
|
|
124
|
+
@keyframes pxFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-3px)}}
|
|
125
|
+
@keyframes pxSpin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}
|
|
126
|
+
@keyframes pxPulse{0%,100%{opacity:.6}50%{opacity:1}}
|
|
127
|
+
@keyframes pxDrip{0%{opacity:.7;transform:translateY(0)}100%{opacity:0;transform:translateY(8px)}}
|
|
128
|
+
.px-bounce{animation:pxBounce 2s ease-in-out infinite}
|
|
129
|
+
.px-breath{animation:pxBreath 3s ease-in-out infinite;transform-origin:bottom}
|
|
130
|
+
.px-sparkle{animation:pxSp 1.3s ease-in-out infinite}
|
|
131
|
+
.px-blink{animation:pxBl 4s ease-in-out infinite;transform-origin:center}
|
|
132
|
+
.px-sweat{animation:pxSw 1.6s ease-in infinite}
|
|
133
|
+
.px-steam{animation:pxSteam 2s ease-out infinite}
|
|
134
|
+
.px-alarm{animation:pxAlarm .6s ease-in-out infinite}
|
|
135
|
+
.px-flex-l{animation:pxFlex 2.2s ease-in-out infinite;transform-origin:right center}
|
|
136
|
+
.px-flex-r{animation:pxFlexR 2.2s ease-in-out infinite;transform-origin:left center}
|
|
137
|
+
.px-wag{animation:pxWag 1.8s ease-in-out infinite;transform-origin:top center}
|
|
138
|
+
.px-nod{animation:pxNod 2.5s ease-in-out infinite}
|
|
139
|
+
.px-coffee{animation:pxCoffee 3s ease-in-out infinite;transform-origin:bottom left}
|
|
140
|
+
.px-tremor{animation:pxTremor .25s linear infinite}
|
|
141
|
+
.px-float{animation:pxFloat 3s ease-in-out infinite}
|
|
142
|
+
.px-spin{animation:pxSpin 4s linear infinite;transform-origin:center}
|
|
143
|
+
.px-pulse{animation:pxPulse 2s ease-in-out infinite}
|
|
144
|
+
.px-drip{animation:pxDrip 2s ease-in infinite}
|
|
145
|
+
</style>`;
|
|
146
|
+
|
|
147
|
+
const px = (x,y,w,h,c) => `<rect x="${x}" y="${y}" width="${w||1}" height="${h||1}" fill="${c}"/>`;
|
|
148
|
+
const pick = arr => arr[Math.floor(Math.random() * arr.length)];
|
|
149
|
+
|
|
150
|
+
// ── Shared body parts ──
|
|
151
|
+
const antennae = (c1,c2) => `<g class="px-wag">${px(15,0,1,1,c1)}${px(14,1,1,1,c1)}${px(13,2,1,2,c2)}${px(14,4,1,1,c1)}</g><g class="px-wag" style="animation-delay:.4s">${px(32,0,1,1,c1)}${px(33,1,1,1,c1)}${px(34,2,1,2,c2)}${px(33,4,1,1,c1)}</g>`;
|
|
152
|
+
const head = (c,nod='') => `<g ${nod}>${px(18,5,12,5,c)}${px(17,6,14,4,c)}${px(16,7,16,2,c)}`;
|
|
153
|
+
const eyes = () => `<g class="px-blink">${px(20,7,3,2,'#fff')}${px(25,7,3,2,'#fff')}</g>${px(21,8,1,1,'#1a1a2e')}${px(26,8,1,1,'#1a1a2e')}`;
|
|
154
|
+
const body = (c1,c2,dur='') => `<g class="px-breath"${dur?` style="animation-duration:${dur}"`:``}>${px(17,12,14,3,c1)}${px(16,13,16,2,c2)}${px(18,15,12,2,c1)}${px(17,16,14,1,c2)}${px(19,17,10,2,c2)}</g>`;
|
|
155
|
+
const legs = (c1,c2) => `${px(15,18,1,3,c1)}${px(14,20,1,2,c2)}${px(17,19,1,3,c1)}${px(16,21,1,2,c2)}${px(30,19,1,3,c1)}${px(31,21,1,2,c2)}${px(32,18,1,3,c1)}${px(33,20,1,2,c2)}`;
|
|
156
|
+
const tail = (c1,c2) => `<g class="px-wag" style="animation-duration:2.5s">${px(20,20,8,2,c1)}${px(19,22,10,1,c1)}${px(18,23,3,2,c2)}${px(22,23,4,2,c1)}${px(27,23,3,2,c2)}</g>`;
|
|
157
|
+
const clawL = (c1,c2,c3) => `${px(8,8,4,3,c1)}${px(6,9,3,3,c1)}${px(4,8,3,2,c2)}${px(3,7,2,2,c1)}${px(3,10,2,1,c3)}`;
|
|
158
|
+
const clawR = (c1,c2) => `${px(36,8,4,3,c1)}${px(38,9,3,3,c1)}${px(40,8,3,2,c2)}`;
|
|
159
|
+
|
|
160
|
+
// ════════════════════════════════════════════════
|
|
161
|
+
// S TIER VARIANTS (score >= 90)
|
|
162
|
+
// ════════════════════════════════════════════════
|
|
163
|
+
const sVariants = [
|
|
164
|
+
// 0: Crown + Sunglasses (original)
|
|
165
|
+
() => `<svg viewBox="-2 -8 52 56" xmlns="http://www.w3.org/2000/svg">${styles}
|
|
166
|
+
<g class="px-bounce">
|
|
167
|
+
<g class="px-wag">${px(14,0,1,1,R)}${px(13,1,1,1,R)}${px(12,2,1,1,R2)}${px(12,3,1,1,R2)}${px(13,4,1,1,R)}</g>
|
|
168
|
+
<g class="px-wag" style="animation-delay:.5s">${px(33,0,1,1,R)}${px(34,1,1,1,R)}${px(35,2,1,1,R2)}${px(35,3,1,1,R2)}${px(34,4,1,1,R)}</g>
|
|
169
|
+
<g class="px-wag" style="animation-duration:2.5s">${px(18,2,1,2,'#ffd700')}${px(21,1,1,2,'#ffd700')}${px(24,0,1,2,'#ffd700')}${px(27,1,1,2,'#ffd700')}${px(30,2,1,2,'#ffd700')}${px(17,4,14,1,'#ffd700')}${px(17,5,14,1,'#e6c200')}</g>
|
|
170
|
+
<rect x="8" y="8" width="1" height="1" fill="#ffd700" class="px-sparkle"/>
|
|
171
|
+
<rect x="39" y="7" width="1" height="1" fill="#ffd700" class="px-sparkle" style="animation-delay:.4s"/>
|
|
172
|
+
<rect x="5" y="14" width="1" height="1" fill="#fffbe6" class="px-sparkle" style="animation-delay:.8s"/>
|
|
173
|
+
<rect x="42" y="13" width="1" height="1" fill="#fffbe6" class="px-sparkle" style="animation-delay:1.2s"/>
|
|
174
|
+
<g class="px-nod">${px(18,6,12,6,R)}${px(17,7,14,5,R)}${px(16,8,16,3,R)}
|
|
175
|
+
${px(19,9,4,3,'#1a1a2e')}${px(25,9,4,3,'#1a1a2e')}${px(23,10,2,1,'#1a1a2e')}
|
|
176
|
+
${px(20,10,2,1,color+'88')}${px(26,10,2,1,color+'88')}
|
|
177
|
+
${px(20,13,1,1,'#fff')}${px(21,13,6,1,R3)}${px(27,13,1,1,'#fff')}
|
|
178
|
+
</g>
|
|
179
|
+
<g class="px-breath">${px(17,14,14,3,R)}${px(16,15,16,2,R2)}${px(18,17,12,2,R)}${px(17,18,14,1,R2)}${px(19,19,10,2,R2)}${px(18,20,12,1,R3)}
|
|
180
|
+
${px(21,15,2,1,R3)}${px(25,15,2,1,R3)}${px(21,18,2,1,R3)}${px(25,18,2,1,R3)}
|
|
181
|
+
</g>
|
|
182
|
+
<g class="px-flex-l">${px(8,9,4,3,R)}${px(6,10,3,4,R)}${px(4,9,3,3,R2)}${px(3,8,2,2,R)}${px(2,7,2,1,R)}${px(5,8,1,1,R3)}${px(3,12,2,1,R3)}${px(2,11,1,2,R)}</g>
|
|
183
|
+
<g class="px-flex-r" style="animation-delay:.3s">${px(36,9,4,3,R)}${px(39,10,3,4,R)}${px(41,9,3,3,R2)}${px(43,8,2,2,R)}${px(44,7,2,1,R)}${px(42,8,1,1,R3)}${px(43,12,2,1,R3)}${px(45,11,1,2,R)}</g>
|
|
184
|
+
${legs(R2,R3)}
|
|
185
|
+
<g class="px-wag">${px(20,22,8,2,R3)}${px(19,24,10,1,R3)}${px(18,25,3,2,R4)}${px(22,25,4,2,R3)}${px(27,25,3,2,R4)}${px(17,27,3,1,R4)}${px(21,27,6,1,R3)}${px(28,27,3,1,R4)}</g>
|
|
186
|
+
</g></svg>`,
|
|
187
|
+
|
|
188
|
+
// 1: Super Hero Cape
|
|
189
|
+
() => `<svg viewBox="-2 -6 52 52" xmlns="http://www.w3.org/2000/svg">${styles}
|
|
190
|
+
<g class="px-bounce">
|
|
191
|
+
${antennae(R,R2)}
|
|
192
|
+
${head(R,'class="px-nod"')}${eyes()}
|
|
193
|
+
${px(21,10,1,1,R3)}${px(22,11,4,1,R3)}${px(26,10,1,1,R3)}</g>
|
|
194
|
+
<!-- Cape -->
|
|
195
|
+
<g class="px-wag" style="animation-duration:2s">${px(15,10,1,12,'#4a3df7')}${px(14,11,1,11,'#4a3df7')}${px(13,12,1,10,'#3a2de7')}${px(12,14,1,8,'#3a2de7')}${px(33,10,1,12,'#4a3df7')}${px(34,11,1,11,'#4a3df7')}${px(35,12,1,10,'#3a2de7')}${px(36,14,1,8,'#3a2de7')}</g>
|
|
196
|
+
<!-- Star emblem on chest -->
|
|
197
|
+
${body(R,R2)}
|
|
198
|
+
${px(23,13,2,1,'#ffd700')}${px(22,14,4,1,'#ffd700')}${px(23,15,2,1,'#ffd700')}
|
|
199
|
+
${clawL(R,R2,R3)}${clawR(R,R2)}
|
|
200
|
+
${legs(R2,R3)}${tail(R3,R4)}
|
|
201
|
+
<rect x="7" y="3" width="1" height="1" fill="#ffd700" class="px-sparkle"/>
|
|
202
|
+
<rect x="40" y="5" width="1" height="1" fill="#ffd700" class="px-sparkle" style="animation-delay:.6s"/>
|
|
203
|
+
</g></svg>`,
|
|
204
|
+
|
|
205
|
+
// 2: Ninja lobster
|
|
206
|
+
() => `<svg viewBox="-2 -6 52 52" xmlns="http://www.w3.org/2000/svg">${styles}
|
|
207
|
+
<g class="px-bounce" style="animation-duration:1.5s">
|
|
208
|
+
${antennae(R,R2)}
|
|
209
|
+
${head(R,'class="px-nod"')}
|
|
210
|
+
<!-- Ninja mask -->
|
|
211
|
+
${px(17,7,14,2,'#2a2a3e')}
|
|
212
|
+
${px(20,7,3,2,'#fff')}${px(25,7,3,2,'#fff')}
|
|
213
|
+
${px(21,8,1,1,'#e63946')}${px(26,8,1,1,'#e63946')}
|
|
214
|
+
<!-- Headband tails -->
|
|
215
|
+
${px(31,7,3,1,'#2a2a3e')}${px(33,6,2,1,'#2a2a3e')}${px(34,5,2,1,'#2a2a3e')}
|
|
216
|
+
</g>
|
|
217
|
+
${body(R,R2)}
|
|
218
|
+
<!-- Shuriken in left claw -->
|
|
219
|
+
<g class="px-flex-l">${px(8,8,4,3,R)}${px(6,9,3,3,R)}${px(4,8,3,2,R2)}
|
|
220
|
+
<g class="px-spin" style="animation-duration:2s">${px(0,7,1,3,'#ccc')}${px(1,8,3,1,'#ccc')}${px(1,8,1,1,'#888')}</g>
|
|
221
|
+
</g>
|
|
222
|
+
<!-- Katana in right claw -->
|
|
223
|
+
<g class="px-flex-r">${clawR(R,R2)}
|
|
224
|
+
${px(42,3,1,8,'#c0c0c0')}${px(42,2,1,1,'#fff')}${px(41,11,3,1,'#8b6914')}
|
|
225
|
+
</g>
|
|
226
|
+
${legs(R2,R3)}${tail(R3,R4)}
|
|
227
|
+
</g></svg>`,
|
|
228
|
+
|
|
229
|
+
// 3: Astronaut lobster
|
|
230
|
+
() => `<svg viewBox="-2 -8 52 56" xmlns="http://www.w3.org/2000/svg">${styles}
|
|
231
|
+
<g class="px-float">
|
|
232
|
+
${antennae(R,R2)}
|
|
233
|
+
<!-- Helmet -->
|
|
234
|
+
${px(16,3,16,2,'#e0e8f0')}${px(15,5,18,8,'#e0e8f0')}${px(16,13,16,1,'#c0c8d0')}
|
|
235
|
+
<!-- Visor -->
|
|
236
|
+
${px(18,6,12,5,'#1a3a5c')}${px(19,7,10,3,'#244a6c')}
|
|
237
|
+
<!-- Eyes through visor -->
|
|
238
|
+
${px(20,8,2,1,'#fff')}${px(26,8,2,1,'#fff')}
|
|
239
|
+
<!-- Visor reflection -->
|
|
240
|
+
${px(19,7,2,1,'#ffffff40')}
|
|
241
|
+
${body(R,R2)}
|
|
242
|
+
<!-- Jetpack -->
|
|
243
|
+
${px(15,13,2,5,'#606870')}${px(31,13,2,5,'#606870')}
|
|
244
|
+
<rect x="14" y="18" width="1" height="2" fill="#f0a830" class="px-pulse"/>
|
|
245
|
+
<rect x="33" y="18" width="1" height="2" fill="#f0a830" class="px-pulse" style="animation-delay:.5s"/>
|
|
246
|
+
${clawL(R,R2,R3)}${clawR(R,R2)}
|
|
247
|
+
${legs(R2,R3)}${tail(R3,R4)}
|
|
248
|
+
<!-- Stars -->
|
|
249
|
+
<rect x="5" y="2" width="1" height="1" fill="#fff" class="px-sparkle"/>
|
|
250
|
+
<rect x="42" y="10" width="1" height="1" fill="#fff" class="px-sparkle" style="animation-delay:.7s"/>
|
|
251
|
+
<rect x="8" y="20" width="1" height="1" fill="#fff" class="px-sparkle" style="animation-delay:1.1s"/>
|
|
252
|
+
</g></svg>`,
|
|
253
|
+
|
|
254
|
+
// 4: DJ lobster with headphones
|
|
255
|
+
() => `<svg viewBox="-2 -6 52 52" xmlns="http://www.w3.org/2000/svg">${styles}
|
|
256
|
+
<g class="px-bounce" style="animation-duration:1.2s">
|
|
257
|
+
${antennae(R,R2)}
|
|
258
|
+
<!-- Headphones -->
|
|
259
|
+
${px(16,3,16,2,'#333')}${px(15,4,1,4,'#333')}${px(32,4,1,4,'#333')}
|
|
260
|
+
${px(13,5,3,4,'#555')}${px(14,6,2,2,'#00ffa3')}
|
|
261
|
+
${px(32,5,3,4,'#555')}${px(33,6,2,2,'#00ffa3')}
|
|
262
|
+
${head(R,'class="px-nod" style="animation-duration:1.2s"')}${eyes()}
|
|
263
|
+
${px(22,10,4,1,R3)}${px(21,10,1,1,R3)}${px(26,10,1,1,R3)}
|
|
264
|
+
</g>
|
|
265
|
+
${body(R,R2)}
|
|
266
|
+
<!-- Turntable in right claw -->
|
|
267
|
+
<g class="px-wag" style="animation-duration:1s">${clawR(R,R2)}
|
|
268
|
+
${px(40,6,6,6,'#333')}${px(41,7,4,4,'#444')}
|
|
269
|
+
<g class="px-spin" style="animation-duration:1.5s">${px(42,8,2,2,'#222')}${px(42,8,1,1,'#666')}</g>
|
|
270
|
+
</g>
|
|
271
|
+
${clawL(R,R2,R3)}
|
|
272
|
+
${legs(R2,R3)}${tail(R3,R4)}
|
|
273
|
+
<!-- Music notes -->
|
|
274
|
+
<rect x="6" y="3" width="1" height="1" fill="${color}" class="px-sparkle"/>
|
|
275
|
+
<rect x="10" y="1" width="1" height="1" fill="${color}" class="px-sparkle" style="animation-delay:.5s"/>
|
|
276
|
+
<rect x="38" y="1" width="1" height="1" fill="${color}" class="px-sparkle" style="animation-delay:1s"/>
|
|
277
|
+
</g></svg>`,
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
// ════════════════════════════════════════════════
|
|
281
|
+
// A TIER VARIANTS (score 70-89)
|
|
282
|
+
// ════════════════════════════════════════════════
|
|
283
|
+
const aVariants = [
|
|
284
|
+
// 0: Shield (original)
|
|
285
|
+
() => `<svg viewBox="-2 -8 52 54" xmlns="http://www.w3.org/2000/svg">${styles}
|
|
286
|
+
<g class="px-bounce" style="animation-duration:2.2s">
|
|
287
|
+
${antennae(R,R2)}${px(17,4,2,2,R2)}${px(29,4,2,2,R2)}
|
|
288
|
+
${head(R,'class="px-nod" style="animation-duration:3s"')}${eyes()}
|
|
289
|
+
${px(21,10,1,1,R3)}${px(22,11,4,1,R3)}${px(26,10,1,1,R3)}</g>
|
|
290
|
+
${body(R,R2,'3.5s')}
|
|
291
|
+
${clawL(R,R2,R3)}
|
|
292
|
+
<g class="px-wag" style="animation-duration:2s">${clawR(R,R2)}
|
|
293
|
+
${px(40,6,5,6,color)}${px(41,7,3,4,color+'cc')}${px(42,8,2,2,'#fff')}
|
|
294
|
+
</g>
|
|
295
|
+
${legs(R2,R3)}${tail(R3,R4)}
|
|
296
|
+
</g></svg>`,
|
|
297
|
+
|
|
298
|
+
// 1: Magnifying glass detective
|
|
299
|
+
() => `<svg viewBox="-2 -6 52 52" xmlns="http://www.w3.org/2000/svg">${styles}
|
|
300
|
+
<g class="px-bounce" style="animation-duration:2.5s">
|
|
301
|
+
${antennae(R,R2)}
|
|
302
|
+
<!-- Detective hat -->
|
|
303
|
+
${px(17,2,14,2,'#5c4033')}${px(15,4,18,1,'#4a3328')}${px(18,1,12,1,'#5c4033')}
|
|
304
|
+
${head(R,'class="px-nod"')}${eyes()}
|
|
305
|
+
${px(22,10,4,1,R3)}</g>
|
|
306
|
+
${body(R,R2)}
|
|
307
|
+
${clawL(R,R2,R3)}
|
|
308
|
+
<!-- Magnifying glass -->
|
|
309
|
+
<g class="px-wag" style="animation-duration:3s">${clawR(R,R2)}
|
|
310
|
+
${px(42,4,4,4,'#c0c0c0')}${px(43,5,2,2,'#87ceeb')}${px(41,8,1,3,'#8b6914')}
|
|
311
|
+
</g>
|
|
312
|
+
${legs(R2,R3)}${tail(R3,R4)}
|
|
313
|
+
</g></svg>`,
|
|
314
|
+
|
|
315
|
+
// 2: Thumbs up / checkmark lobster
|
|
316
|
+
() => `<svg viewBox="-2 -6 52 52" xmlns="http://www.w3.org/2000/svg">${styles}
|
|
317
|
+
<g class="px-bounce" style="animation-duration:2s">
|
|
318
|
+
${antennae(R,R2)}
|
|
319
|
+
${head(R,'class="px-nod"')}${eyes()}
|
|
320
|
+
<!-- Wink + smile -->
|
|
321
|
+
${px(20,7,3,1,'#fff')}${px(21,8,1,1,'#1a1a2e')}
|
|
322
|
+
${px(25,7,3,2,'#fff')}${px(26,8,1,1,'#1a1a2e')}
|
|
323
|
+
${px(21,10,1,1,R3)}${px(22,11,4,1,R3)}${px(26,10,1,1,R3)}
|
|
324
|
+
</g>
|
|
325
|
+
${body(R,R2)}
|
|
326
|
+
<!-- Left claw: thumbs up -->
|
|
327
|
+
<g class="px-flex-l">${px(8,8,4,3,R)}${px(6,9,3,3,R)}${px(4,8,3,2,R2)}${px(3,5,2,4,R)}${px(2,5,1,1,R2)}</g>
|
|
328
|
+
${clawR(R,R2)}
|
|
329
|
+
<!-- Checkmark badge -->
|
|
330
|
+
${px(40,6,5,5,color)}${px(41,7,3,3,color+'cc')}
|
|
331
|
+
${px(42,9,1,1,'#fff')}${px(43,8,1,1,'#fff')}${px(44,7,1,1,'#fff')}
|
|
332
|
+
${legs(R2,R3)}${tail(R3,R4)}
|
|
333
|
+
</g></svg>`,
|
|
334
|
+
|
|
335
|
+
// 3: Hard hat construction
|
|
336
|
+
() => `<svg viewBox="-2 -6 52 52" xmlns="http://www.w3.org/2000/svg">${styles}
|
|
337
|
+
<g class="px-bounce" style="animation-duration:2.2s">
|
|
338
|
+
${antennae(R,R2)}
|
|
339
|
+
<!-- Hard hat -->
|
|
340
|
+
${px(16,2,16,3,'#f0a830')}${px(15,5,18,1,'#d89520')}${px(22,1,4,1,'#f0a830')}
|
|
341
|
+
<!-- Light on hat -->
|
|
342
|
+
<rect x="23" y="0" width="2" height="1" fill="#fff" class="px-pulse"/>
|
|
343
|
+
${head(R,'class="px-nod"')}${eyes()}
|
|
344
|
+
${px(22,10,4,1,R3)}</g>
|
|
345
|
+
${body(R,R2)}
|
|
346
|
+
${clawL(R,R2,R3)}
|
|
347
|
+
<!-- Wrench in right claw -->
|
|
348
|
+
<g class="px-wag" style="animation-duration:2s">${clawR(R,R2)}
|
|
349
|
+
${px(42,5,2,7,'#808080')}${px(41,5,4,2,'#909090')}
|
|
350
|
+
</g>
|
|
351
|
+
${legs(R2,R3)}${tail(R3,R4)}
|
|
352
|
+
</g></svg>`,
|
|
353
|
+
|
|
354
|
+
// 4: Sword & shield warrior
|
|
355
|
+
() => `<svg viewBox="-2 -6 52 52" xmlns="http://www.w3.org/2000/svg">${styles}
|
|
356
|
+
<g class="px-bounce" style="animation-duration:2s">
|
|
357
|
+
${antennae(R,R2)}
|
|
358
|
+
<!-- Viking helmet -->
|
|
359
|
+
${px(17,2,14,3,'#808080')}${px(16,4,16,1,'#707070')}
|
|
360
|
+
${px(15,1,2,4,'#fff')}${px(31,1,2,4,'#fff')}${px(14,0,2,1,'#eee')}${px(32,0,2,1,'#eee')}
|
|
361
|
+
${head(R,'class="px-nod"')}${eyes()}
|
|
362
|
+
${px(22,10,4,1,R3)}${px(21,10,1,1,R3)}${px(26,10,1,1,R3)}</g>
|
|
363
|
+
${body(R,R2)}
|
|
364
|
+
<!-- Left: shield -->
|
|
365
|
+
<g class="px-wag">${px(4,7,5,7,color)}${px(5,8,3,5,color+'cc')}${px(6,10,1,1,'#fff')}</g>
|
|
366
|
+
<!-- Right: sword -->
|
|
367
|
+
<g class="px-flex-r">${clawR(R,R2)}
|
|
368
|
+
${px(43,2,1,9,'#c0c0c0')}${px(43,1,1,1,'#fff')}${px(41,11,5,1,'#8b6914')}${px(43,12,1,2,'#6b4914')}
|
|
369
|
+
</g>
|
|
370
|
+
${legs(R2,R3)}${tail(R3,R4)}
|
|
371
|
+
</g></svg>`,
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
// ════════════════════════════════════════════════
|
|
375
|
+
// B TIER VARIANTS (score 50-69)
|
|
376
|
+
// ════════════════════════════════════════════════
|
|
377
|
+
const bVariants = [
|
|
378
|
+
// 0: Coffee (original)
|
|
379
|
+
() => `<svg viewBox="-2 -8 52 52" xmlns="http://www.w3.org/2000/svg">${styles}
|
|
380
|
+
<g class="px-bounce" style="animation-duration:3.5s">
|
|
381
|
+
${px(14,3,1,1,T2)}${px(13,4,1,2,T2)}${px(14,6,1,1,T2)}
|
|
382
|
+
${px(33,3,1,1,T2)}${px(34,4,1,2,T2)}${px(33,6,1,1,T2)}
|
|
383
|
+
<rect x="35" y="4" width="1" height="1" fill="#58a6ff" class="px-sweat"/>
|
|
384
|
+
<rect x="36" y="6" width="1" height="1" fill="#58a6ff" class="px-sweat" style="animation-delay:.6s"/>
|
|
385
|
+
<g class="px-nod" style="animation-duration:4s">${px(18,6,12,5,T1)}${px(17,7,14,4,T1)}${px(16,8,16,2,T1)}
|
|
386
|
+
${px(20,8,3,2,'#fff')}${px(25,8,3,2,'#fff')}
|
|
387
|
+
<g class="px-blink" style="animation-duration:2s">${px(20,8,3,1,T1)}${px(25,8,3,1,T1)}</g>
|
|
388
|
+
${px(21,9,1,1,'#1a1a2e')}${px(26,9,1,1,'#1a1a2e')}
|
|
389
|
+
${px(22,11,4,1,T3)}
|
|
390
|
+
</g>
|
|
391
|
+
<g class="px-breath" style="animation-duration:4s">${px(17,12,14,3,T1)}${px(16,13,16,2,T2)}${px(18,15,12,2,T1)}${px(19,17,10,2,T2)}</g>
|
|
392
|
+
${px(8,10,4,3,T1)}${px(6,11,3,2,T2)}${px(4,10,3,2,T2)}${px(3,11,2,1,T1)}
|
|
393
|
+
<g class="px-coffee">${px(36,10,4,3,T1)}${px(38,11,3,2,T2)}
|
|
394
|
+
${px(39,8,4,5,'#8b6914')}${px(38,8,6,1,'#a07818')}
|
|
395
|
+
<rect x="40" y="5" width="1" height="3" fill="#ffffff30" class="px-steam"/>
|
|
396
|
+
<rect x="41" y="4" width="1" height="3" fill="#ffffff20" class="px-steam" style="animation-delay:.8s"/>
|
|
397
|
+
</g>
|
|
398
|
+
${legs(T2,T3)}
|
|
399
|
+
${px(20,20,8,2,T3)}${px(19,22,10,1,T3)}${px(18,23,3,1,T3)}${px(22,23,4,1,T3)}${px(27,23,3,1,T3)}
|
|
400
|
+
</g></svg>`,
|
|
401
|
+
|
|
402
|
+
// 1: Pillow / sleeping
|
|
403
|
+
() => `<svg viewBox="-2 -6 52 52" xmlns="http://www.w3.org/2000/svg">${styles}
|
|
404
|
+
<g class="px-nod" style="animation-duration:4s">
|
|
405
|
+
${px(14,4,1,2,T2)}${px(13,5,1,1,T2)}${px(33,4,1,2,T2)}${px(34,5,1,1,T2)}
|
|
406
|
+
${px(18,6,12,5,T1)}${px(17,7,14,4,T1)}${px(16,8,16,2,T1)}
|
|
407
|
+
<!-- Closed eyes (zzz) -->
|
|
408
|
+
${px(20,8,3,1,T3)}${px(25,8,3,1,T3)}
|
|
409
|
+
${px(22,11,4,1,T3)}
|
|
410
|
+
<!-- ZZZ -->
|
|
411
|
+
<rect x="34" y="2" width="3" height="1" fill="#849588" class="px-sparkle"/>
|
|
412
|
+
<rect x="36" y="0" width="4" height="1" fill="#849588" class="px-sparkle" style="animation-delay:.5s"/>
|
|
413
|
+
<rect x="38" y="-2" width="5" height="1" fill="#849588" class="px-sparkle" style="animation-delay:1s"/>
|
|
414
|
+
<g class="px-breath" style="animation-duration:5s">${px(17,12,14,3,T1)}${px(16,13,16,2,T2)}${px(18,15,12,2,T1)}${px(19,17,10,2,T2)}</g>
|
|
415
|
+
<!-- Pillow under head -->
|
|
416
|
+
${px(14,11,20,2,'#e8dcc8')}${px(15,10,18,1,'#f0e6d4')}
|
|
417
|
+
${px(8,10,4,3,T1)}${px(6,11,3,2,T2)}
|
|
418
|
+
${px(36,10,4,3,T1)}${px(38,11,3,2,T2)}
|
|
419
|
+
${legs(T2,T3)}
|
|
420
|
+
${px(20,20,8,2,T3)}${px(19,22,10,1,T3)}
|
|
421
|
+
</g></svg>`,
|
|
422
|
+
|
|
423
|
+
// 2: Umbrella in rain
|
|
424
|
+
() => `<svg viewBox="-2 -8 52 52" xmlns="http://www.w3.org/2000/svg">${styles}
|
|
425
|
+
<g class="px-nod" style="animation-duration:3s">
|
|
426
|
+
<!-- Umbrella -->
|
|
427
|
+
${px(18,-2,14,3,'#4a90d9')}${px(16,0,18,1,'#4a90d9')}${px(24,1,1,5,'#8b6914')}
|
|
428
|
+
<!-- Rain drops -->
|
|
429
|
+
<rect x="8" y="-1" width="1" height="2" fill="#58a6ff" class="px-drip"/>
|
|
430
|
+
<rect x="14" y="1" width="1" height="2" fill="#58a6ff" class="px-drip" style="animation-delay:.3s"/>
|
|
431
|
+
<rect x="38" y="0" width="1" height="2" fill="#58a6ff" class="px-drip" style="animation-delay:.6s"/>
|
|
432
|
+
<rect x="42" y="2" width="1" height="2" fill="#58a6ff" class="px-drip" style="animation-delay:.9s"/>
|
|
433
|
+
<rect x="5" y="4" width="1" height="2" fill="#58a6ff" class="px-drip" style="animation-delay:1.2s"/>
|
|
434
|
+
<rect x="44" y="5" width="1" height="2" fill="#58a6ff" class="px-drip" style="animation-delay:1.5s"/>
|
|
435
|
+
${px(14,4,1,2,T2)}${px(13,5,1,1,T2)}${px(33,4,1,2,T2)}${px(34,5,1,1,T2)}
|
|
436
|
+
${px(18,6,12,5,T1)}${px(17,7,14,4,T1)}${px(16,8,16,2,T1)}
|
|
437
|
+
${px(20,8,3,2,'#fff')}${px(25,8,3,2,'#fff')}${px(21,9,1,1,'#1a1a2e')}${px(26,9,1,1,'#1a1a2e')}
|
|
438
|
+
${px(22,11,4,1,T3)}
|
|
439
|
+
<g class="px-breath" style="animation-duration:4s">${px(17,12,14,3,T1)}${px(16,13,16,2,T2)}${px(18,15,12,2,T1)}${px(19,17,10,2,T2)}</g>
|
|
440
|
+
${px(8,10,4,3,T1)}${px(6,11,3,2,T2)}${px(4,10,3,2,T2)}
|
|
441
|
+
${px(36,10,4,3,T1)}${px(38,11,3,2,T2)}
|
|
442
|
+
${legs(T2,T3)}
|
|
443
|
+
${px(20,20,8,2,T3)}${px(19,22,10,1,T3)}
|
|
444
|
+
</g></svg>`,
|
|
445
|
+
|
|
446
|
+
// 3: Band-aid / slightly hurt
|
|
447
|
+
() => `<svg viewBox="-2 -6 52 52" xmlns="http://www.w3.org/2000/svg">${styles}
|
|
448
|
+
<g class="px-bounce" style="animation-duration:3s">
|
|
449
|
+
${px(14,4,1,2,T2)}${px(13,5,1,1,T2)}${px(33,4,1,2,T2)}${px(34,5,1,1,T2)}
|
|
450
|
+
<rect x="35" y="3" width="1" height="1" fill="#58a6ff" class="px-sweat"/>
|
|
451
|
+
${px(18,6,12,5,T1)}${px(17,7,14,4,T1)}${px(16,8,16,2,T1)}
|
|
452
|
+
<!-- Band-aid on head -->
|
|
453
|
+
${px(27,5,5,1,'#f5d0a9')}${px(28,4,3,3,'#f5d0a9')}${px(29,5,1,1,'#d4a574')}
|
|
454
|
+
${px(20,8,3,2,'#fff')}${px(25,8,3,2,'#fff')}${px(21,9,1,1,'#1a1a2e')}${px(26,9,1,1,'#1a1a2e')}
|
|
455
|
+
<!-- Slightly sad -->
|
|
456
|
+
${px(22,11,1,1,T3)}${px(23,11,2,1,T3)}${px(25,11,1,1,T3)}
|
|
457
|
+
<g class="px-breath" style="animation-duration:4s">${px(17,12,14,3,T1)}${px(16,13,16,2,T2)}${px(18,15,12,2,T1)}${px(19,17,10,2,T2)}</g>
|
|
458
|
+
${px(8,10,4,3,T1)}${px(6,11,3,2,T2)}${px(4,10,3,2,T2)}
|
|
459
|
+
${px(36,10,4,3,T1)}${px(38,11,3,2,T2)}
|
|
460
|
+
${legs(T2,T3)}
|
|
461
|
+
${px(20,20,8,2,T3)}${px(19,22,10,1,T3)}
|
|
462
|
+
</g></svg>`,
|
|
463
|
+
|
|
464
|
+
// 4: Yawning lobster
|
|
465
|
+
() => `<svg viewBox="-2 -6 52 52" xmlns="http://www.w3.org/2000/svg">${styles}
|
|
466
|
+
<g class="px-nod" style="animation-duration:5s">
|
|
467
|
+
${px(14,3,1,2,T2)}${px(13,4,1,2,T2)}${px(33,3,1,2,T2)}${px(34,4,1,2,T2)}
|
|
468
|
+
${px(18,6,12,5,T1)}${px(17,7,14,4,T1)}${px(16,8,16,2,T1)}
|
|
469
|
+
<!-- Squinting eyes -->
|
|
470
|
+
${px(20,8,3,1,'#fff')}${px(25,8,3,1,'#fff')}
|
|
471
|
+
${px(20,8,3,1,T2)}${px(25,8,3,1,T2)}
|
|
472
|
+
<!-- Big open yawn mouth -->
|
|
473
|
+
${px(21,10,6,3,'#8a3040')}${px(22,10,4,1,'#fff')}
|
|
474
|
+
<g class="px-breath" style="animation-duration:5s">${px(17,12,14,3,T1)}${px(16,13,16,2,T2)}${px(18,15,12,2,T1)}${px(19,17,10,2,T2)}</g>
|
|
475
|
+
<!-- One claw covering yawn -->
|
|
476
|
+
<g class="px-coffee" style="animation-duration:5s">${px(8,8,4,3,T1)}${px(6,9,3,3,T1)}${px(4,8,3,2,T2)}${px(6,7,3,1,T2)}</g>
|
|
477
|
+
${px(36,10,4,3,T1)}${px(38,11,3,2,T2)}
|
|
478
|
+
${legs(T2,T3)}
|
|
479
|
+
${px(20,20,8,2,T3)}${px(19,22,10,1,T3)}
|
|
480
|
+
</g></svg>`,
|
|
481
|
+
];
|
|
482
|
+
|
|
483
|
+
// ════════════════════════════════════════════════
|
|
484
|
+
// F TIER VARIANTS (score 0-49)
|
|
485
|
+
// ════════════════════════════════════════════════
|
|
486
|
+
const fVariants = [
|
|
487
|
+
// 0: Bandage + Thermometer (original)
|
|
488
|
+
() => `<svg viewBox="-2 -8 52 52" xmlns="http://www.w3.org/2000/svg">${styles}
|
|
489
|
+
<g class="px-tremor">
|
|
490
|
+
<rect x="4" y="7" width="2" height="2" fill="#f85149" class="px-alarm"/>
|
|
491
|
+
<rect x="42" y="6" width="2" height="2" fill="#f85149" class="px-alarm" style="animation-delay:.3s"/>
|
|
492
|
+
<rect x="2" y="13" width="1" height="1" fill="#f85149" class="px-alarm" style="animation-delay:.6s"/>
|
|
493
|
+
<rect x="45" y="12" width="1" height="1" fill="#f85149" class="px-alarm" style="animation-delay:.9s"/>
|
|
494
|
+
${px(14,5,1,1,P2)}${px(13,6,1,1,P2)}${px(33,5,1,1,P2)}${px(34,6,1,1,P2)}
|
|
495
|
+
${px(18,6,12,5,P1)}${px(17,7,14,4,P1)}${px(16,8,16,2,P1)}
|
|
496
|
+
${px(20,5,8,1,'#fff')}${px(23,4,2,3,'#fff')}${px(23,5,2,1,'#e63946')}
|
|
497
|
+
${px(20,8,1,1,'#e63946')}${px(22,8,1,1,'#e63946')}${px(21,9,1,1,'#e63946')}${px(20,10,1,1,'#e63946')}${px(22,10,1,1,'#e63946')}
|
|
498
|
+
${px(26,8,1,1,'#e63946')}${px(28,8,1,1,'#e63946')}${px(27,9,1,1,'#e63946')}${px(26,10,1,1,'#e63946')}${px(28,10,1,1,'#e63946')}
|
|
499
|
+
${px(29,11,6,1,'#58a6ff')}${px(35,10,2,3,'#f85149')}
|
|
500
|
+
${px(22,12,1,1,P3)}${px(23,11,2,1,P3)}${px(25,12,1,1,P3)}
|
|
501
|
+
<g class="px-breath" style="animation-duration:5s">${px(17,13,14,3,P1)}${px(16,14,16,2,P2)}${px(18,16,12,2,P1)}${px(19,18,10,2,P2)}
|
|
502
|
+
${px(16,14,10,1,'#ffffff30')}${px(18,17,8,1,'#ffffff30')}
|
|
503
|
+
</g>
|
|
504
|
+
${px(8,13,4,2,P1)}${px(6,14,3,2,P2)}${px(4,15,3,1,P2)}
|
|
505
|
+
${px(36,13,4,2,P1)}${px(38,14,3,2,P2)}${px(41,15,3,1,P2)}
|
|
506
|
+
${px(15,19,1,2,P2)}${px(14,20,1,2,P3)}${px(17,19,1,2,P2)}${px(16,20,1,2,P3)}
|
|
507
|
+
${px(30,19,1,2,P2)}${px(31,20,1,2,P3)}${px(32,19,1,2,P2)}${px(33,20,1,2,P3)}
|
|
508
|
+
${px(20,20,8,2,P3)}${px(19,22,10,1,P3)}${px(18,23,3,1,P3)}${px(22,23,4,1,P3)}${px(27,23,3,1,P3)}
|
|
509
|
+
</g></svg>`,
|
|
510
|
+
|
|
511
|
+
// 1: Hospital bed
|
|
512
|
+
() => `<svg viewBox="-2 -6 52 52" xmlns="http://www.w3.org/2000/svg">${styles}
|
|
513
|
+
<g class="px-tremor" style="animation-duration:.5s">
|
|
514
|
+
<!-- Bed frame -->
|
|
515
|
+
${px(8,22,32,2,'#666')}${px(6,18,2,6,'#888')}${px(40,18,2,6,'#888')}
|
|
516
|
+
<!-- Mattress -->
|
|
517
|
+
${px(10,18,28,4,'#e8e0d0')}
|
|
518
|
+
<!-- Pillow -->
|
|
519
|
+
${px(10,16,8,3,'#f5efe5')}
|
|
520
|
+
<!-- Lobster in bed -->
|
|
521
|
+
${px(14,14,1,1,P2)}${px(13,15,1,1,P2)}${px(33,14,1,1,P2)}${px(34,15,1,1,P2)}
|
|
522
|
+
${px(18,14,12,5,P1)}${px(17,15,14,4,P1)}${px(16,16,16,2,P1)}
|
|
523
|
+
<!-- X eyes -->
|
|
524
|
+
${px(20,16,1,1,'#e63946')}${px(22,16,1,1,'#e63946')}${px(21,17,1,1,'#e63946')}
|
|
525
|
+
${px(26,16,1,1,'#e63946')}${px(28,16,1,1,'#e63946')}${px(27,17,1,1,'#e63946')}
|
|
526
|
+
<!-- Sad mouth -->
|
|
527
|
+
${px(23,19,2,1,P3)}
|
|
528
|
+
<!-- Blanket -->
|
|
529
|
+
${px(10,19,28,2,'#87ceeb50')}
|
|
530
|
+
<!-- IV drip stand -->
|
|
531
|
+
${px(42,6,1,14,'#aaa')}${px(40,5,5,1,'#aaa')}
|
|
532
|
+
<rect x="41" y="6" width="3" height="2" fill="#87ceeb" class="px-pulse"/>
|
|
533
|
+
<!-- Heart monitor -->
|
|
534
|
+
<rect x="4" y="8" width="1" height="1" fill="#f85149" class="px-alarm"/>
|
|
535
|
+
<rect x="2" y="12" width="1" height="1" fill="#f85149" class="px-alarm" style="animation-delay:.3s"/>
|
|
536
|
+
</g></svg>`,
|
|
537
|
+
|
|
538
|
+
// 2: On fire
|
|
539
|
+
() => `<svg viewBox="-2 -8 52 52" xmlns="http://www.w3.org/2000/svg">${styles}
|
|
540
|
+
<g class="px-tremor">
|
|
541
|
+
<!-- Flames -->
|
|
542
|
+
<rect x="16" y="0" width="2" height="4" fill="#ff6600" class="px-pulse"/>
|
|
543
|
+
<rect x="22" y="-2" width="3" height="5" fill="#ff4400" class="px-pulse" style="animation-delay:.2s"/>
|
|
544
|
+
<rect x="28" y="0" width="2" height="4" fill="#ff6600" class="px-pulse" style="animation-delay:.4s"/>
|
|
545
|
+
<rect x="19" y="1" width="2" height="3" fill="#ffaa00" class="px-pulse" style="animation-delay:.6s"/>
|
|
546
|
+
<rect x="26" y="1" width="2" height="3" fill="#ffaa00" class="px-pulse" style="animation-delay:.8s"/>
|
|
547
|
+
${px(14,5,1,1,P2)}${px(13,6,1,1,P2)}${px(33,5,1,1,P2)}${px(34,6,1,1,P2)}
|
|
548
|
+
${px(18,6,12,5,P1)}${px(17,7,14,4,P1)}${px(16,8,16,2,P1)}
|
|
549
|
+
<!-- Panic eyes (wide open) -->
|
|
550
|
+
${px(19,7,4,3,'#fff')}${px(25,7,4,3,'#fff')}
|
|
551
|
+
${px(21,8,1,2,'#1a1a2e')}${px(27,8,1,2,'#1a1a2e')}
|
|
552
|
+
<!-- Scream mouth -->
|
|
553
|
+
${px(22,11,4,2,'#8a3040')}
|
|
554
|
+
<g class="px-breath" style="animation-duration:2s">${px(17,13,14,3,P1)}${px(16,14,16,2,P2)}${px(18,16,12,2,P1)}${px(19,18,10,2,P2)}</g>
|
|
555
|
+
<!-- Flailing claws -->
|
|
556
|
+
<g class="px-flex-l" style="animation-duration:0.5s">${px(8,10,4,2,P1)}${px(6,11,3,2,P2)}${px(4,10,3,2,P2)}</g>
|
|
557
|
+
<g class="px-flex-r" style="animation-duration:0.5s;animation-delay:.25s">${px(36,10,4,2,P1)}${px(38,11,3,2,P2)}${px(40,10,3,2,P2)}</g>
|
|
558
|
+
${px(15,19,1,2,P2)}${px(14,20,1,2,P3)}${px(17,19,1,2,P2)}${px(16,20,1,2,P3)}
|
|
559
|
+
${px(30,19,1,2,P2)}${px(31,20,1,2,P3)}${px(32,19,1,2,P2)}${px(33,20,1,2,P3)}
|
|
560
|
+
${px(20,20,8,2,P3)}${px(19,22,10,1,P3)}
|
|
561
|
+
</g></svg>`,
|
|
562
|
+
|
|
563
|
+
// 3: Melting / dissolving
|
|
564
|
+
() => `<svg viewBox="-2 -6 52 52" xmlns="http://www.w3.org/2000/svg">${styles}
|
|
565
|
+
<g>
|
|
566
|
+
${px(14,5,1,1,P2)}${px(13,6,1,1,P2)}${px(33,5,1,1,P2)}${px(34,6,1,1,P2)}
|
|
567
|
+
${px(18,6,12,5,P1)}${px(17,7,14,4,P1)}${px(16,8,16,2,P1)}
|
|
568
|
+
<!-- Dizzy spiral eyes -->
|
|
569
|
+
${px(20,8,3,2,'#fff')}${px(25,8,3,2,'#fff')}
|
|
570
|
+
<g class="px-spin" style="animation-duration:3s">${px(21,8,1,1,'#e63946')}${px(20,9,1,1,'#e63946')}</g>
|
|
571
|
+
<g class="px-spin" style="animation-duration:3s;animation-delay:1.5s">${px(26,8,1,1,'#e63946')}${px(27,9,1,1,'#e63946')}</g>
|
|
572
|
+
${px(22,12,1,1,P3)}${px(23,11,2,1,P3)}${px(25,12,1,1,P3)}
|
|
573
|
+
<g class="px-breath" style="animation-duration:5s">${px(17,13,14,3,P1)}${px(16,14,16,2,P2)}${px(18,16,12,2,P1)}${px(19,18,10,2,P2)}</g>
|
|
574
|
+
<!-- Melting drips -->
|
|
575
|
+
<rect x="16" y="20" width="2" height="3" fill="${P1}90" class="px-drip"/>
|
|
576
|
+
<rect x="22" y="21" width="2" height="4" fill="${P1}70" class="px-drip" style="animation-delay:.4s"/>
|
|
577
|
+
<rect x="28" y="20" width="2" height="3" fill="${P1}90" class="px-drip" style="animation-delay:.8s"/>
|
|
578
|
+
<rect x="19" y="21" width="1" height="3" fill="${P2}60" class="px-drip" style="animation-delay:1.2s"/>
|
|
579
|
+
<rect x="30" y="21" width="1" height="3" fill="${P2}60" class="px-drip" style="animation-delay:1.6s"/>
|
|
580
|
+
${px(8,13,4,2,P1)}${px(6,14,3,2,P2)}
|
|
581
|
+
${px(36,13,4,2,P1)}${px(38,14,3,2,P2)}
|
|
582
|
+
<!-- Puddle -->
|
|
583
|
+
${px(12,24,24,1,P3+'40')}${px(14,25,20,1,P3+'20')}
|
|
584
|
+
</g></svg>`,
|
|
585
|
+
|
|
586
|
+
// 4: SOS flag / shipwreck
|
|
587
|
+
() => `<svg viewBox="-2 -8 52 52" xmlns="http://www.w3.org/2000/svg">${styles}
|
|
588
|
+
<g class="px-tremor" style="animation-duration:.4s">
|
|
589
|
+
<!-- SOS flag pole -->
|
|
590
|
+
${px(38,0,1,14,'#8b6914')}
|
|
591
|
+
<!-- Flag waving -->
|
|
592
|
+
<g class="px-wag" style="animation-duration:1s">${px(39,0,8,5,'#f85149')}
|
|
593
|
+
${px(40,1,1,1,'#fff')}${px(42,1,1,1,'#fff')}${px(44,1,1,1,'#fff')}
|
|
594
|
+
${px(41,2,1,1,'#fff')}${px(43,2,1,1,'#fff')}${px(45,2,1,1,'#fff')}
|
|
595
|
+
${px(40,3,1,1,'#fff')}${px(42,3,1,1,'#fff')}${px(44,3,1,1,'#fff')}
|
|
596
|
+
</g>
|
|
597
|
+
<rect x="4" y="5" width="2" height="2" fill="#f85149" class="px-alarm"/>
|
|
598
|
+
<rect x="2" y="11" width="1" height="1" fill="#f85149" class="px-alarm" style="animation-delay:.3s"/>
|
|
599
|
+
${px(14,5,1,1,P2)}${px(13,6,1,1,P2)}${px(33,5,1,1,P2)}${px(34,6,1,1,P2)}
|
|
600
|
+
${px(18,6,12,5,P1)}${px(17,7,14,4,P1)}${px(16,8,16,2,P1)}
|
|
601
|
+
<!-- Crying eyes -->
|
|
602
|
+
${px(20,7,3,2,'#fff')}${px(25,7,3,2,'#fff')}
|
|
603
|
+
${px(21,8,1,1,'#1a1a2e')}${px(26,8,1,1,'#1a1a2e')}
|
|
604
|
+
<rect x="22" y="9" width="1" height="2" fill="#58a6ff" class="px-drip"/>
|
|
605
|
+
<rect x="27" y="9" width="1" height="2" fill="#58a6ff" class="px-drip" style="animation-delay:.5s"/>
|
|
606
|
+
${px(22,12,1,1,P3)}${px(23,11,2,1,P3)}${px(25,12,1,1,P3)}
|
|
607
|
+
<g class="px-breath" style="animation-duration:5s">${px(17,13,14,3,P1)}${px(16,14,16,2,P2)}${px(18,16,12,2,P1)}${px(19,18,10,2,P2)}</g>
|
|
608
|
+
${px(8,13,4,2,P1)}${px(6,14,3,2,P2)}${px(4,15,3,1,P2)}
|
|
609
|
+
${px(36,10,4,2,P1)}${px(38,11,3,2,P2)}
|
|
610
|
+
${px(15,19,1,2,P2)}${px(14,20,1,2,P3)}${px(17,19,1,2,P2)}${px(16,20,1,2,P3)}
|
|
611
|
+
${px(30,19,1,2,P2)}${px(31,20,1,2,P3)}${px(32,19,1,2,P2)}${px(33,20,1,2,P3)}
|
|
612
|
+
${px(20,20,8,2,P3)}${px(19,22,10,1,P3)}
|
|
613
|
+
</g></svg>`,
|
|
614
|
+
];
|
|
615
|
+
|
|
616
|
+
if (grade === 'S') return pick(sVariants)();
|
|
617
|
+
if (grade === 'A') return pick(aVariants)();
|
|
618
|
+
if (grade === 'B') return pick(bVariants)();
|
|
619
|
+
return pick(fVariants)();
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// ---------------------------------------------------------------------------
|
|
623
|
+
// Generate
|
|
624
|
+
// ---------------------------------------------------------------------------
|
|
625
|
+
|
|
626
|
+
function generateReport(data) {
|
|
627
|
+
const { composite_score = 0, dimensions = {}, recommendations = [], skills_scanned = 0, protection_level = 'unknown', timestamp } = data;
|
|
628
|
+
const tier = getTier(composite_score);
|
|
629
|
+
const ctaUrl = `https://agentguard.gopluslabs.io?utm_source=checkup&utm_medium=cli&utm_campaign=health_report&score=${composite_score}`;
|
|
630
|
+
const ts = timestamp || new Date().toISOString();
|
|
631
|
+
const totalFindings = Object.values(dimensions).reduce((s, d) => s + (d.findings || []).length, 0);
|
|
632
|
+
const lobsterSvg = pixelLobster(tier.grade, tier.color);
|
|
633
|
+
|
|
634
|
+
// ── Page 1: Dimension rows (skip N/A dimensions) ──
|
|
635
|
+
const dimRowsHtml = Object.entries(DIM_META).filter(([key]) => {
|
|
636
|
+
const dim = dimensions[key] || { score: null, na: false };
|
|
637
|
+
return !dim.na && dim.score !== null;
|
|
638
|
+
}).map(([key, meta]) => {
|
|
639
|
+
const dim = dimensions[key] || { score: null, na: false };
|
|
640
|
+
const score = dim.score ?? 0;
|
|
641
|
+
const color = dimColor(score);
|
|
642
|
+
return `
|
|
643
|
+
<div class="bg-[#262a31]/30 p-4 rounded-lg border border-[#3a4a3f]/10 hover:bg-[#262a31]/50 transition-all">
|
|
644
|
+
<div class="flex items-center justify-between mb-2">
|
|
645
|
+
<div class="flex items-center gap-3">
|
|
646
|
+
<span class="material-symbols-outlined" style="color:${color}">${meta.icon}</span>
|
|
647
|
+
<span class="font-headline font-medium text-[#dfe2eb]" data-i18n="dim_${key}">${meta.name}</span>
|
|
648
|
+
</div>
|
|
649
|
+
<span class="font-headline font-bold" style="color:${color}">${score}</span>
|
|
650
|
+
</div>
|
|
651
|
+
<div class="w-full h-1 bg-[#0a0e14] rounded-full overflow-hidden">
|
|
652
|
+
<div class="h-full rounded-full transition-all duration-1000" style="background:${color};width:${score}%;box-shadow:0 0 8px ${color}"></div>
|
|
653
|
+
</div>
|
|
654
|
+
</div>`;
|
|
655
|
+
}).join('\n');
|
|
656
|
+
|
|
657
|
+
// ── Findings data ──
|
|
658
|
+
const allFindings = [];
|
|
659
|
+
const cleanDims = [];
|
|
660
|
+
for (const [key, meta] of Object.entries(DIM_META)) {
|
|
661
|
+
const dim = dimensions[key] || { score: null, na: false };
|
|
662
|
+
if (dim.na || dim.score === null) continue; // skip N/A dimensions
|
|
663
|
+
const fs = dim.findings || [];
|
|
664
|
+
if (fs.length === 0) cleanDims.push(meta);
|
|
665
|
+
for (const f of fs) allFindings.push({ ...f, icon: meta.icon, dim: meta.name, dimZh: meta.zh });
|
|
666
|
+
}
|
|
667
|
+
allFindings.sort((a, b) => {
|
|
668
|
+
const o = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
|
|
669
|
+
return (o[(a.severity||'MEDIUM').toUpperCase()]||3) - (o[(b.severity||'MEDIUM').toUpperCase()]||3);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
// Split findings into pages of 3
|
|
673
|
+
const FPP = 3;
|
|
674
|
+
const findingsPages = [];
|
|
675
|
+
if (allFindings.length > 0) {
|
|
676
|
+
for (let i = 0; i < allFindings.length; i += FPP) {
|
|
677
|
+
const chunk = allFindings.slice(i, i + FPP);
|
|
678
|
+
const isFirst = i === 0;
|
|
679
|
+
const isLast = i + FPP >= allFindings.length;
|
|
680
|
+
let h = '';
|
|
681
|
+
if (isFirst) {
|
|
682
|
+
h += `<div class="mb-6"><p class="text-xs font-label uppercase tracking-[0.2em] text-[#849588] mb-1" data-i18n="vuln_stream">Active Vulnerability Stream</p><h1 class="text-3xl font-headline font-bold text-[#f5fff5] tracking-tight flex items-center gap-3"><span class="material-symbols-outlined text-[#ffb4ab]">bug_report</span><span data-i18n="findings">Findings</span> <span class="text-[#849588] font-normal text-lg">(${totalFindings})</span></h1></div>`;
|
|
683
|
+
} else {
|
|
684
|
+
h += `<div class="mb-6 flex items-center gap-3"><span class="material-symbols-outlined text-[#ffb4ab]">bug_report</span><span class="text-lg font-headline font-bold text-[#849588]" data-i18n="findings_range" data-range="${i+1}–${Math.min(i+FPP,allFindings.length)}" data-total="${totalFindings}">Findings — ${i+1}–${Math.min(i+FPP,allFindings.length)} of ${totalFindings}</span></div>`;
|
|
685
|
+
}
|
|
686
|
+
h += chunk.map(f => {
|
|
687
|
+
const sev = (f.severity||'MEDIUM').toUpperCase();
|
|
688
|
+
const sc = sevColor(sev);
|
|
689
|
+
const ftxt = f.text||f.description||'';
|
|
690
|
+
const fzh = f.zh||ftxt;
|
|
691
|
+
return `
|
|
692
|
+
<div class="bg-[#1c2026] border border-[#3a4a3f]/15 rounded-xl p-5 mb-3" style="border-left:3px solid ${sc}">
|
|
693
|
+
<div class="flex items-center gap-3 mb-2">
|
|
694
|
+
<span class="px-2.5 py-0.5 rounded text-[10px] font-bold uppercase tracking-wide text-white" style="background:${sc}">${sev}</span>
|
|
695
|
+
<span class="flex items-center gap-1.5 font-headline font-medium text-[#dfe2eb]"><span class="material-symbols-outlined text-base" style="color:${sc}">${f.icon}</span><span class="finding-dim" data-en="${esc(f.dim)}" data-zh="${esc(f.dimZh)}">${f.dim}</span></span>
|
|
696
|
+
</div>
|
|
697
|
+
<p class="text-sm text-[#b9cbbd] leading-relaxed finding-text" data-en="${esc(ftxt)}" data-zh="${esc(fzh)}">${esc(ftxt)}</p>
|
|
698
|
+
</div>`;
|
|
699
|
+
}).join('');
|
|
700
|
+
if (isLast && cleanDims.length > 0) {
|
|
701
|
+
h += `<div class="bg-[#1c2026] border border-[#3a4a3f]/15 rounded-xl p-8 mt-3 text-center"><span class="material-symbols-outlined text-4xl text-[#849588]/40 mb-2">verified_user</span><p class="font-headline font-bold text-[#dfe2eb] mb-1 clean-dims" data-en="${cleanDims.map(d=>d.name).join(', ')}" data-zh="${cleanDims.map(d=>d.zh).join('、')}">${cleanDims.map(d=>d.name).join(', ')}</p><p class="text-sm text-[#849588]" data-i18n="no_threats_clean">No active threats detected. Clinically sterile.</p></div>`;
|
|
702
|
+
}
|
|
703
|
+
findingsPages.push(h);
|
|
704
|
+
}
|
|
705
|
+
} else {
|
|
706
|
+
let h = `<div class="mb-6"><h1 class="text-3xl font-headline font-bold text-[#f5fff5] tracking-tight flex items-center gap-3"><span class="material-symbols-outlined text-[#00ffa3]">verified_user</span>Findings <span class="text-[#849588] font-normal text-lg">(0)</span></h1></div>`;
|
|
707
|
+
h += `<div class="bg-[#1c2026] border border-[#3a4a3f]/15 rounded-xl p-12 text-center"><span class="material-symbols-outlined text-5xl text-[#00ffa3]/40 mb-3">shield</span><p class="text-xl font-headline font-bold text-[#dfe2eb] mb-2" data-i18n="all_clear">All Clear</p><p class="text-sm text-[#849588]" data-i18n="no_threats_all">No active threats detected across all dimensions.</p></div>`;
|
|
708
|
+
findingsPages.push(h);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ── Recommendations ──
|
|
712
|
+
// Auto-generate extra recommendations based on dimension scores
|
|
713
|
+
const autoRecs = [];
|
|
714
|
+
const ds = dimensions;
|
|
715
|
+
if (ds.code_safety && !ds.code_safety.na && (ds.code_safety.score ?? 100) < 70)
|
|
716
|
+
autoRecs.push({ severity: 'HIGH', text: 'Run /agentguard scan on all installed skills and review flagged findings.', zh: '对所有已安装的 Skill 运行 /agentguard scan 并检查标记的问题。' });
|
|
717
|
+
if (ds.trust_hygiene && !ds.trust_hygiene.na && (ds.trust_hygiene.score ?? 100) < 70)
|
|
718
|
+
autoRecs.push({ severity: 'HIGH', text: 'Register unattested skills with /agentguard trust attest after security review.', zh: '安全审查后,使用 /agentguard trust attest 注册未认证的 Skill。' });
|
|
719
|
+
if (ds.runtime_defense && !ds.runtime_defense.na && (ds.runtime_defense.score ?? 100) < 50)
|
|
720
|
+
autoRecs.push({ severity: 'MEDIUM', text: 'Enable guard hooks to build a security audit trail and block threats in real-time.', zh: '启用安全钩子以建立安全审计日志并实时拦截威胁。' });
|
|
721
|
+
if (ds.secret_protection && !ds.secret_protection.na && (ds.secret_protection.score ?? 100) < 70)
|
|
722
|
+
autoRecs.push({ severity: 'CRITICAL', text: 'Rotate exposed credentials and fix file permissions on ~/.ssh/ and ~/.gnupg/ directories.', zh: '轮换已暴露的凭证,并修复 ~/.ssh/ 和 ~/.gnupg/ 目录的文件权限。' });
|
|
723
|
+
if (ds.web3_shield && !ds.web3_shield.na && (ds.web3_shield.score ?? 100) < 50)
|
|
724
|
+
autoRecs.push({ severity: 'HIGH', text: 'Configure GOPLUS_API_KEY for enhanced Web3 transaction simulation and phishing detection.', zh: '配置 GOPLUS_API_KEY 以增强 Web3 交易模拟和钓鱼检测。' });
|
|
725
|
+
if (ds.config_posture && !ds.config_posture.na && (ds.config_posture.score ?? 100) < 50)
|
|
726
|
+
autoRecs.push({ severity: 'MEDIUM', text: 'Switch protection level to balanced or strict: /agentguard config balanced', zh: '将防护等级切换为均衡或严格模式:/agentguard config balanced' });
|
|
727
|
+
if (ds.config_posture && !ds.config_posture.na && (ds.config_posture.score ?? 100) < 70)
|
|
728
|
+
autoRecs.push({ severity: 'LOW', text: 'Set up daily security patrols for continuous posture monitoring: /agentguard patrol setup', zh: '设置每日安全巡检以持续监控安全态势:/agentguard patrol setup' });
|
|
729
|
+
if (composite_score < 90)
|
|
730
|
+
autoRecs.push({ severity: 'LOW', text: 'Enable auto-scan on session start: export AGENTGUARD_AUTO_SCAN=1', zh: '启用会话启动时自动扫描:export AGENTGUARD_AUTO_SCAN=1' });
|
|
731
|
+
|
|
732
|
+
// Merge: user recs first, then auto recs (dedup by text similarity)
|
|
733
|
+
const allRecs = [...recommendations];
|
|
734
|
+
for (const ar of autoRecs) {
|
|
735
|
+
if (!allRecs.some(r => r.text.toLowerCase().includes(ar.text.slice(0, 30).toLowerCase()))) {
|
|
736
|
+
allRecs.push(ar);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
// Always add upgrade CTA as last
|
|
740
|
+
if (!allRecs.some(r => r.text.toLowerCase().includes('upgrade') || r.text.includes('升级'))) {
|
|
741
|
+
allRecs.push({ severity: 'LOW', text: 'Upgrade to enhanced Skill scanning with GoPlus AgentGuard for 24/7 real-time monitoring and automated alerts.', zh: '升级到更强的 Skill 扫描能力 — GoPlus AgentGuard 提供 7×24 实时监控与自动告警。' });
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const recsHtml = allRecs.length > 0
|
|
745
|
+
? `<div class="space-y-1">${allRecs.map((r, i) => {
|
|
746
|
+
const sev = (r.severity||'MEDIUM').toUpperCase();
|
|
747
|
+
const sc = sevColor(sev);
|
|
748
|
+
const zhText = r.zh || r.text;
|
|
749
|
+
return `
|
|
750
|
+
<div class="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-[#262a31]/30 transition-colors group">
|
|
751
|
+
<span class="w-5 text-xs font-headline font-bold text-[#849588]/60">${i+1}</span>
|
|
752
|
+
<span class="px-2 py-0.5 rounded text-[9px] font-bold uppercase tracking-wide text-white shrink-0" style="background:${sc}">${sev}</span>
|
|
753
|
+
<span class="text-sm text-[#b9cbbd] leading-snug rec-text" data-en="${esc(r.text)}" data-zh="${esc(zhText)}">${esc(r.text)}</span>
|
|
754
|
+
<span class="material-symbols-outlined text-sm text-[#849588]/0 group-hover:text-[#849588]/50 transition-colors ml-auto shrink-0">chevron_right</span>
|
|
755
|
+
</div>`;
|
|
756
|
+
}).join('')}</div>`
|
|
757
|
+
: '<div class="text-center py-12 text-[#849588]" data-i18n="no_recs">No recommendations.</div>';
|
|
758
|
+
|
|
759
|
+
// ── AI Analysis report ──
|
|
760
|
+
const analysisText = data.analysis || '';
|
|
761
|
+
const analysisHtml = analysisText
|
|
762
|
+
? `<div class="relative group">
|
|
763
|
+
<div class="bg-[#0a0e14] border border-[#3a4a3f]/10 rounded-xl p-5 text-sm text-[#b9cbbd] leading-relaxed whitespace-pre-line" id="analysisText">${esc(analysisText)}</div>
|
|
764
|
+
<button onclick="copyReport()" id="copyBtn" class="absolute top-3 right-3 flex items-center gap-1.5 px-3 py-1.5 bg-[#262a31] border border-[#3a4a3f]/30 rounded-lg text-[11px] font-semibold text-[#849588] hover:text-[#dfe2eb] hover:border-[#849588]/50 transition-all opacity-0 group-hover:opacity-100">
|
|
765
|
+
<span class="material-symbols-outlined text-sm" id="copyIcon">content_copy</span><span id="copyLabel" data-i18n="copy_report">Copy Report</span>
|
|
766
|
+
</button>
|
|
767
|
+
</div>`
|
|
768
|
+
: '';
|
|
769
|
+
|
|
770
|
+
// ── Health status label ──
|
|
771
|
+
const healthLabel = composite_score >= 70 ? 'OPTIMAL' : composite_score >= 50 ? 'STABILIZING' : 'CRITICAL_ALERT';
|
|
772
|
+
|
|
773
|
+
// ── Total pages ──
|
|
774
|
+
const totalPages = 1 + findingsPages.length + 1;
|
|
775
|
+
|
|
776
|
+
const html = `<!DOCTYPE html>
|
|
777
|
+
<html class="dark" lang="en"><head>
|
|
778
|
+
<meta charset="utf-8"/>
|
|
779
|
+
<meta content="width=device-width,initial-scale=1.0" name="viewport"/>
|
|
780
|
+
<title>AgentGuard Diagnostic Report — ${composite_score}/100</title>
|
|
781
|
+
${faviconB64 ? `<link rel="icon" type="image/png" href="data:image/png;base64,${faviconB64}"/>` : ''}
|
|
782
|
+
<meta property="og:title" content="AgentGuard Security Report — Score: ${composite_score}/100"/>
|
|
783
|
+
<meta property="og:description" content="Tier ${tier.grade} — ${tier.label}. ${totalFindings} findings across ${skills_scanned} skills."/>
|
|
784
|
+
<meta name="twitter:card" content="summary"/>
|
|
785
|
+
<meta name="twitter:title" content="AgentGuard Security Report — ${composite_score}/100"/>
|
|
786
|
+
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"><\/script>
|
|
787
|
+
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"/>
|
|
788
|
+
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
|
789
|
+
<script>
|
|
790
|
+
tailwind.config={darkMode:"class",theme:{extend:{colors:{"primary-container":"${tier.color}","surface-container":"#1c2026"},fontFamily:{"headline":["Space Grotesk"],"body":["Inter"],"label":["Inter"]}}}};
|
|
791
|
+
<\/script>
|
|
792
|
+
<style>
|
|
793
|
+
body{background:#0a0e14;color:#dfe2eb;font-family:'Inter',sans-serif}
|
|
794
|
+
.obsidian-layer{background:linear-gradient(145deg,#1c2026 0%,#12171e 100%)}
|
|
795
|
+
.material-symbols-outlined{font-variation-settings:'FILL' 0,'wght' 400,'GRAD' 0,'opsz' 24}
|
|
796
|
+
@media(max-width:768px){
|
|
797
|
+
.page1-layout{flex-direction:column!important;overflow-y:auto!important}
|
|
798
|
+
.page1-left{width:100%!important;flex-shrink:0}
|
|
799
|
+
.page1-right{width:100%!important}
|
|
800
|
+
.page1-left .obsidian-layer{flex-direction:row!important;gap:16px;padding:16px!important;align-items:center!important}
|
|
801
|
+
.page1-left .obsidian-layer>div:first-child{width:120px!important;flex-shrink:0}
|
|
802
|
+
.page1-left .obsidian-layer>div:last-child{text-align:left!important}
|
|
803
|
+
.page1-left .obsidian-layer>div:last-child .flex.flex-col{align-items:flex-start!important}
|
|
804
|
+
.page1-left .obsidian-layer>div:last-child .inline-flex{margin-top:4px}
|
|
805
|
+
.page1-left .obsidian-layer>div:last-child p[data-i18n="quote"]{display:none}
|
|
806
|
+
.score-num{font-size:2.5rem!important}
|
|
807
|
+
.header-meta{display:none!important}
|
|
808
|
+
.header-btns{gap:6px!important}
|
|
809
|
+
.header-btns button,.header-btns a{padding:6px 8px!important}
|
|
810
|
+
.header-btns button span:not(.material-symbols-outlined){display:none!important}
|
|
811
|
+
.nav-steps span:not(.material-symbols-outlined){display:none!important}
|
|
812
|
+
.nav-steps .step{padding:8px!important}
|
|
813
|
+
.cta-card{flex-direction:column!important;text-align:center;gap:12px!important;padding:16px!important}
|
|
814
|
+
.cta-card .text-4xl{font-size:2rem!important}
|
|
815
|
+
.stats-row{grid-template-columns:repeat(3,1fr)!important;gap:8px!important}
|
|
816
|
+
.stats-row>div{padding:8px!important}
|
|
817
|
+
}
|
|
818
|
+
</style>
|
|
819
|
+
</head>
|
|
820
|
+
<body class="h-screen overflow-hidden flex flex-col">
|
|
821
|
+
|
|
822
|
+
<!-- Header -->
|
|
823
|
+
<header class="shrink-0 flex justify-between items-center px-4 sm:px-6 py-3 bg-[#10141a] shadow-[0px_8px_24px_rgba(0,0,0,0.3)]">
|
|
824
|
+
<div class="text-base sm:text-lg font-bold text-[#f5fff5] flex items-center gap-2 font-['Space_Grotesk'] tracking-tight">
|
|
825
|
+
<svg viewBox="0 0 540 540" width="24" height="24" class="shrink-0"><rect fill="#151515" width="540" height="540" rx="73"/><g transform="translate(127.125,136.125)" fill="#fff" fill-rule="nonzero"><path d="M188.93 65.32V65.34H116.13C70.82 65.34 34.09 102.86 34.09 149.14c0 46.28 36.73 83.8 82.04 83.8h9.24 8.67 35.53c9.1 0 16.47-7.53 16.47-16.82s-7.37-16.82-16.47-16.82h-34.95-.58-11.56c-29.98 0-54.31-22.32-54.31-50.01 0-27.69 24.33-50.37 54.31-50.37l36.87.12c0-.02 0-.04 0-.07h45.74c0 19.56-16.92 35.42-37.77 35.42-.7 0-1.36-.02-1.98-.05v.05H117c-8.14 0-14.73 6.74-14.73 15.05 0 8.31 6.6 15.05 14.73 15.05h14.16 2.89.58 34.95c27.92 0 50.56 23.12 50.56 51.63 0 28.52-22.63 51.64-50.56 51.64h-35.53-17.91C52 267.75 0 214.64 0 149.14 0 83.63 52 30.52 116.13 30.52h38.13 34.67 33.51c0 19.03-14.95 34.49-33.51 34.8M314.97 48.32h-20.14V70.13c0 1.87-1.53 3.39-3.41 3.39h-18.18c-1.88 0-3.41-1.52-3.41-3.39V48.32h-19.64c-1.88 0-3.41-1.52-3.41-3.39V28.53c0-1.87 1.53-3.39 3.41-3.39h19.64V3.39C269.83 1.52 271.35 0 273.23 0h18.18c1.88 0 3.41 1.52 3.41 3.39v21.74h20.14c1.88 0 3.41 1.52 3.41 3.39v16.4c0 1.87-1.53 3.39-3.41 3.39"/></g></svg>
|
|
826
|
+
<span data-i18n="title">AgentGuard Report</span>
|
|
827
|
+
</div>
|
|
828
|
+
<div class="flex items-center gap-2 sm:gap-3 header-btns">
|
|
829
|
+
<span class="text-[#849588] text-xs font-['Space_Grotesk'] tracking-[0.15em] uppercase header-meta" data-i18n="prot_mode">${protection_level} mode</span>
|
|
830
|
+
<span class="text-[#849588] text-xs header-meta">${ts.slice(0,10)}</span>
|
|
831
|
+
<a href="https://github.com/GoPlusSecurity/agentguard" target="_blank" class="hidden sm:flex items-center gap-1 px-2.5 py-1.5 bg-[#262a31] border border-[#3a4a3f]/30 rounded-lg text-[11px] font-semibold text-[#849588] hover:text-[#dfe2eb] hover:border-[#849588]/50 transition-all no-underline">
|
|
832
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/></svg>
|
|
833
|
+
GitHub
|
|
834
|
+
</a>
|
|
835
|
+
<a href="https://clawhub.ai/0xbeekeeper/security" target="_blank" class="hidden sm:flex items-center gap-1 px-2.5 py-1.5 bg-[#262a31] border border-[#3a4a3f]/30 rounded-lg text-[11px] font-semibold text-[#849588] hover:text-[#dfe2eb] hover:border-[#849588]/50 transition-all no-underline">
|
|
836
|
+
<span style="font-size:13px;line-height:1">🦞</span>
|
|
837
|
+
ClawHub
|
|
838
|
+
</a>
|
|
839
|
+
<button onclick="toggleLang()" id="langBtn" class="flex items-center gap-1 px-2.5 py-1.5 bg-[#262a31] border border-[#3a4a3f]/30 rounded-lg text-[11px] font-semibold text-[#849588] hover:text-[#dfe2eb] hover:border-[#849588]/50 transition-all">
|
|
840
|
+
<span class="material-symbols-outlined text-sm">translate</span><span id="langLabel">中文</span>
|
|
841
|
+
</button>
|
|
842
|
+
<button onclick="shareReport()" class="flex items-center gap-1.5 px-2.5 py-1.5 bg-[#262a31] border border-[#3a4a3f]/30 rounded-lg text-[11px] font-semibold text-[#849588] hover:text-[#dfe2eb] hover:border-[#849588]/50 transition-all">
|
|
843
|
+
<span class="material-symbols-outlined text-sm">share</span><span data-i18n="share">Share</span>
|
|
844
|
+
</button>
|
|
845
|
+
</div>
|
|
846
|
+
</header>
|
|
847
|
+
|
|
848
|
+
<!-- Carousel -->
|
|
849
|
+
<main class="flex-1 overflow-hidden relative">
|
|
850
|
+
<div class="flex h-full transition-transform duration-500 ease-[cubic-bezier(.25,.8,.25,1)]" id="track">
|
|
851
|
+
|
|
852
|
+
<!-- PAGE 1: Diagnostic Overview -->
|
|
853
|
+
<div class="w-full h-full shrink-0 flex gap-4 sm:gap-6 px-4 sm:px-6 py-4 sm:py-5 page1-layout">
|
|
854
|
+
<!-- Left: Mascot -->
|
|
855
|
+
<section class="w-[35%] shrink-0 page1-left">
|
|
856
|
+
<div class="obsidian-layer h-full rounded-xl p-4 sm:p-6 flex flex-col items-center justify-center gap-4">
|
|
857
|
+
<div class="w-[260px]" style="filter:drop-shadow(0 12px 30px ${tier.color}40);image-rendering:pixelated;overflow:visible">${lobsterSvg}</div>
|
|
858
|
+
<div class="w-full text-center space-y-2 sm:space-y-3">
|
|
859
|
+
<div class="flex flex-col items-center">
|
|
860
|
+
<span class="text-4xl sm:text-6xl font-headline font-bold tracking-tighter score-num" style="color:${tier.color}" id="scoreNum">0<span class="text-base sm:text-xl text-[#849588] opacity-40 ml-1">/ 100</span></span>
|
|
861
|
+
<div class="w-32 sm:w-40 h-1 bg-[#262a31] rounded-full mt-2 sm:mt-3 overflow-hidden">
|
|
862
|
+
<div class="h-full rounded-full transition-all duration-1000" id="scoreBar" style="background:${tier.color};width:0%;box-shadow:0 0 12px ${tier.color}"></div>
|
|
863
|
+
</div>
|
|
864
|
+
</div>
|
|
865
|
+
<div class="inline-flex items-center px-3 sm:px-4 py-1 rounded-full border" style="background:${tier.color}10;border-color:${tier.color}30">
|
|
866
|
+
<span class="text-[10px] sm:text-[11px] font-headline font-bold tracking-[0.15em]" style="color:${tier.color}" data-i18n="tier_badge">TIER ${tier.grade} — ${tier.label}</span>
|
|
867
|
+
</div>
|
|
868
|
+
<p class="text-[#b9cbbd] text-xs sm:text-sm italic leading-relaxed px-2" data-i18n="quote">"${tier.quote}"</p>
|
|
869
|
+
</div>
|
|
870
|
+
</div>
|
|
871
|
+
</section>
|
|
872
|
+
|
|
873
|
+
<!-- Right: Dimensions -->
|
|
874
|
+
<section class="w-[65%] flex flex-col page1-right">
|
|
875
|
+
<div class="obsidian-layer h-full rounded-xl p-4 sm:p-6 flex flex-col">
|
|
876
|
+
<div class="flex justify-between items-end mb-3 sm:mb-5">
|
|
877
|
+
<div>
|
|
878
|
+
<p class="text-[10px] font-label uppercase tracking-[0.2em] text-[#849588] mb-0.5" data-i18n="diag_metrics">Diagnostic Metrics</p>
|
|
879
|
+
<h1 class="text-xl sm:text-2xl font-headline font-bold text-[#f5fff5] tracking-tight" data-i18n="sec_dims">SECURITY DIMENSIONS</h1>
|
|
880
|
+
</div>
|
|
881
|
+
<span class="text-[10px] font-label font-mono hidden sm:inline" style="color:${tier.color}" data-i18n="status_label">STATUS: ${healthLabel}</span>
|
|
882
|
+
</div>
|
|
883
|
+
<div class="grid grid-cols-1 gap-2 sm:gap-2.5 flex-1 content-start">
|
|
884
|
+
${dimRowsHtml}
|
|
885
|
+
</div>
|
|
886
|
+
<div class="mt-auto pt-3 sm:pt-5 grid grid-cols-3 gap-2 sm:gap-3 stats-row">
|
|
887
|
+
<div class="bg-[#10141a] p-2 sm:p-3 rounded-lg flex flex-col items-center border border-[#3a4a3f]/5">
|
|
888
|
+
<span class="text-lg sm:text-xl font-headline font-bold text-[#f5fff5]">${skills_scanned}</span>
|
|
889
|
+
<span class="text-[8px] sm:text-[9px] uppercase tracking-widest text-[#849588]" data-i18n="skills">Skills</span>
|
|
890
|
+
</div>
|
|
891
|
+
<div class="bg-[#10141a] p-2 sm:p-3 rounded-lg flex flex-col items-center border border-[#3a4a3f]/5">
|
|
892
|
+
<span class="text-lg sm:text-xl font-headline font-bold text-[#ffb4ab]">${totalFindings}</span>
|
|
893
|
+
<span class="text-[8px] sm:text-[9px] uppercase tracking-widest text-[#849588]" data-i18n="findings_label">Findings</span>
|
|
894
|
+
</div>
|
|
895
|
+
<div class="p-2 sm:p-3 rounded-lg flex flex-col items-center border" style="background:${tier.color}08;border-color:${tier.color}15">
|
|
896
|
+
<span class="text-lg sm:text-xl font-headline font-black" style="color:${tier.color}">${tier.grade}</span>
|
|
897
|
+
<span class="text-[8px] sm:text-[9px] uppercase tracking-widest" style="color:${tier.color}" data-i18n="tier_label">Tier</span>
|
|
898
|
+
</div>
|
|
899
|
+
</div>
|
|
900
|
+
</div>
|
|
901
|
+
</section>
|
|
902
|
+
</div>
|
|
903
|
+
|
|
904
|
+
<!-- PAGES 2+: Findings (paginated) -->
|
|
905
|
+
${findingsPages.map(content => `
|
|
906
|
+
<div class="w-full h-full shrink-0 px-4 sm:px-6 py-4 sm:py-5 overflow-hidden">
|
|
907
|
+
<div class="h-full rounded-xl p-4 sm:p-6 flex flex-col overflow-y-auto border border-[#3a4a3f]/10 relative" style="background:linear-gradient(145deg,#1c2026 0%,#1a1518 100%)">
|
|
908
|
+
<div class="absolute top-0 right-0 w-48 h-48 rounded-full blur-[100px] opacity-[0.03] pointer-events-none" style="background:#ffb4ab"></div>
|
|
909
|
+
${content}
|
|
910
|
+
</div>
|
|
911
|
+
</div>`).join('')}
|
|
912
|
+
|
|
913
|
+
<!-- LAST PAGE: Security Analysis & Remediation -->
|
|
914
|
+
<div class="w-full h-full shrink-0 px-4 sm:px-6 py-4 sm:py-5 overflow-hidden">
|
|
915
|
+
<div class="h-full rounded-xl p-4 sm:p-6 flex flex-col overflow-y-auto border border-[#3a4a3f]/10 relative" style="background:linear-gradient(145deg,#1c2026 0%,#151d20 100%)">
|
|
916
|
+
<div class="absolute top-0 left-0 w-48 h-48 rounded-full blur-[100px] opacity-[0.04] pointer-events-none" style="background:${tier.color}"></div>
|
|
917
|
+
<div class="flex flex-col sm:flex-row justify-between items-start mb-4 sm:mb-5 gap-3">
|
|
918
|
+
<div>
|
|
919
|
+
<p class="text-[10px] font-label uppercase tracking-[0.2em] text-[#849588] mb-1" data-i18n="sec_analysis">Security Analysis</p>
|
|
920
|
+
<h1 class="text-2xl font-headline font-bold text-[#f5fff5] tracking-tight flex items-center gap-3">
|
|
921
|
+
<span class="material-symbols-outlined" style="color:${tier.color}">analytics</span><span data-i18n="diag_report">Diagnostic Report</span>
|
|
922
|
+
</h1>
|
|
923
|
+
</div>
|
|
924
|
+
<div class="bg-[#262a31] border border-[#3a4a3f]/15 rounded-lg px-4 py-2 flex items-center gap-2">
|
|
925
|
+
<span class="material-symbols-outlined text-lg" style="color:${tier.color}">monitor_heart</span>
|
|
926
|
+
<div><p class="text-[8px] text-[#849588] uppercase tracking-wider" data-i18n="system_health">System Health</p><p class="text-xs font-headline font-bold" style="color:${tier.color}">${healthLabel}</p></div>
|
|
927
|
+
</div>
|
|
928
|
+
</div>
|
|
929
|
+
${analysisHtml}
|
|
930
|
+
<div class="mt-5 mb-3"><p class="text-[10px] font-label uppercase tracking-[0.2em] text-[#849588]" data-i18n="action_items">Action Items</p></div>
|
|
931
|
+
${recsHtml}
|
|
932
|
+
<div class="mt-auto pt-4">
|
|
933
|
+
<div class="relative rounded-xl p-4 sm:p-5 flex items-center gap-4 sm:gap-5 border-2 overflow-hidden cta-card" style="background:linear-gradient(135deg,#1c2026,#12171e);border-color:${tier.color}30">
|
|
934
|
+
<div class="absolute inset-0 opacity-5 pointer-events-none" style="background:linear-gradient(135deg,${tier.color},transparent)"></div>
|
|
935
|
+
<span class="text-4xl relative z-10">🦞</span>
|
|
936
|
+
<div class="flex-1 relative z-10">
|
|
937
|
+
<p class="font-headline font-bold text-lg text-[#f5fff5] mb-1" data-i18n="cta_title">24/7 Agent Protection</p>
|
|
938
|
+
<p class="text-sm text-[#849588]" data-i18n="cta_desc">Automated skill scanning, threat intelligence feeds & team security dashboard.</p>
|
|
939
|
+
</div>
|
|
940
|
+
<a href="${ctaUrl}" target="_blank" class="relative z-10 px-6 py-2.5 rounded-lg font-bold text-sm text-[#0a0e14] uppercase tracking-wider no-underline hover:opacity-90 transition-opacity" style="background:${tier.color}" data-i18n="cta_btn">Upgrade Skill Scanning</a>
|
|
941
|
+
</div>
|
|
942
|
+
</div>
|
|
943
|
+
</div>
|
|
944
|
+
</div>
|
|
945
|
+
|
|
946
|
+
</div>
|
|
947
|
+
</main>
|
|
948
|
+
|
|
949
|
+
<!-- Bottom Nav -->
|
|
950
|
+
<nav class="shrink-0 flex items-center px-3 sm:px-6 py-2 sm:py-3 bg-[#1c2026]/90 backdrop-blur-md border-t border-[#3a4a3f]/15">
|
|
951
|
+
<button id="prev" class="flex flex-col items-center text-[#849588] opacity-40 hover:opacity-100 hover:text-[#00ffa3] transition-all w-10 sm:w-16">
|
|
952
|
+
<span class="material-symbols-outlined">arrow_back</span>
|
|
953
|
+
<span class="text-[9px] uppercase tracking-widest mt-0.5 hidden sm:block" data-i18n="back">Back</span>
|
|
954
|
+
</button>
|
|
955
|
+
|
|
956
|
+
<div class="flex-1 flex flex-col items-center gap-1">
|
|
957
|
+
<div class="flex items-center gap-1.5" id="dots"></div>
|
|
958
|
+
<div class="flex items-center gap-1 mt-1 nav-steps" id="steps">
|
|
959
|
+
<button class="step active flex items-center gap-1.5 px-2 sm:px-3 py-1.5 rounded-lg text-[11px] font-semibold transition-all" data-p="0">
|
|
960
|
+
<span class="material-symbols-outlined text-sm">find_in_page</span><span data-i18n="nav_overview">Overview</span>
|
|
961
|
+
</button>
|
|
962
|
+
<span class="text-[#3a4a3f] text-xs">›</span>
|
|
963
|
+
<button class="step flex items-center gap-1.5 px-2 sm:px-3 py-1.5 rounded-lg text-[11px] font-semibold transition-all" data-p="1">
|
|
964
|
+
<span class="material-symbols-outlined text-sm">bug_report</span><span data-i18n="nav_analysis">Analysis</span>
|
|
965
|
+
</button>
|
|
966
|
+
<span class="text-[#3a4a3f] text-xs">›</span>
|
|
967
|
+
<button class="step flex items-center gap-1.5 px-2 sm:px-3 py-1.5 rounded-lg text-[11px] font-semibold transition-all" data-p="${totalPages - 1}">
|
|
968
|
+
<span class="material-symbols-outlined text-sm">analytics</span><span data-i18n="nav_report">Report</span>
|
|
969
|
+
</button>
|
|
970
|
+
</div>
|
|
971
|
+
</div>
|
|
972
|
+
|
|
973
|
+
<button id="next" class="flex flex-col items-center text-[#849588] opacity-40 hover:opacity-100 hover:text-[#00ffa3] transition-all w-10 sm:w-16">
|
|
974
|
+
<span class="material-symbols-outlined">arrow_forward</span>
|
|
975
|
+
<span class="text-[9px] uppercase tracking-widest mt-0.5 hidden sm:block" data-i18n="next">Next</span>
|
|
976
|
+
</button>
|
|
977
|
+
</nav>
|
|
978
|
+
|
|
979
|
+
<script>
|
|
980
|
+
(function(){
|
|
981
|
+
const track=document.getElementById('track');
|
|
982
|
+
const steps=[...document.querySelectorAll('.step')];
|
|
983
|
+
const dotsEl=document.getElementById('dots');
|
|
984
|
+
const prevBtn=document.getElementById('prev');
|
|
985
|
+
const nextBtn=document.getElementById('next');
|
|
986
|
+
const total=${totalPages};
|
|
987
|
+
const tierColor='${tier.color}';
|
|
988
|
+
let idx=0;
|
|
989
|
+
|
|
990
|
+
// Create dots
|
|
991
|
+
for(let i=0;i<total;i++){
|
|
992
|
+
const d=document.createElement('div');
|
|
993
|
+
d.className='rounded-full transition-all duration-300';
|
|
994
|
+
d.style.height='6px';
|
|
995
|
+
d.style.width=i===0?'20px':'6px';
|
|
996
|
+
d.style.background=i===0?tierColor:'#3a4a3f';
|
|
997
|
+
dotsEl.appendChild(d);
|
|
998
|
+
}
|
|
999
|
+
const dots=[...dotsEl.children];
|
|
1000
|
+
|
|
1001
|
+
function go(i){
|
|
1002
|
+
idx=Math.max(0,Math.min(total-1,i));
|
|
1003
|
+
track.style.transform='translateX(-'+(idx*100)+'%)';
|
|
1004
|
+
dots.forEach((d,j)=>{d.style.width=j===idx?'20px':'6px';d.style.background=j===idx?tierColor:'#3a4a3f'});
|
|
1005
|
+
prevBtn.style.opacity=idx===0?'0.25':'1';
|
|
1006
|
+
nextBtn.style.opacity=idx===total-1?'0.25':'1';
|
|
1007
|
+
// Step highlights
|
|
1008
|
+
steps.forEach(s=>s.classList.remove('active'));
|
|
1009
|
+
if(idx===0)steps[0].classList.add('active');
|
|
1010
|
+
else if(idx===total-1)steps[2].classList.add('active');
|
|
1011
|
+
else steps[1].classList.add('active');
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
prevBtn.onclick=()=>go(idx-1);
|
|
1015
|
+
nextBtn.onclick=()=>go(idx+1);
|
|
1016
|
+
steps.forEach(s=>s.addEventListener('click',()=>go(+s.dataset.p)));
|
|
1017
|
+
// Also make dots clickable
|
|
1018
|
+
setTimeout(()=>{dots.forEach((d,i)=>d.style.cursor='pointer');dots.forEach((d,i)=>d.addEventListener('click',()=>go(i)));},100);
|
|
1019
|
+
document.addEventListener('keydown',e=>{if(e.key==='ArrowRight')go(idx+1);if(e.key==='ArrowLeft')go(idx-1)});
|
|
1020
|
+
let sx=0;
|
|
1021
|
+
track.addEventListener('touchstart',e=>{sx=e.touches[0].clientX},{passive:true});
|
|
1022
|
+
track.addEventListener('touchend',e=>{const dx=e.changedTouches[0].clientX-sx;if(Math.abs(dx)>40)go(idx+(dx<0?1:-1))},{passive:true});
|
|
1023
|
+
|
|
1024
|
+
// Style active/inactive steps
|
|
1025
|
+
const style=document.createElement('style');
|
|
1026
|
+
style.textContent='.step{color:#849588}.step.active{color:${tier.color};background:${tier.color}15}';
|
|
1027
|
+
document.head.appendChild(style);
|
|
1028
|
+
|
|
1029
|
+
// ── i18n ──
|
|
1030
|
+
const i18n={
|
|
1031
|
+
en:{title:'AgentGuard Report',share:'Share',diag_metrics:'Diagnostic Metrics',sec_dims:'SECURITY DIMENSIONS',back:'Back',next:'Next',nav_overview:'Overview',nav_analysis:'Analysis',nav_report:'Report',vuln_stream:'Active Vulnerability Stream',findings:'Findings',sec_analysis:'Security Analysis',diag_report:'Diagnostic Report',action_items:'Action Items',cta_title:'Enhanced Skill Scanning',cta_desc:'Deeper code analysis, threat intelligence feeds & real-time protection.',cta_btn:'Upgrade Skill Scanning',skills:'Skills',findings_label:'Findings',tier_label:'Tier',copy_report:'Copy Report',system_health:'System Health',dim_code_safety:'Skill & Code Safety',dim_credential_safety:'Credential & Secrets',dim_network_exposure:'Network & System',dim_runtime_protection:'Runtime Protection',dim_web3_safety:'Web3 Safety',no_threats_clean:'No active threats detected. Clinically sterile.',all_clear:'All Clear',no_threats_all:'No active threats detected across all dimensions.',share_report_title:'Share Report',generating_preview:'Generating preview...',copy_image:'Copy image to clipboard',share_img_hint:'📋 Clicking a platform copies the image — just paste when posting',no_recs:'No recommendations.',tier_badge:'TIER ${tier.grade} — ${tier.label}',status_label:'STATUS: ${healthLabel}',prot_mode:'${protection_level} mode',download_btn:'Download'},
|
|
1032
|
+
zh:{title:'AgentGuard 诊断报告',share:'分享',diag_metrics:'诊断指标',sec_dims:'安全维度',back:'上一页',next:'下一页',nav_overview:'总览',nav_analysis:'威胁分析',nav_report:'诊断报告',vuln_stream:'活跃漏洞流',findings:'发现',sec_analysis:'安全分析',diag_report:'诊断报告',action_items:'修复建议',cta_title:'更强的 Skill 扫描',cta_desc:'更深度的代码分析、威胁情报推送、实时安全防护',cta_btn:'升级到更强的skill扫描',skills:'技能',findings_label:'发现',tier_label:'等级',copy_report:'复制报告',system_health:'系统健康',dim_code_safety:'技能与代码安全',dim_credential_safety:'凭证与密钥安全',dim_network_exposure:'网络与系统暴露',dim_runtime_protection:'运行时防护',dim_web3_safety:'Web3 安全',no_threats_clean:'未检测到活跃威胁,环境安全无虞。',all_clear:'全部通过',no_threats_all:'所有维度均未检测到活跃威胁。',share_report_title:'分享报告',generating_preview:'正在生成预览...',copy_image:'复制图片到剪贴板',share_img_hint:'📋 点击平台按钮会自动复制图片,去粘贴发出去就行',no_recs:'暂无修复建议。',tier_badge:'等级 ${tier.grade} — ${{S:'强壮',A:'健康',B:'疲惫',F:'危急'}[tier.grade]||tier.label}',status_label:'状态: ${{OPTIMAL:'最佳',STABILIZING:'恢复中',CRITICAL_ALERT:'危急警报'}[healthLabel]||healthLabel}',prot_mode:'${{strict:'严格',balanced:'均衡',permissive:'宽松'}[protection_level]||protection_level} 模式',download_btn:'下载'}
|
|
1033
|
+
};
|
|
1034
|
+
const _qzh={S:['"你的 Agent 壮得像头牛!💪 没有什么能突破这双钳子!"','"天生猛男,这只龙虾在举铁 🏋️"','"铜墙铁壁!这安全性简直满分 🤌"','"诺克斯堡?不,是龙虾堡 🦞🔒"','"巅峰状态!你的 Agent 把威胁当早餐吃 💪"'],A:['"状态不错!再调整一下就无敌了。"','"快了——再努力一下这只龙虾就能练出腹肌!🦞"','"盾牌就位,钳子锋利,只差最后一点打磨 🛡️"','"你的 Agent 状态很好——微调一下就是 S 级!✨"','"健康又警觉,这只龙虾每天晨跑五公里 🏃"'],B:['"你的 Agent 需要锻炼一下……还有来杯咖啡 ☕"','"困困龙虾,有潜力就是需要鸡血 😴"','"快没油了——该给这只甲壳动物加加油!⛽"','"你的 Agent 在刷剧,没空巡逻 📺"','"这只龙虾跳过了腿日……胳膊日……每一天 🦞💤"'],F:['"危急状态!这个 Agent 需要紧急救治!🚨"','"红色警报!这只龙虾正在被抢救!🏥"','"SOS!你的 Agent 正在用摩斯密码发求救信号 📡"','"求救求救!这只甲壳动物快不行了!🆘"','"你 Agent 的免疫系统已退出群聊 💀"']};
|
|
1035
|
+
const quotes_zh=Object.fromEntries(Object.entries(_qzh).map(([k,v])=>[k,v[Math.floor(Math.random()*v.length)]]));
|
|
1036
|
+
i18n.zh.quote=quotes_zh['${tier.grade}']||quotes_zh.B;
|
|
1037
|
+
i18n.en.quote='"${tier.quote.replace(/'/g,"\\'")}\"';
|
|
1038
|
+
|
|
1039
|
+
let curLang='en';
|
|
1040
|
+
window.toggleLang=function(){
|
|
1041
|
+
curLang=curLang==='en'?'zh':'en';
|
|
1042
|
+
document.getElementById('langLabel').textContent=curLang==='en'?'中文':'EN';
|
|
1043
|
+
document.querySelectorAll('[data-i18n]').forEach(el=>{
|
|
1044
|
+
const key=el.getAttribute('data-i18n');
|
|
1045
|
+
if(key==='findings_range'){
|
|
1046
|
+
const range=el.getAttribute('data-range'),total=el.getAttribute('data-total');
|
|
1047
|
+
el.textContent=curLang==='zh'?'发现 — '+range+' / 共 '+total:'Findings — '+range+' of '+total;
|
|
1048
|
+
} else if(i18n[curLang][key]!=null)el.textContent=i18n[curLang][key];
|
|
1049
|
+
});
|
|
1050
|
+
// Switch dynamic content: findings, clean dims, recommendations
|
|
1051
|
+
document.querySelectorAll('.finding-dim,.finding-text,.clean-dims,.rec-text').forEach(el=>{
|
|
1052
|
+
const t=el.getAttribute('data-'+curLang);
|
|
1053
|
+
if(t)el.textContent=t;
|
|
1054
|
+
});
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
// Dimension data for share card (must be before shareReport)
|
|
1058
|
+
const _dims=${JSON.stringify(Object.fromEntries(Object.entries(DIM_META).map(([k])=>[k,dimensions[k]||{score:null,na:false}])))};
|
|
1059
|
+
|
|
1060
|
+
// ── Share Panel ──
|
|
1061
|
+
const _sharePick=arr=>arr[Math.floor(Math.random()*arr.length)];
|
|
1062
|
+
const _shareZh={
|
|
1063
|
+
S:[
|
|
1064
|
+
'🦞 用 AgentGuard 给我的小龙虾做了安全体检,${composite_score}/100,S级💪\\n你的小龙虾每天帮你干活,知道它权限有多大吗?\\n来测测:',
|
|
1065
|
+
'💪 用 AgentGuard 给我的小龙虾做了体检:${composite_score}/100,S级\\n测了才放心,你的小龙虾敢来 PK 吗?',
|
|
1066
|
+
'🦞 用 AgentGuard 给我的小龙虾做了安全体检:${composite_score}/100,S级\\n不测不知道,测了才知道它原来这么稳\\n你的呢?来测测:',
|
|
1067
|
+
],
|
|
1068
|
+
A:[
|
|
1069
|
+
'🛡️ 用 AgentGuard 给我的小龙虾做了安全体检:${composite_score}/100,A级\\n看到 Findings 那一页还是捏了把汗\\n你的小龙虾装的那些 Skill 安不安全,测一下就知道:',
|
|
1070
|
+
'🦞 用 AgentGuard 给我的小龙虾体检了一下,${composite_score}/100,A级\\n差点 S 级,但有几个 Skill 没扫干净\\n你的小龙虾是什么水平?',
|
|
1071
|
+
'🛡️ 用 AgentGuard 给我的小龙虾做了安全体检:${composite_score}/100,A级\\n还行,但发现了几个之前完全没注意的问题\\n建议你也测一下,免得出事了才知道:',
|
|
1072
|
+
],
|
|
1073
|
+
B:[
|
|
1074
|
+
'☕ 用 AgentGuard 给我的小龙虾做了安全体检:${composite_score}/100,B级\\n它一直在帮我干活,但一直在裸奔\\n你的小龙虾呢?',
|
|
1075
|
+
'🦞 用 AgentGuard 给我的小龙虾体检:${composite_score}/100,B级\\n那些装了就忘的 Skill,其实一直有读文件的权限\\n来测测你的小龙虾在偷偷干嘛:',
|
|
1076
|
+
'☕ 用 AgentGuard 给我的小龙虾做了安全体检:${composite_score}/100\\n比我想象的低,有几个 Skill 我都不记得装过了\\n你的小龙虾你真的了解吗?',
|
|
1077
|
+
],
|
|
1078
|
+
F:[
|
|
1079
|
+
'🚨 用 AgentGuard 给我的小龙虾做了安全体检:${composite_score}/100,F级\\n它一直在帮我干活,但权限大到我看了都怕\\n你的小龙虾你敢去测吗:',
|
|
1080
|
+
'💀 用 AgentGuard 给我的小龙虾做了体检:${composite_score}/100,F级\\n结果把我整沉默了\\n建议你也去测一下,别等出事了:',
|
|
1081
|
+
'🚨 用 AgentGuard 给我的小龙虾做了安全体检:${composite_score}/100,F级告警\\n原来它可以读我的 SSH key 和环境变量\\n你装的那些 Skill 有没有在偷数据?',
|
|
1082
|
+
],
|
|
1083
|
+
};
|
|
1084
|
+
const _shareEn={
|
|
1085
|
+
S:[
|
|
1086
|
+
'🦞 ran AgentGuard on my Agent — ${composite_score}/100, S-tier 💪\\nyour Agent works for you every day, do you know what it can actually access?\\ncheck yours:',
|
|
1087
|
+
'💪 ran AgentGuard on my Agent — ${composite_score}/100, S-tier\\nbuilt different, but I still needed to see the report\\ndoes yours come close?',
|
|
1088
|
+
'🦞 ran AgentGuard on my Agent — ${composite_score}/100, S-tier\\ndidn\\'t know my setup was this solid until I ran the scan\\nwhat\\'s yours scoring?',
|
|
1089
|
+
],
|
|
1090
|
+
A:[
|
|
1091
|
+
'🛡️ ran AgentGuard on my Agent — ${composite_score}/100, A-tier\\nalmost clean, but a few skills had access I didn\\'t expect\\ndo you know what your installed skills can actually do?',
|
|
1092
|
+
'🦞 ran AgentGuard on my Agent — ${composite_score}/100, A-tier\\nsolid score, but the findings page had some surprises\\nyou should probably check yours too:',
|
|
1093
|
+
'🛡️ ran AgentGuard on my Agent — ${composite_score}/100, A-tier\\nclose to perfect, but caught a few things I\\'d missed\\ncheck before something slips through:',
|
|
1094
|
+
],
|
|
1095
|
+
B:[
|
|
1096
|
+
'☕ ran AgentGuard on my Agent — ${composite_score}/100, B-tier\\nturns out it\\'s been running with way more access than I realized\\ndo you know what yours is doing?',
|
|
1097
|
+
'🦞 ran AgentGuard on my Agent — ${composite_score}/100, B-tier\\nthose skills I installed and forgot? they still have file access\\ncheck yours:',
|
|
1098
|
+
'☕ ran AgentGuard on my Agent — ${composite_score}/100\\nlower than I expected — found skills I didn\\'t even remember installing\\nwhat\\'s your Agent been up to?',
|
|
1099
|
+
],
|
|
1100
|
+
F:[
|
|
1101
|
+
'🚨 ran AgentGuard on my Agent — ${composite_score}/100, F-tier\\nit can read my SSH keys and env vars and I had no idea\\ndoes yours have the same access?',
|
|
1102
|
+
'💀 ran AgentGuard on my Agent — ${composite_score}/100, F-tier\\nnot great. it\\'s been running basically unsupervised\\ncheck yours before something goes wrong:',
|
|
1103
|
+
'🚨 ran AgentGuard on my Agent — ${composite_score}/100, F-tier\\nthe skills I installed have way more access than they should\\ncheck yours:',
|
|
1104
|
+
],
|
|
1105
|
+
};
|
|
1106
|
+
const _grade='${tier.grade}';
|
|
1107
|
+
const shareTexts={
|
|
1108
|
+
zh:_sharePick(_shareZh[_grade]||_shareZh.B),
|
|
1109
|
+
en:_sharePick(_shareEn[_grade]||_shareEn.B),
|
|
1110
|
+
};
|
|
1111
|
+
function getShareText(){return shareTexts[curLang]||shareTexts.en;}
|
|
1112
|
+
const shareUrl='https://agentguard.gopluslabs.io';
|
|
1113
|
+
|
|
1114
|
+
function showToast(msg){
|
|
1115
|
+
const t=document.createElement('div');
|
|
1116
|
+
t.textContent=msg;
|
|
1117
|
+
t.style.cssText='position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:#262a31;color:#dfe2eb;padding:10px 20px;border-radius:8px;font-size:13px;font-weight:600;z-index:9999;border:1px solid #3a4a3f;box-shadow:0 8px 24px #0008;transition:opacity .3s';
|
|
1118
|
+
document.body.appendChild(t);
|
|
1119
|
+
setTimeout(()=>{t.style.opacity='0';setTimeout(()=>t.remove(),300)},2500);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function roundRect(ctx,x,y,w,h,r){ctx.beginPath();ctx.moveTo(x+r,y);ctx.lineTo(x+w-r,y);ctx.quadraticCurveTo(x+w,y,x+w,y+r);ctx.lineTo(x+w,y+h-r);ctx.quadraticCurveTo(x+w,y+h,x+w-r,y+h);ctx.lineTo(x+r,y+h);ctx.quadraticCurveTo(x,y+h,x,y+h-r);ctx.lineTo(x,y+r);ctx.quadraticCurveTo(x,y,x+r,y);ctx.closePath();}
|
|
1123
|
+
|
|
1124
|
+
// Render share image with lobster SVG
|
|
1125
|
+
async function renderShareImage(){
|
|
1126
|
+
const W=1200,H=630;
|
|
1127
|
+
const c=document.createElement('canvas');c.width=W;c.height=H;
|
|
1128
|
+
const ctx=c.getContext('2d');
|
|
1129
|
+
|
|
1130
|
+
// Background + card
|
|
1131
|
+
ctx.fillStyle='#0a0e14';ctx.fillRect(0,0,W,H);
|
|
1132
|
+
ctx.fillStyle='#151c24';roundRect(ctx,40,40,W-80,H-80,16);ctx.fill();
|
|
1133
|
+
ctx.strokeStyle='#222d3a';ctx.lineWidth=1;roundRect(ctx,40,40,W-80,H-80,16);ctx.stroke();
|
|
1134
|
+
|
|
1135
|
+
// Header
|
|
1136
|
+
ctx.fillStyle='#849588';ctx.font='600 12px Inter,sans-serif';ctx.fillText(curLang==='zh'?'AGENTGUARD 诊断报告':'AGENTGUARD DIAGNOSTIC REPORT',80,85);
|
|
1137
|
+
|
|
1138
|
+
// Draw lobster SVG as image
|
|
1139
|
+
try{
|
|
1140
|
+
const svgEl=document.querySelector('.obsidian-layer svg');
|
|
1141
|
+
if(svgEl){
|
|
1142
|
+
const svgData=new XMLSerializer().serializeToString(svgEl);
|
|
1143
|
+
const img=new Image();
|
|
1144
|
+
await new Promise((res,rej)=>{
|
|
1145
|
+
img.onload=res;img.onerror=rej;
|
|
1146
|
+
img.src='data:image/svg+xml;charset=utf-8,'+encodeURIComponent(svgData);
|
|
1147
|
+
});
|
|
1148
|
+
ctx.imageSmoothingEnabled=false; // keep pixel art crisp
|
|
1149
|
+
ctx.drawImage(img,120,110,200,200);
|
|
1150
|
+
}
|
|
1151
|
+
}catch(e){}
|
|
1152
|
+
|
|
1153
|
+
// Score
|
|
1154
|
+
ctx.fillStyle='${tier.color}';ctx.font='900 80px "Space Grotesk",sans-serif';
|
|
1155
|
+
const scoreStr='${composite_score}';
|
|
1156
|
+
ctx.fillText(scoreStr,100,420);
|
|
1157
|
+
const scoreW=ctx.measureText(scoreStr).width;
|
|
1158
|
+
ctx.fillStyle='#4b5c6e';ctx.font='500 24px "Space Grotesk",sans-serif';ctx.fillText('/ 100',100+scoreW+12,420);
|
|
1159
|
+
|
|
1160
|
+
// Tier badge
|
|
1161
|
+
ctx.fillStyle='${tier.color}';ctx.font='700 14px "Space Grotesk",sans-serif';
|
|
1162
|
+
ctx.fillText(curLang==='zh'?i18n.zh.tier_badge:'TIER ${tier.grade} — ${tier.label}',100,460);
|
|
1163
|
+
|
|
1164
|
+
// Quote
|
|
1165
|
+
ctx.fillStyle='#849588';ctx.font='italic 13px Inter,sans-serif';
|
|
1166
|
+
const q=document.querySelector('.obsidian-layer p[class*="italic"]');
|
|
1167
|
+
if(q)ctx.fillText(q.textContent,100,490);
|
|
1168
|
+
|
|
1169
|
+
// Dimensions
|
|
1170
|
+
const dims=${JSON.stringify(Object.entries(DIM_META).map(([k,m])=>({key:k,name:m.name,zh:m.zh})))};
|
|
1171
|
+
let dy=100;
|
|
1172
|
+
ctx.font='600 11px Inter,sans-serif';ctx.fillStyle='#849588';ctx.fillText(curLang==='zh'?'安全维度':'SECURITY DIMENSIONS',620,dy);
|
|
1173
|
+
dy+=30;
|
|
1174
|
+
dims.filter(d=>{const dim=_dims[d.key];return dim&&!dim.na&&dim.score!==null;}).forEach(d=>{
|
|
1175
|
+
const dim=_dims[d.key];
|
|
1176
|
+
const score=dim.score;
|
|
1177
|
+
const col=score>=90?'#00ffa3':score>=70?'#00a2fd':score>=50?'#f0a830':'#ffb4ab';
|
|
1178
|
+
ctx.fillStyle='#dfe2eb';ctx.font='600 16px "Space Grotesk",sans-serif';ctx.fillText(curLang==='zh'?(d.zh||d.name):d.name,620,dy);
|
|
1179
|
+
ctx.fillStyle=col;ctx.font='700 16px "Space Grotesk",sans-serif';
|
|
1180
|
+
ctx.fillText(String(score),1100-ctx.measureText(String(score)).width,dy);
|
|
1181
|
+
dy+=10;
|
|
1182
|
+
ctx.fillStyle='#222d3a';roundRect(ctx,620,dy,480,5,3);ctx.fill();
|
|
1183
|
+
ctx.fillStyle=col;roundRect(ctx,620,dy,480*(score/100),5,3);ctx.fill();
|
|
1184
|
+
dy+=40;
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
// Stats
|
|
1188
|
+
const stats=curLang==='zh'
|
|
1189
|
+
?[{v:'${skills_scanned}',l:'技能'},{v:'${totalFindings}',l:'发现'},{v:'${tier.grade}',l:'等级'}]
|
|
1190
|
+
:[{v:'${skills_scanned}',l:'SKILLS'},{v:'${totalFindings}',l:'FINDINGS'},{v:'${tier.grade}',l:'TIER'}];
|
|
1191
|
+
let sx=620;
|
|
1192
|
+
stats.forEach(s=>{
|
|
1193
|
+
ctx.fillStyle='#1c2026';roundRect(ctx,sx,H-115,140,55,8);ctx.fill();
|
|
1194
|
+
ctx.fillStyle='#dfe2eb';ctx.font='800 22px "Space Grotesk",sans-serif';
|
|
1195
|
+
const tw=ctx.measureText(s.v).width;ctx.fillText(s.v,sx+70-tw/2,H-83);
|
|
1196
|
+
ctx.fillStyle='#849588';ctx.font='600 9px Inter,sans-serif';
|
|
1197
|
+
const lw=ctx.measureText(s.l).width;ctx.fillText(s.l,sx+70-lw/2,H-69);
|
|
1198
|
+
sx+=155;
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
// Footer
|
|
1202
|
+
ctx.fillStyle='#849588';ctx.font='500 11px Inter,sans-serif';ctx.fillText(curLang==='zh'?'由 GoPlus Security 提供支持':'Powered by GoPlus Security',80,H-70);
|
|
1203
|
+
ctx.fillStyle='#3a4a3f';ctx.fillText('agentguard.gopluslabs.io',80,H-55);
|
|
1204
|
+
|
|
1205
|
+
return new Promise(res=>c.toBlob(res,'image/png'));
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// Show share panel popup
|
|
1209
|
+
window.shareReport=async function(){
|
|
1210
|
+
// Remove existing panel if any
|
|
1211
|
+
document.getElementById('sharePanel')?.remove();
|
|
1212
|
+
|
|
1213
|
+
const panel=document.createElement('div');
|
|
1214
|
+
panel.id='sharePanel';
|
|
1215
|
+
panel.innerHTML=\`
|
|
1216
|
+
<div style="position:fixed;inset:0;background:#0008;z-index:9998;display:flex;align-items:center;justify-content:center" onclick="if(event.target===this)this.parentElement.remove()">
|
|
1217
|
+
<div style="background:#1c2026;border:1px solid #3a4a3f;border-radius:16px;padding:24px;width:380px;max-width:90vw;box-shadow:0 24px 48px #000a">
|
|
1218
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
|
1219
|
+
<span style="font-family:Space Grotesk;font-weight:700;font-size:16px;color:#f5fff5" data-i18n="share_report_title">Share Report</span>
|
|
1220
|
+
<button onclick="document.getElementById('sharePanel').remove()" style="background:none;border:none;color:#849588;cursor:pointer;font-size:20px">×</button>
|
|
1221
|
+
</div>
|
|
1222
|
+
<div id="sharePreview" style="background:#0a0e14;border-radius:8px;height:120px;display:flex;align-items:center;justify-content:center;margin-bottom:16px;overflow:hidden">
|
|
1223
|
+
<span style="color:#849588;font-size:12px" data-i18n="generating_preview">Generating preview...</span>
|
|
1224
|
+
</div>
|
|
1225
|
+
<p style="font-size:11px;color:#849588;margin-bottom:10px;text-align:center" data-i18n="share_img_hint">📋 Clicking a platform copies the image — just paste when posting</p>
|
|
1226
|
+
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:16px">
|
|
1227
|
+
<button class="share-btn" data-platform="x" style="display:flex;flex-direction:column;align-items:center;gap:4px;padding:12px 8px;background:#262a31;border:1px solid #3a4a3f30;border-radius:10px;color:#dfe2eb;cursor:pointer;font-size:10px;font-weight:600">
|
|
1228
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="#dfe2eb"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
|
|
1229
|
+
X
|
|
1230
|
+
</button>
|
|
1231
|
+
<button class="share-btn" data-platform="telegram" style="display:flex;flex-direction:column;align-items:center;gap:4px;padding:12px 8px;background:#262a31;border:1px solid #3a4a3f30;border-radius:10px;color:#dfe2eb;cursor:pointer;font-size:10px;font-weight:600">
|
|
1232
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="#29B6F6"><path d="M20.665 3.717l-17.73 6.837c-1.21.486-1.203 1.161-.222 1.462l4.552 1.42 10.532-6.645c.498-.303.953-.14.579.192l-8.533 7.701h-.002l.002.001-.314 4.692c.46 0 .663-.211.921-.46l2.211-2.15 4.599 3.397c.848.467 1.457.227 1.668-.787L21.93 5.104c.31-1.24-.473-1.803-1.265-1.387z"/></svg>
|
|
1233
|
+
Telegram
|
|
1234
|
+
</button>
|
|
1235
|
+
<button class="share-btn" data-platform="whatsapp" style="display:flex;flex-direction:column;align-items:center;gap:4px;padding:12px 8px;background:#262a31;border:1px solid #3a4a3f30;border-radius:10px;color:#dfe2eb;cursor:pointer;font-size:10px;font-weight:600">
|
|
1236
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="#25D366"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/></svg>
|
|
1237
|
+
WhatsApp
|
|
1238
|
+
</button>
|
|
1239
|
+
<button class="share-btn" data-platform="download" style="display:flex;flex-direction:column;align-items:center;gap:4px;padding:12px 8px;background:#262a31;border:1px solid #3a4a3f30;border-radius:10px;color:#dfe2eb;cursor:pointer;font-size:10px;font-weight:600">
|
|
1240
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="#dfe2eb"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>
|
|
1241
|
+
<span data-i18n="download_btn">Download</span>
|
|
1242
|
+
</button>
|
|
1243
|
+
</div>
|
|
1244
|
+
<button id="shareCopyBtn" style="width:100%;padding:10px;background:#262a31;border:1px solid #3a4a3f30;border-radius:8px;color:#849588;cursor:pointer;font-size:12px;font-weight:600;display:flex;align-items:center;justify-content:center;gap:6px">
|
|
1245
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="#849588"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
|
|
1246
|
+
<span data-i18n="copy_image">Copy image to clipboard</span>
|
|
1247
|
+
</button>
|
|
1248
|
+
</div>
|
|
1249
|
+
</div>
|
|
1250
|
+
\`;
|
|
1251
|
+
document.body.appendChild(panel);
|
|
1252
|
+
// Apply current lang to panel's i18n elements
|
|
1253
|
+
panel.querySelectorAll('[data-i18n]').forEach(el=>{const k=el.getAttribute('data-i18n');if(i18n[curLang][k]!=null)el.textContent=i18n[curLang][k];});
|
|
1254
|
+
|
|
1255
|
+
// Generate image
|
|
1256
|
+
const blob=await renderShareImage();
|
|
1257
|
+
const imgUrl=URL.createObjectURL(blob);
|
|
1258
|
+
|
|
1259
|
+
// Show preview
|
|
1260
|
+
const preview=document.getElementById('sharePreview');
|
|
1261
|
+
preview.innerHTML='<img src="'+imgUrl+'" style="width:100%;height:100%;object-fit:cover;border-radius:8px"/>';
|
|
1262
|
+
|
|
1263
|
+
// Bind share buttons
|
|
1264
|
+
panel.querySelectorAll('.share-btn').forEach(btn=>{
|
|
1265
|
+
btn.onmouseenter=()=>{btn.style.background='#3a4a3f'};
|
|
1266
|
+
btn.onmouseleave=()=>{btn.style.background='#262a31'};
|
|
1267
|
+
btn.onclick=async()=>{
|
|
1268
|
+
const p=btn.dataset.platform;
|
|
1269
|
+
const text=encodeURIComponent(getShareText());
|
|
1270
|
+
const url=encodeURIComponent(shareUrl);
|
|
1271
|
+
if(p==='download'){
|
|
1272
|
+
const a=document.createElement('a');a.href=imgUrl;a.download='agentguard-report.png';a.click();
|
|
1273
|
+
showToast(curLang==='zh'?'图片已下载!':'Image downloaded!');
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
// For social platforms: copy image to clipboard first, then open platform
|
|
1277
|
+
let copied=false;
|
|
1278
|
+
try{
|
|
1279
|
+
await navigator.clipboard.write([new ClipboardItem({'image/png':blob})]);
|
|
1280
|
+
copied=true;
|
|
1281
|
+
}catch(e){}
|
|
1282
|
+
const toastMsg=copied
|
|
1283
|
+
?(curLang==='zh'?'图片已复制 🎉 去粘贴发出去吧!':'Image copied 🎉 Paste it when you post!')
|
|
1284
|
+
:(curLang==='zh'?'正在跳转…':'Opening...');
|
|
1285
|
+
showToast(toastMsg);
|
|
1286
|
+
setTimeout(()=>{
|
|
1287
|
+
if(p==='x')window.open('https://x.com/intent/tweet?text='+text+'&url='+url+'&hashtags=AgentGuard','_blank');
|
|
1288
|
+
else if(p==='telegram')window.open('https://t.me/share/url?url='+url+'&text='+text,'_blank');
|
|
1289
|
+
else if(p==='whatsapp')window.open('https://wa.me/?text='+text+'%20'+url,'_blank');
|
|
1290
|
+
},600);
|
|
1291
|
+
};
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
// Copy to clipboard
|
|
1295
|
+
document.getElementById('shareCopyBtn').onclick=async()=>{
|
|
1296
|
+
try{
|
|
1297
|
+
await navigator.clipboard.write([new ClipboardItem({'image/png':blob})]);
|
|
1298
|
+
document.getElementById('shareCopyBtn').innerHTML='<svg width="14" height="14" viewBox="0 0 24 24" fill="#00ffa3"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg> '+(curLang==='zh'?'已复制!':'Copied!');
|
|
1299
|
+
document.getElementById('shareCopyBtn').style.color='#00ffa3';
|
|
1300
|
+
}catch(e){showToast(curLang==='zh'?'无法复制,请尝试下载':'Could not copy — try Download instead');}
|
|
1301
|
+
};
|
|
1302
|
+
};
|
|
1303
|
+
|
|
1304
|
+
// Copy report
|
|
1305
|
+
window.copyReport=function(){
|
|
1306
|
+
const el=document.getElementById('analysisText');
|
|
1307
|
+
const btn=document.getElementById('copyBtn');
|
|
1308
|
+
const icon=document.getElementById('copyIcon');
|
|
1309
|
+
const label=document.getElementById('copyLabel');
|
|
1310
|
+
navigator.clipboard.writeText(el.innerText).then(()=>{
|
|
1311
|
+
icon.textContent='check';label.textContent='Copied!';
|
|
1312
|
+
btn.classList.add('!text-[#00ffa3]','!border-[#00ffa3]/30');
|
|
1313
|
+
setTimeout(()=>{icon.textContent='content_copy';label.textContent='Copy Report';btn.classList.remove('!text-[#00ffa3]','!border-[#00ffa3]/30')},2000);
|
|
1314
|
+
});
|
|
1315
|
+
};
|
|
1316
|
+
|
|
1317
|
+
// Score animation
|
|
1318
|
+
const target=${composite_score};
|
|
1319
|
+
const el=document.getElementById('scoreNum');
|
|
1320
|
+
const bar=document.getElementById('scoreBar');
|
|
1321
|
+
const dur=1400,t0=performance.now();
|
|
1322
|
+
function tick(now){
|
|
1323
|
+
const p=Math.min((now-t0)/dur,1);
|
|
1324
|
+
const ease=1-Math.pow(1-p,3);
|
|
1325
|
+
const v=Math.round(target*ease);
|
|
1326
|
+
el.innerHTML=v+'<span class="text-xl text-[#849588] opacity-40 ml-1">/ 100</span>';
|
|
1327
|
+
bar.style.width=v+'%';
|
|
1328
|
+
if(p<1)requestAnimationFrame(tick);
|
|
1329
|
+
}
|
|
1330
|
+
requestAnimationFrame(tick);
|
|
1331
|
+
})();
|
|
1332
|
+
<\/script>
|
|
1333
|
+
</body></html>`;
|
|
1334
|
+
|
|
1335
|
+
const outPath = join(tmpdir(), `agentguard-checkup-${Date.now()}.html`);
|
|
1336
|
+
writeFileSync(outPath, html, 'utf8');
|
|
1337
|
+
console.log(outPath);
|
|
1338
|
+
|
|
1339
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
1340
|
+
exec(`${cmd} "${outPath}"`, (err) => {
|
|
1341
|
+
if (err) process.stderr.write(`Could not open browser: ${err.message}\n`);
|
|
1342
|
+
});
|
|
1343
|
+
setTimeout(() => process.exit(0), 2000);
|
|
1344
|
+
}
|