@fiyuu/runtime 0.1.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/inspector.ts DELETED
@@ -1,329 +0,0 @@
1
- import { promises as fs, existsSync } from "node:fs";
2
- import path from "node:path";
3
- import type { FeatureRecord, FiyuuConfig } from "@fiyuu/core";
4
-
5
- type InsightCategory = "security" | "performance" | "design" | "architecture";
6
- type InsightSeverity = "low" | "medium" | "high";
7
-
8
- export interface InsightItem {
9
- id: string;
10
- category: InsightCategory;
11
- severity: InsightSeverity;
12
- title: string;
13
- summary: string;
14
- recommendation: string;
15
- route?: string;
16
- file?: string;
17
- fixable: boolean;
18
- }
19
-
20
- export interface InsightsReport {
21
- generatedAt: string;
22
- summary: {
23
- total: number;
24
- high: number;
25
- medium: number;
26
- low: number;
27
- };
28
- items: InsightItem[];
29
- assistant: {
30
- mode: "rule-only";
31
- status: "ready";
32
- details: string;
33
- suggestions: string[];
34
- };
35
- }
36
-
37
- interface BuildInsightsOptions {
38
- rootDirectory: string;
39
- appDirectory: string;
40
- features: FeatureRecord[];
41
- config?: FiyuuConfig;
42
- }
43
-
44
- export async function buildInsightsReport(options: BuildInsightsOptions): Promise<InsightsReport> {
45
- const items = await collectInsightItems(options);
46
- const assistant = await buildAssistantOutput(options, items);
47
-
48
- const summary = {
49
- total: items.length,
50
- high: items.filter((item) => item.severity === "high").length,
51
- medium: items.filter((item) => item.severity === "medium").length,
52
- low: items.filter((item) => item.severity === "low").length,
53
- };
54
-
55
- return {
56
- generatedAt: new Date().toISOString(),
57
- summary,
58
- items,
59
- assistant,
60
- };
61
- }
62
-
63
- async function collectInsightItems(options: BuildInsightsOptions): Promise<InsightItem[]> {
64
- const items: InsightItem[] = [];
65
-
66
- for (const feature of options.features) {
67
- items.push(...toFeatureStructureInsights(feature));
68
-
69
- const fileEntries = Object.entries(feature.files).filter((entry): entry is [string, string] => Boolean(entry[1]));
70
- for (const [, filePath] of fileEntries) {
71
- const source = await readFileSafe(filePath);
72
- if (!source) {
73
- continue;
74
- }
75
-
76
- if (/dangerouslySetInnerHTML/.test(source)) {
77
- items.push({
78
- id: `security-dangerous-html-${filePath}`,
79
- category: "security",
80
- severity: "high",
81
- title: "Potential XSS surface detected",
82
- summary: "`dangerouslySetInnerHTML` is used in a route module.",
83
- recommendation: "Prefer escaped rendering or sanitize content before passing HTML strings.",
84
- route: feature.route,
85
- file: filePath,
86
- fixable: true,
87
- });
88
- }
89
-
90
- if (/\beval\s*\(|new\s+Function\s*\(/.test(source)) {
91
- items.push({
92
- id: `security-dynamic-eval-${filePath}`,
93
- category: "security",
94
- severity: "high",
95
- title: "Dynamic code execution detected",
96
- summary: "`eval` or `new Function` appears in application logic.",
97
- recommendation: "Replace dynamic execution with explicit, typed control flow.",
98
- route: feature.route,
99
- file: filePath,
100
- fixable: true,
101
- });
102
- }
103
-
104
- const lineCount = source.split(/\r?\n/).length;
105
- if (lineCount > 450) {
106
- items.push({
107
- id: `performance-large-module-${filePath}`,
108
- category: "performance",
109
- severity: "medium",
110
- title: "Large route module",
111
- summary: `Module has ${lineCount} lines and may increase parse and hydration cost.`,
112
- recommendation: "Split heavy route logic into smaller server/client helpers.",
113
- route: feature.route,
114
- file: filePath,
115
- fixable: true,
116
- });
117
- }
118
- }
119
-
120
- if (feature.files["meta.ts"]) {
121
- const metaSource = await readFileSafe(feature.files["meta.ts"]);
122
- if (metaSource) {
123
- const seoTitle = extractSeoField(metaSource, "title");
124
- const seoDescription = extractSeoField(metaSource, "description");
125
-
126
- if (!seoTitle) {
127
- items.push({
128
- id: `design-missing-seo-title-${feature.route}`,
129
- category: "design",
130
- severity: "low",
131
- title: "SEO title missing in meta",
132
- summary: "`meta.ts` exists but does not define `seo.title`.",
133
- recommendation: "Add route-specific `seo.title` to improve previews and search snippets.",
134
- route: feature.route,
135
- file: feature.files["meta.ts"],
136
- fixable: true,
137
- });
138
- } else if (seoTitle.length > 62) {
139
- items.push({
140
- id: `design-seo-title-length-${feature.route}`,
141
- category: "design",
142
- severity: "low",
143
- title: "SEO title is longer than recommended",
144
- summary: `Current title length is ${seoTitle.length} characters.`,
145
- recommendation: "Keep seo titles around 45-60 characters to avoid truncation in SERP previews.",
146
- route: feature.route,
147
- file: feature.files["meta.ts"],
148
- fixable: true,
149
- });
150
- }
151
-
152
- if (!seoDescription) {
153
- items.push({
154
- id: `design-missing-seo-description-${feature.route}`,
155
- category: "design",
156
- severity: "medium",
157
- title: "SEO description missing in meta",
158
- summary: "`meta.ts` does not define `seo.description`.",
159
- recommendation: "Add a concise description (90-160 chars) for better search and social previews.",
160
- route: feature.route,
161
- file: feature.files["meta.ts"],
162
- fixable: true,
163
- });
164
- } else if (seoDescription.length < 80 || seoDescription.length > 170) {
165
- items.push({
166
- id: `design-seo-description-length-${feature.route}`,
167
- category: "design",
168
- severity: "low",
169
- title: "SEO description length can be improved",
170
- summary: `Current description length is ${seoDescription.length} characters.`,
171
- recommendation: "Keep seo descriptions in the 90-160 character range for reliable previews.",
172
- route: feature.route,
173
- file: feature.files["meta.ts"],
174
- fixable: true,
175
- });
176
- }
177
- }
178
- }
179
- }
180
-
181
- items.push(...(await collectGlobalSecurityInsights(options.appDirectory)));
182
- items.push(...collectGlobalRenderInsights(options.features));
183
- return dedupeInsights(items);
184
- }
185
-
186
- function toFeatureStructureInsights(feature: FeatureRecord): InsightItem[] {
187
- const items: InsightItem[] = [];
188
-
189
- for (const missing of feature.missingRequiredFiles) {
190
- items.push({
191
- id: `architecture-missing-${feature.route}-${missing}`,
192
- category: "architecture",
193
- severity: missing === "schema.ts" ? "high" : "medium",
194
- title: `Required file missing: ${missing}`,
195
- summary: `Feature ${feature.route} is missing ${missing}.`,
196
- recommendation: "Complete the feature contract so tooling and AI context stay deterministic.",
197
- route: feature.route,
198
- file: path.join(feature.directory, missing),
199
- fixable: true,
200
- });
201
- }
202
-
203
- return items;
204
- }
205
-
206
- async function collectGlobalSecurityInsights(appDirectory: string): Promise<InsightItem[]> {
207
- const items: InsightItem[] = [];
208
- const middlewarePath = path.join(appDirectory, "middleware.ts");
209
- if (existsSync(middlewarePath)) {
210
- const middlewareSource = await readFileSafe(middlewarePath);
211
- if (middlewareSource && /access-control-allow-origin["']?\s*[:,=]\s*["']\*/i.test(middlewareSource)) {
212
- items.push({
213
- id: "security-open-cors-middleware",
214
- category: "security",
215
- severity: "high",
216
- title: "Wildcard CORS header in middleware",
217
- summary: "Middleware appears to allow all origins globally.",
218
- recommendation: "Restrict allowed origins by environment and keep credentials disabled for public origins.",
219
- file: middlewarePath,
220
- fixable: true,
221
- });
222
- }
223
- }
224
-
225
- return items;
226
- }
227
-
228
- function collectGlobalRenderInsights(features: FeatureRecord[]): InsightItem[] {
229
- const csrCount = features.filter((feature) => feature.render === "csr").length;
230
- if (features.length < 4) {
231
- return [];
232
- }
233
-
234
- const ratio = csrCount / features.length;
235
- if (ratio < 0.6) {
236
- return [];
237
- }
238
-
239
- return [
240
- {
241
- id: "performance-csr-heavy",
242
- category: "performance",
243
- severity: ratio > 0.8 ? "high" : "medium",
244
- title: "Project is CSR-heavy",
245
- summary: `${csrCount}/${features.length} routes render in CSR mode.`,
246
- recommendation: "Move content-driven pages to SSR where possible to reduce blank-first-paint risk.",
247
- fixable: true,
248
- },
249
- ];
250
- }
251
-
252
- async function buildAssistantOutput(options: BuildInsightsOptions, items: InsightItem[]): Promise<InsightsReport["assistant"]> {
253
- void options;
254
- return {
255
- mode: "rule-only",
256
- status: "ready",
257
- details: "Using deterministic project checks (no integrated model).",
258
- suggestions: createRuleBasedSuggestions(items),
259
- };
260
- }
261
-
262
- function createRuleBasedSuggestions(items: InsightItem[]): string[] {
263
- if (items.length === 0) {
264
- return [
265
- "Run `fiyuu doctor --fix` after scaffolding new routes to keep contracts deterministic.",
266
- "Keep SEO descriptions in 12-28 words and route-specific titles for stable search snippets.",
267
- "Add focused tests around auth, middleware, and API routes before release builds.",
268
- ];
269
- }
270
-
271
- const topItems = items
272
- .slice()
273
- .sort((left, right) => severityScore(right.severity) - severityScore(left.severity))
274
- .slice(0, 4);
275
- return [
276
- ...topItems.map((item) => `${capitalize(item.category)}: ${item.recommendation}`),
277
- "Use `fiyuu doctor --fix` for safe auto-fixes (SEO fields, missing execute(), fallback pages, className->class).",
278
- ].slice(0, 6);
279
- }
280
-
281
- function dedupeInsights(items: InsightItem[]): InsightItem[] {
282
- const seen = new Set<string>();
283
- const output: InsightItem[] = [];
284
- for (const item of items) {
285
- if (seen.has(item.id)) {
286
- continue;
287
- }
288
- seen.add(item.id);
289
- output.push(item);
290
- }
291
- return output;
292
- }
293
-
294
- async function readFileSafe(filePath: string): Promise<string | null> {
295
- try {
296
- return await fs.readFile(filePath, "utf8");
297
- } catch {
298
- return null;
299
- }
300
- }
301
-
302
- function severityScore(severity: InsightSeverity): number {
303
- if (severity === "high") {
304
- return 3;
305
- }
306
- if (severity === "medium") {
307
- return 2;
308
- }
309
- return 1;
310
- }
311
-
312
- function capitalize(value: string): string {
313
- return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
314
- }
315
-
316
- function extractSeoField(source: string, field: "title" | "description"): string | null {
317
- const seoBlock = source.match(/seo\s*:\s*\{([\s\S]*?)\}/m);
318
- if (!seoBlock) {
319
- return null;
320
- }
321
-
322
- const fieldRegex = new RegExp(`${field}\\s*:\\s*(["'\`])([\\s\\S]*?)\\1`, "m");
323
- const match = seoBlock[1].match(fieldRegex);
324
- if (!match) {
325
- return null;
326
- }
327
-
328
- return match[2].trim() || null;
329
- }
@@ -1,133 +0,0 @@
1
- /**
2
- * Dev-only browser script generators for the Fiyuu runtime.
3
- * These return inline <script> strings injected into the rendered document.
4
- */
5
-
6
- import type { RenderMode } from "@fiyuu/core";
7
-
8
- export function renderInsightsPanelScript(): string {
9
- return `<script type="module">
10
- const host=document.createElement('div');
11
- host.style.cssText='position:fixed;left:16px;bottom:16px;z-index:9998;font:12px/1.5 ui-monospace,monospace';
12
- const toggle=document.createElement('button');
13
- toggle.textContent='Fiyuu Insights';
14
- toggle.style.cssText='border:1px solid rgba(197,214,181,.18);background:rgba(18,24,19,.94);color:#f8f3ea;border-radius:999px;padding:10px 14px;box-shadow:0 18px 56px rgba(0,0,0,.22);cursor:pointer';
15
- const panel=document.createElement('aside');
16
- panel.style.cssText='display:none;margin-top:10px;width:min(420px,calc(100vw - 30px));max-height:min(72vh,620px);overflow:auto;background:rgba(18,24,19,.96);color:#f8f3ea;border:1px solid rgba(197,214,181,.16);border-radius:18px;padding:14px 16px;box-shadow:0 18px 56px rgba(0,0,0,.22);backdrop-filter:blur(14px)';
17
- toggle.addEventListener('click',()=>{panel.style.display=panel.style.display==='none'?'block':'none';});
18
-
19
- function renderItems(items){
20
- if(!items.length){return '<li style="margin-top:6px">No findings.</li>'}
21
- return items.slice(0,8).map((item)=>'<li style="margin-top:8px"><strong>['+item.severity.toUpperCase()+'] '+item.title+'</strong><br/><span style="opacity:.82">'+item.summary+'</span><br/><span style="opacity:.7">'+item.recommendation+'</span></li>').join('');
22
- }
23
-
24
- async function mount(){
25
- const response=await fetch('/__fiyuu/insights');
26
- const data=await response.json();
27
- const byCategory={
28
- security:(data.items||[]).filter((item)=>item.category==='security'),
29
- performance:(data.items||[]).filter((item)=>item.category==='performance'),
30
- design:(data.items||[]).filter((item)=>item.category==='design'),
31
- architecture:(data.items||[]).filter((item)=>item.category==='architecture'),
32
- };
33
- panel.innerHTML=''
34
- +'<div style="display:flex;justify-content:space-between;gap:10px;align-items:center"><strong>AI Insights</strong><span style="opacity:.7">'+(data.generatedAt||'')+'</span></div>'
35
- +'<p style="margin:8px 0 0;opacity:.82">'+data.summary.high+' high · '+data.summary.medium+' medium · '+data.summary.low+' low</p>'
36
- +'<div style="margin-top:10px;display:flex;gap:8px"><button data-tab="findings" style="padding:6px 10px;border-radius:999px;border:1px solid rgba(197,214,181,.16);background:rgba(255,255,255,.06);color:#f8f3ea;cursor:pointer">Findings</button><button data-tab="assistant" style="padding:6px 10px;border-radius:999px;border:1px solid rgba(197,214,181,.16);background:transparent;color:#f8f3ea;cursor:pointer">Assistant</button></div>'
37
- +'<section data-view="findings" style="margin-top:10px">'
38
- +'<p style="margin:0"><strong>Security</strong></p><ul style="margin:6px 0 0;padding-left:18px">'+renderItems(byCategory.security)+'</ul>'
39
- +'<p style="margin:10px 0 0"><strong>Performance</strong></p><ul style="margin:6px 0 0;padding-left:18px">'+renderItems(byCategory.performance)+'</ul>'
40
- +'<p style="margin:10px 0 0"><strong>Design</strong></p><ul style="margin:6px 0 0;padding-left:18px">'+renderItems(byCategory.design)+'</ul>'
41
- +'<p style="margin:10px 0 0"><strong>Architecture</strong></p><ul style="margin:6px 0 0;padding-left:18px">'+renderItems(byCategory.architecture)+'</ul>'
42
- +'</section>'
43
- +'<section data-view="assistant" style="display:none;margin-top:10px">'
44
- +'<p style="margin:0;opacity:.84">Mode: '+data.assistant.mode+' · '+data.assistant.status+'</p>'
45
- +'<p style="margin:8px 0 0;opacity:.72">'+data.assistant.details+'</p>'
46
- +'<ul style="margin:8px 0 0;padding-left:18px">'+(data.assistant.suggestions||[]).map((line)=>'<li style="margin-top:6px">'+line+'</li>').join('')+'</ul>'
47
- +'</section>';
48
-
49
- const findingsButton=panel.querySelector('[data-tab="findings"]');
50
- const assistantButton=panel.querySelector('[data-tab="assistant"]');
51
- const findingsView=panel.querySelector('[data-view="findings"]');
52
- const assistantView=panel.querySelector('[data-view="assistant"]');
53
- findingsButton?.addEventListener('click',()=>{findingsView.style.display='block';assistantView.style.display='none';findingsButton.style.background='rgba(255,255,255,.06)';assistantButton.style.background='transparent';});
54
- assistantButton?.addEventListener('click',()=>{findingsView.style.display='none';assistantView.style.display='block';assistantButton.style.background='rgba(255,255,255,.06)';findingsButton.style.background='transparent';});
55
- }
56
-
57
- mount().catch((error)=>{console.warn('Fiyuu insights panel failed',error);});
58
- host.append(toggle,panel);
59
- document.body.append(host);
60
- </script>`;
61
- }
62
-
63
- export function renderUnifiedToolsScript(input: {
64
- route: string;
65
- render: RenderMode;
66
- renderTimeMs: number;
67
- warnings: string[];
68
- requestId: string;
69
- }): string {
70
- const metrics = JSON.stringify({
71
- route: input.route,
72
- render: input.render,
73
- renderTimeMs: input.renderTimeMs,
74
- warnings: input.warnings,
75
- requestId: input.requestId,
76
- });
77
-
78
- return `<script type="module">(function(){
79
- const metrics=${metrics};
80
- const removeLegacyPanels=()=>{const old=[...document.querySelectorAll('button')].filter((button)=>button.textContent==='Fiyuu Devtools'||button.textContent==='Fiyuu Insights');for(const button of old){const host=button.closest('div');if(host&&host.parentNode){host.parentNode.removeChild(host);}}};
81
- removeLegacyPanels();
82
- setInterval(removeLegacyPanels,500);
83
-
84
- const host=document.createElement('div');
85
- host.style.cssText='position:fixed;right:16px;bottom:16px;z-index:10001;font:12px/1.5 ui-monospace,monospace';
86
- const toggle=document.createElement('button');
87
- toggle.textContent='Fiyuu Console';
88
- toggle.style.cssText='border:1px solid rgba(197,214,181,.18);background:rgba(18,24,19,.94);color:#f8f3ea;border-radius:999px;padding:10px 14px;box-shadow:0 18px 56px rgba(0,0,0,.22);cursor:pointer';
89
- const panel=document.createElement('aside');
90
- panel.style.cssText='display:none;margin-top:10px;width:min(500px,calc(100vw - 30px));max-height:min(78vh,700px);overflow:auto;background:rgba(18,24,19,.96);color:#f8f3ea;border:1px solid rgba(197,214,181,.16);border-radius:18px;padding:14px 16px;box-shadow:0 18px 56px rgba(0,0,0,.22);backdrop-filter:blur(14px)';
91
- toggle.addEventListener('click',()=>{panel.style.display=panel.style.display==='none'?'block':'none';});
92
-
93
- let serverTraceEnabled=false;
94
- let pollingId;
95
- const renderItems=(items)=>{if(!items.length){return '<li style="margin-top:6px">No findings.</li>';}return items.slice(0,8).map((item)=>'<li style="margin-top:8px"><strong>['+item.severity.toUpperCase()+'] '+item.title+'</strong><br/><span style="opacity:.82">'+item.summary+'</span><br/><span style="opacity:.72">'+item.recommendation+'</span></li>').join('');};
96
- const renderServerItems=(events)=>{if(!events||!events.length){return '<li style="margin-top:6px">No server activity yet.</li>';}return events.slice(0,40).map((event)=>'<li style="margin-top:8px"><strong>'+event.event+'</strong><span style="opacity:.7"> ['+event.level.toUpperCase()+']</span><br/><span style="opacity:.75">'+event.at+'</span><br/><span style="opacity:.82">'+(event.details||'')+'</span></li>').join('');};
97
- const refreshServerEvents=async()=>{if(!serverTraceEnabled){return;}const response=await fetch('/__fiyuu/server-events');if(!response.ok){return;}const payload=await response.json();const list=panel.querySelector('[data-server-list]');if(list){list.innerHTML=renderServerItems(payload.events||[]);}};
98
- const startServerTrace=()=>{if(pollingId){clearInterval(pollingId);}pollingId=setInterval(()=>{refreshServerEvents().catch(()=>{});},1200);refreshServerEvents().catch(()=>{});};
99
- const stopServerTrace=()=>{if(pollingId){clearInterval(pollingId);pollingId=undefined;}};
100
-
101
- const mount=async()=>{
102
- const [runtimeResponse,insightsResponse]=await Promise.all([fetch('/__fiyuu/devtools'),fetch('/__fiyuu/insights')]);
103
- const runtime=runtimeResponse.ok?await runtimeResponse.json():{warnings:[],config:{featureFlags:{}}};
104
- const insights=insightsResponse.ok?await insightsResponse.json():{summary:{high:0,medium:0,low:0},assistant:{mode:'rule-only',status:'fallback',details:'unavailable'},items:[]};
105
- const warnings=(metrics.warnings.length?metrics.warnings:(runtime.warnings||[])).slice(0,4);
106
- const flags=Object.entries((runtime.config&&runtime.config.featureFlags)||{}).map(([k,v])=>'<span style="display:inline-flex;margin:4px 6px 0 0;padding:3px 8px;border-radius:999px;background:rgba(233,240,224,.08)">'+k+': '+v+'</span>').join('')||'<span style="opacity:.7">none</span>';
107
- const byCategory={security:(insights.items||[]).filter((i)=>i.category==='security'),performance:(insights.items||[]).filter((i)=>i.category==='performance'),design:(insights.items||[]).filter((i)=>i.category==='design'),architecture:(insights.items||[]).filter((i)=>i.category==='architecture')};
108
-
109
- panel.innerHTML=''
110
- +'<div style="display:flex;justify-content:space-between;gap:10px;align-items:center"><strong>Fiyuu Console</strong><span style="opacity:.7">'+metrics.requestId+'</span></div>'
111
- +'<div style="margin-top:10px;display:flex;gap:8px"><button data-tab="runtime" style="padding:6px 10px;border-radius:999px;border:1px solid rgba(197,214,181,.16);background:rgba(255,255,255,.06);color:#f8f3ea;cursor:pointer">Runtime</button><button data-tab="insights" style="padding:6px 10px;border-radius:999px;border:1px solid rgba(197,214,181,.16);background:transparent;color:#f8f3ea;cursor:pointer">Insights</button><button data-tab="server" style="padding:6px 10px;border-radius:999px;border:1px solid rgba(197,214,181,.16);background:transparent;color:#f8f3ea;cursor:pointer">Server</button></div>'
112
- +'<section data-view="runtime" style="margin-top:12px"><p style="margin:0;opacity:.86">Route <strong>'+metrics.route+'</strong> · '+String(metrics.render).toUpperCase()+' · '+metrics.renderTimeMs+'ms</p><p style="margin:8px 0 0;opacity:.7">Warnings</p><ul style="margin:6px 0 0;padding-left:18px">'+(warnings.map((w)=>'<li style="margin-top:6px">'+w+'</li>').join('')||'<li style="margin-top:6px">none</li>')+'</ul><p style="margin:10px 0 0;opacity:.7">Feature Flags</p><div style="margin-top:6px">'+flags+'</div></section>'
113
- +'<section data-view="insights" style="display:none;margin-top:12px"><p style="margin:0;opacity:.86">'+insights.summary.high+' high · '+insights.summary.medium+' medium · '+insights.summary.low+' low</p><p style="margin:8px 0 0;opacity:.72">Assistant: '+insights.assistant.mode+' ('+insights.assistant.status+')</p><p style="margin:8px 0 0;opacity:.72">'+insights.assistant.details+'</p><div style="margin-top:10px"><strong>Security</strong><ul style="margin:6px 0 0;padding-left:18px">'+renderItems(byCategory.security)+'</ul></div><div style="margin-top:10px"><strong>Performance</strong><ul style="margin:6px 0 0;padding-left:18px">'+renderItems(byCategory.performance)+'</ul></div><div style="margin-top:10px"><strong>Design</strong><ul style="margin:6px 0 0;padding-left:18px">'+renderItems(byCategory.design)+'</ul></div><div style="margin-top:10px"><strong>Architecture</strong><ul style="margin:6px 0 0;padding-left:18px">'+renderItems(byCategory.architecture)+'</ul></div></section>'
114
- +'<section data-view="server" style="display:none;margin-top:12px"><label style="display:flex;align-items:center;gap:8px"><input data-server-toggle type="checkbox"/> <span>Enable live server trace (dev only)</span></label><ul data-server-list style="margin:10px 0 0;padding-left:18px"></ul></section>';
115
-
116
- const runtimeButton=panel.querySelector('[data-tab="runtime"]');
117
- const insightsButton=panel.querySelector('[data-tab="insights"]');
118
- const serverButton=panel.querySelector('[data-tab="server"]');
119
- const runtimeView=panel.querySelector('[data-view="runtime"]');
120
- const insightsView=panel.querySelector('[data-view="insights"]');
121
- const serverView=panel.querySelector('[data-view="server"]');
122
- const serverToggle=panel.querySelector('[data-server-toggle]');
123
- runtimeButton?.addEventListener('click',()=>{runtimeView.style.display='block';insightsView.style.display='none';serverView.style.display='none';runtimeButton.style.background='rgba(255,255,255,.06)';insightsButton.style.background='transparent';serverButton.style.background='transparent';});
124
- insightsButton?.addEventListener('click',()=>{runtimeView.style.display='none';insightsView.style.display='block';serverView.style.display='none';insightsButton.style.background='rgba(255,255,255,.06)';runtimeButton.style.background='transparent';serverButton.style.background='transparent';});
125
- serverButton?.addEventListener('click',()=>{runtimeView.style.display='none';insightsView.style.display='none';serverView.style.display='block';serverButton.style.background='rgba(255,255,255,.06)';runtimeButton.style.background='transparent';insightsButton.style.background='transparent';});
126
- serverToggle?.addEventListener('change',(event)=>{serverTraceEnabled=Boolean(event.target&&event.target.checked);if(serverTraceEnabled){startServerTrace();}else{stopServerTrace();}});
127
- };
128
-
129
- mount().catch((error)=>{console.warn('Fiyuu console mount failed',error);});
130
- host.append(toggle,panel);
131
- document.body.append(host);
132
- })();</script>`;
133
- }