@fluojs/studio 1.0.0 → 1.0.2

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.ko.md CHANGED
@@ -41,7 +41,7 @@ pnpm add @fluojs/studio
41
41
 
42
42
  ## 빠른 시작
43
43
 
44
- Studio는 fluo CLI에서 내보낸 JSON 파일을 소비합니다. 런타임은 snapshot을 생산하고, CLI는 검사 데이터를 내보내거나 위임하며, Studio는 뷰어와 자동화 호출자가 사용할 수 있도록 snapshot을 파싱, 필터링, 보기, 렌더링하는 공개 헬퍼를 소유합니다. 지원되는 inspect artifact에는 raw snapshot, standalone timing diagnostics, snapshot-plus-timing envelope, `fluo inspect --report`가 생성한 report artifact가 포함됩니다.
44
+ Studio는 fluo CLI에서 내보낸 JSON 파일을 소비합니다. 런타임은 snapshot을 생산하고, CLI는 검사 데이터를 내보내거나 위임하며, Studio는 뷰어와 자동화 호출자가 사용할 수 있도록 snapshot을 파싱, 필터링, 보기, 렌더링하는 공개 헬퍼를 소유합니다. 지원되는 inspect artifact에는 raw snapshot, snapshot-plus-timing envelope, `fluo inspect --report`가 생성한 report artifact, legacy standalone timing diagnostics가 포함됩니다.
45
45
 
46
46
  1. **Snapshot 내보내기**:
47
47
  ```bash
@@ -69,6 +69,7 @@ Studio는 fluo CLI에서 내보낸 JSON 파일을 소비합니다. 런타임은
69
69
  **Diagnostics issues** 섹션을 사용하여 런타임 부트스트랩 과정에서 수집된 이슈들을 확인합니다.
70
70
  - 심각도(Error, Warning)별로 필터링합니다.
71
71
  - `fixHint`를 통해 문제를 해결하기 위한 구체적인 조치 방법을 확인합니다.
72
+ - `docsUrl`은 절대 `http:` 또는 `https:` URL일 때만 연결된 문서를 열 수 있습니다. 안전하지 않은 scheme과 상대 경로 또는 URL이 아닌 문자열은 클릭할 수 없으며, Studio는 이를 escaped text로 렌더링합니다.
72
73
  - `dependsOn`을 통해 어떤 컴포넌트가 실패 지점을 차단하고 있는지 확인합니다.
73
74
 
74
75
  ### 아키텍처 다이어그램 내보내기
@@ -88,7 +89,7 @@ Studio는 주로 웹 애플리케이션이지만, 배포된 패키지는 도구/
88
89
  | `PlatformDiagnosticIssue` | 플랫폼 오류 보고 및 수정을 위한 스키마입니다. |
89
90
  | `parseStudioPayload(rawJson)` | raw snapshot JSON, standalone timing JSON, snapshot+timing envelope, `fluo inspect --report` artifact를 받아 parsed payload와 원본 JSON string을 반환합니다. |
90
91
  | `ParsedPayload` | `parseStudioPayload(...)`가 반환하는 parsed Studio payload shape입니다. |
91
- | `StudioPayload` | 허용되는 raw snapshot/timing/report input shape입니다. |
92
+ | `StudioPayload` | `parseStudioPayload(...)`가 반환하는 정규화된 parsed payload envelope이며, 선택적 `report`, `snapshot`, `timing` 필드를 포함합니다. |
92
93
  | `StudioReportArtifact` | CI/support 자동화를 위해 summary, snapshot, timing 데이터를 함께 보존한 `fluo inspect --report` artifact입니다. |
93
94
  | `StudioReportSummary` | report artifact에 포함되는 summary block입니다. |
94
95
  | `FilterState` | `applyFilters(...)`가 받는 filter configuration입니다. |
package/README.md CHANGED
@@ -41,7 +41,7 @@ The published package serves two caller-facing entrypoints:
41
41
 
42
42
  ## Quick Start
43
43
 
44
- Studio consumes JSON exports from the fluo CLI. Runtime produces snapshots, the CLI exports or delegates inspection data, and Studio owns the public helpers that parse, filter, view, and render those snapshots for viewer and automation callers. Supported inspect artifacts include raw snapshots, standalone timing diagnostics, snapshot-plus-timing envelopes, and report artifacts produced by `fluo inspect --report`.
44
+ Studio consumes JSON exports from the fluo CLI. Runtime produces snapshots, the CLI exports or delegates inspection data, and Studio owns the public helpers that parse, filter, view, and render those snapshots for viewer and automation callers. Supported inspect artifacts include raw snapshots, snapshot-plus-timing envelopes, report artifacts produced by `fluo inspect --report`, and legacy standalone timing diagnostics.
45
45
 
46
46
  1. **Export a snapshot**:
47
47
  ```bash
@@ -69,6 +69,7 @@ Studio consumes JSON exports from the fluo CLI. Runtime produces snapshots, the
69
69
  Use the **Diagnostics issues** section to see issues collected during the runtime bootstrap process.
70
70
  - Filter by severity (Error, Warning).
71
71
  - Use `fixHint` to get actionable advice on how to resolve the issue.
72
+ - Use `docsUrl` to open linked documentation when it is an absolute `http:` or `https:` URL. Unsafe schemes and relative or non-URL strings are not clickable; Studio renders them as escaped text instead.
72
73
  - View `dependsOn` to see which components are blocking the failing one.
73
74
 
74
75
  ### Exporting Architecture Diagrams
@@ -88,7 +89,7 @@ Studio is primarily a web application, but the published package also exposes th
88
89
  | `PlatformDiagnosticIssue` | Schema for reporting and fixing platform errors. |
89
90
  | `parseStudioPayload(rawJson)` | Accepts raw snapshot JSON, standalone timing JSON, snapshot+timing envelopes, and `fluo inspect --report` artifacts; returns the parsed payload plus the original JSON string. |
90
91
  | `ParsedPayload` | Parsed Studio payload shape returned by `parseStudioPayload(...)`. |
91
- | `StudioPayload` | Accepted raw snapshot/timing/report input shapes. |
92
+ | `StudioPayload` | Normalized parsed payload envelope returned by `parseStudioPayload(...)`, with optional `report`, `snapshot`, and `timing` fields. |
92
93
  | `StudioReportArtifact` | Preserved `fluo inspect --report` artifact with summary, snapshot, and timing data for CI/support automation. |
93
94
  | `StudioReportSummary` | Summary block included in report artifacts. |
94
95
  | `FilterState` | Filter configuration accepted by `applyFilters(...)`. |
@@ -1 +1 @@
1
- :root{color-scheme:light dark;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}body{margin:0;background:#0b1020;color:#e5e7eb}main{max-width:1200px;margin:0 auto;padding:24px}h1,h2,h3{margin-top:0}code{background:#1f2937;border-radius:4px;padding:2px 6px}.card{border:1px solid #334155;border-radius:10px;background:#111827;padding:16px;margin-bottom:16px}.split-grid{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(320px,1fr))}.actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}button{background:#2563eb;border:none;color:#fff;padding:8px 12px;border-radius:8px;cursor:pointer}button:disabled{background:#475569;cursor:not-allowed}label{display:block;margin-bottom:10px}input[type=text]{width:100%;margin-top:6px;border-radius:8px;border:1px solid #334155;background:#0f172a;color:#e5e7eb;padding:8px 10px}.filter-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:10px}.muted{color:#94a3b8}.chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px}.chip{border:1px solid #334155;border-radius:999px;padding:2px 8px;font-size:12px}table{width:100%;border-collapse:collapse}th,td{border-bottom:1px solid #334155;text-align:left;font-size:13px;padding:6px}pre{overflow:auto;max-height:300px;background:#0f172a;border:1px solid #334155;padding:12px;border-radius:8px}.notice{margin-top:8px;color:#93c5fd}.uploader.drag-active{border-color:#60a5fa;box-shadow:0 0 0 2px #60a5fa59 inset}#graph-host{overflow:auto;border:1px solid #334155;border-radius:8px;background:#0f172a}.edge-line{stroke:#64748b;stroke-width:1.6}.edge-arrow{fill:#64748b}.module-node{fill:#1e293b;stroke:#64748b;stroke-width:1.5;cursor:pointer}.module-root{stroke:#3b82f6;stroke-width:2.6}.component-ready{fill:#14532d}.component-degraded{fill:#7c2d12}.component-not-ready{fill:#7f1d1d}.module-selected{stroke:#f59e0b;stroke-width:3}.module-label{fill:#e5e7eb;font-size:11px;pointer-events:none}.diagnostics-list{display:grid;gap:12px}.issue{margin-bottom:0}.issue.severity-error{border-color:#ef4444}.issue.severity-warning{border-color:#f59e0b}.issue.severity-info{border-color:#3b82f6}a{color:#93c5fd}
1
+ :root{color-scheme:light dark;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}body{margin:0;background:#0b1020;color:#e5e7eb}main{max-width:1200px;margin:0 auto;padding:24px}h1,h2,h3{margin-top:0}code{background:#1f2937;border-radius:4px;padding:2px 6px}.card{border:1px solid #334155;border-radius:10px;background:#111827;padding:16px;margin-bottom:16px}.split-grid{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(320px,1fr))}.actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}button{background:#2563eb;border:none;color:#fff;padding:8px 12px;border-radius:8px;cursor:pointer}button:disabled{background:#475569;cursor:not-allowed}label{display:block;margin-bottom:10px}input[type=text]{width:100%;margin-top:6px;border-radius:8px;border:1px solid #334155;background:#0f172a;color:#e5e7eb;padding:8px 10px}.filter-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:10px}.muted{color:#94a3b8}.chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px}.chip{border:1px solid #334155;border-radius:999px;padding:2px 8px;font-size:12px}table{width:100%;border-collapse:collapse}th,td{border-bottom:1px solid #334155;text-align:left;font-size:13px;padding:6px}pre{overflow:auto;max-height:300px;background:#0f172a;border:1px solid #334155;padding:12px;border-radius:8px}.notice{margin-top:8px;color:#93c5fd}.uploader.drag-active{border-color:#60a5fa;box-shadow:0 0 0 2px #60a5fa59 inset}#graph-host{overflow:auto;border:1px solid #334155;border-radius:8px;background:#0f172a}.edge-line{stroke:#64748b;stroke-width:1.6}.edge-arrow{fill:#64748b}.module-node{fill:#1e293b;stroke:#64748b;stroke-width:1.5;cursor:pointer}.module-root{stroke:#3b82f6;stroke-width:2.6}.component-ready{fill:#14532d}.component-degraded{fill:#7c2d12}.component-not-ready{fill:#7f1d1d}.component-external{fill:#312e81;cursor:default}.module-selected{stroke:#f59e0b;stroke-width:3}.module-label{fill:#e5e7eb;font-size:11px;pointer-events:none}.diagnostics-list{display:grid;gap:12px}.issue{margin-bottom:0}.issue.severity-error{border-color:#ef4444}.issue.severity-warning{border-color:#f59e0b}.issue.severity-info{border-color:#3b82f6}a{color:#93c5fd}
@@ -0,0 +1,134 @@
1
+ (function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))o(s);new MutationObserver(s=>{for(const r of s)if(r.type==="childList")for(const c of r.addedNodes)c.tagName==="LINK"&&c.rel==="modulepreload"&&o(c)}).observe(document,{childList:!0,subtree:!0});function n(s){const r={};return s.integrity&&(r.integrity=s.integrity),s.referrerPolicy&&(r.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?r.credentials="include":s.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function o(s){if(s.ep)return;s.ep=!0;const r=n(s);fetch(s.href,r)}})();function h(e){return typeof e=="object"&&e!==null}function $(e,t){return Object.hasOwn(e,t)}function O(e){return Array.isArray(e)&&e.every(t=>typeof t=="string")}function L(e){return e==="ready"||e==="not-ready"||e==="degraded"}function A(e){return e==="healthy"||e==="unhealthy"||e==="degraded"}function k(e){return e==="error"||e==="warning"||e==="info"}function R(e){if(!h(e))return null;if(typeof e.generatedAt!="string"||!h(e.readiness)||!h(e.health)||!Array.isArray(e.components)||!Array.isArray(e.diagnostics))throw new Error("Invalid platform snapshot payload.");if(!L(e.readiness.status)||typeof e.readiness.critical!="boolean")throw new Error("Invalid aggregate readiness in platform snapshot payload.");if(!A(e.health.status))throw new Error("Invalid aggregate health in platform snapshot payload.");for(const t of e.components){if(!h(t))throw new Error("Invalid component entry in platform snapshot payload.");if(typeof t.id!="string"||typeof t.kind!="string"||typeof t.state!="string"||!h(t.readiness)||!h(t.health)||!O(t.dependencies)||!h(t.telemetry)||!h(t.ownership)||!h(t.details))throw new Error("Invalid component shape in platform snapshot payload.");if(!L(t.readiness.status)||typeof t.readiness.critical!="boolean")throw new Error("Invalid component readiness in platform snapshot payload.");if(!A(t.health.status))throw new Error("Invalid component health in platform snapshot payload.");if(typeof t.telemetry.namespace!="string"||!h(t.telemetry.tags)||typeof t.ownership.ownsResources!="boolean"||typeof t.ownership.externallyManaged!="boolean")throw new Error("Invalid component telemetry/ownership in platform snapshot payload.")}for(const t of e.diagnostics){if(!h(t))throw new Error("Invalid diagnostics issue entry in platform snapshot payload.");if(typeof t.code!="string"||!k(t.severity)||typeof t.componentId!="string"||typeof t.message!="string")throw new Error("Invalid diagnostics issue shape in platform snapshot payload.");if(t.cause!==void 0&&typeof t.cause!="string"||t.fixHint!==void 0&&typeof t.fixHint!="string"||t.docsUrl!==void 0&&typeof t.docsUrl!="string"||t.dependsOn!==void 0&&!O(t.dependsOn))throw new Error("Invalid optional diagnostics issue fields in platform snapshot payload.")}return e}function F(e){if(!h(e))return null;if(e.version!==1)throw new Error("Unsupported bootstrap timing version. Expected version: 1.");if(typeof e.totalMs!="number"||!Array.isArray(e.phases))throw new Error("Invalid bootstrap timing payload.");for(const t of e.phases)if(!h(t)||typeof t.name!="string"||typeof t.durationMs!="number")throw new Error("Invalid phase entry in bootstrap timing payload.");return e}function J(e){if(!h(e))throw new Error("Invalid inspect report summary payload.");if(typeof e.componentCount!="number"||typeof e.diagnosticCount!="number"||typeof e.errorCount!="number"||!A(e.healthStatus)||!L(e.readinessStatus)||typeof e.timingTotalMs!="number"||typeof e.warningCount!="number")throw new Error("Invalid inspect report summary payload.");return e}function q(e){return e.summary!==void 0||$(e,"snapshot")&&$(e,"timing")&&($(e,"generatedAt")||$(e,"version"))}function H(e,t,n){if(!h(e)||!q(e))return null;if(e.summary===void 0||e.version!==1||typeof e.generatedAt!="string"||!t||!n)throw new Error("Invalid inspect report artifact payload.");return{generatedAt:e.generatedAt,snapshot:t,summary:J(e.summary),timing:n,version:1}}function P(e){const t=JSON.parse(e),n=h(t)?t:void 0,o=n!==void 0&&$(n,"snapshot"),s=n!==void 0&&$(n,"timing"),r=n!==void 0&&!o&&!s&&$(n,"version")&&$(n,"totalMs")&&$(n,"phases"),c=R(o?n.snapshot:r?void 0:t),l=F(s?n.timing:c?void 0:t),i=H(t,c,l);if(!c&&!l)throw new Error("Unsupported file format. Expected platform snapshot JSON or timing JSON.");return{payload:{...i?{report:i}:{},...c?{snapshot:c}:{},...l?{timing:l}:{}},rawJson:e}}function U(e,t){const n=t.query.trim().toLowerCase(),o=e.components.filter(r=>t.readinessStatuses.length>0&&!t.readinessStatuses.includes(r.readiness.status)?!1:n?r.id.toLowerCase().includes(n)||r.kind.toLowerCase().includes(n)||r.dependencies.some(c=>c.toLowerCase().includes(n)):!0),s=e.diagnostics.filter(r=>{var c,l,i,m;return t.severities.length>0&&!t.severities.includes(r.severity)?!1:n?r.code.toLowerCase().includes(n)||r.componentId.toLowerCase().includes(n)||r.message.toLowerCase().includes(n)||(((c=r.cause)==null?void 0:c.toLowerCase().includes(n))??!1)||(((l=r.fixHint)==null?void 0:l.toLowerCase().includes(n))??!1)||(((i=r.docsUrl)==null?void 0:i.toLowerCase().includes(n))??!1)||(((m=r.dependsOn)==null?void 0:m.some(f=>f.toLowerCase().includes(n)))??!1):!0});return{...e,components:o,diagnostics:s}}function b(e){return e.replaceAll("\\","\\\\").replaceAll('"','\\"').replaceAll(`\r
2
+ `,"\\n").replaceAll("\r","\\n").replaceAll(`
3
+ `,"\\n")}function _(e){return e.replaceAll(/[^a-zA-Z0-9_]/g,"_")}function z(e){let t=2166136261;for(const n of e)t^=n.codePointAt(0)??0,t=Math.imul(t,16777619)>>>0;return t.toString(16).padStart(8,"0")}function Y(e){return`EXT_${_(e)}_${z(e)}`}function I(e){const t=["graph TD"],n=new Map,o=new Map;if(e.components.length===0)return t.push(' EMPTY["No registered platform components"]'),t.join(`
4
+ `);for(const[c,l]of e.components.entries()){const i=`C${String(c+1)}`;n.set(l.id,i),t.push(` ${i}["${b(l.id)}\\nkind: ${b(l.kind)}\\nreadiness: ${l.readiness.status}\\nhealth: ${l.health.status}"]`)}for(const c of e.components){const l=n.get(c.id);if(l)for(const i of c.dependencies){const m=n.get(i);if(m){t.push(` ${l} --> ${m}`);continue}let f=o.get(i);f||(f=Y(i),o.set(i,f),t.push(` ${f}["${b(i)}"]`)),t.push(` ${l} --> ${f}`)}}const s=[],r=[];for(const c of e.components){const l=n.get(c.id);l&&(c.readiness.status==="degraded"&&s.push(l),c.readiness.status==="not-ready"&&r.push(l))}return s.length>0&&(t.push(` class ${s.join(",")} degraded`),t.push(" classDef degraded stroke:#f59e0b,stroke-width:2px")),r.length>0&&(t.push(` class ${r.join(",")} notReady`),t.push(" classDef notReady stroke:#ef4444,stroke-width:2px")),t.join(`
5
+ `)}function p(e){return e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&#039;")}function B(e){try{const t=new URL(e);if(t.protocol==="https:"||t.protocol==="http:")return t.href}catch{return}}function X(e){const t=B(e),n=p(e);return t?`<p><strong>docs:</strong> <a href="${p(t)}" target="_blank" rel="noopener noreferrer">${n}</a></p>`:`<p><strong>docs:</strong> <span>${n}</span></p>`}function M(e){const t=new Set(e.map(s=>s.id)),n=[],o=new Set;for(const s of e)for(const r of s.dependencies)t.has(r)||o.has(r)||(o.add(r),n.push(r));return n}function V(e,t,n){const o=Math.min(t,n)/2-70,s=t/2,r=n/2,c=new Map,l=M(e.components),i=[...e.components.map(m=>m.id),...l];return i.forEach((m,f)=>{const u=Math.PI*2*f/Math.max(i.length,1);c.set(m,{x:s+o*Math.cos(u),y:r+o*Math.sin(u)})}),c}function G(e,t){const s=e.components,r=V(e,900,460),c=M(s),l=s.flatMap(f=>f.dependencies.map(u=>{const v=r.get(f.id),S=r.get(u);return!v||!S?"":`<line x1="${v.x}" y1="${v.y}" x2="${S.x}" y2="${S.y}" class="edge-line" marker-end="url(#arrow)" />`})).join(""),i=s.map(f=>{const u=r.get(f.id);if(!u)return"";const S=["module-node",f.readiness.status==="not-ready"?"component-not-ready":f.readiness.status==="degraded"?"component-degraded":"component-ready",f.id===t?"module-selected":""].filter(Boolean).join(" ");return`<g>
6
+ <circle cx="${u.x}" cy="${u.y}" r="34" class="${S}" data-component="${p(f.id)}" />
7
+ <text x="${u.x}" y="${u.y+4}" text-anchor="middle" class="module-label">${p(f.id)}</text>
8
+ </g>`}).join(""),m=c.map(f=>{const u=r.get(f);return u?`<g>
9
+ <rect x="${u.x-42}" y="${u.y-20}" width="84" height="40" rx="10" class="module-node component-external" />
10
+ <text x="${u.x}" y="${u.y+4}" text-anchor="middle" class="module-label">${p(f)}</text>
11
+ </g>`:""}).join("");return`<svg viewBox="0 0 900 460" role="img" aria-label="Platform component dependency graph">
12
+ <defs>
13
+ <marker id="arrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
14
+ <polygon points="0 0, 10 3.5, 0 7" class="edge-arrow" />
15
+ </marker>
16
+ </defs>
17
+ ${l}
18
+ ${i}
19
+ ${m}
20
+ </svg>`}function K(e){return e?e.diagnostics.length===0?'<p class="muted">No diagnostics issues.</p>':`<div class="diagnostics-list">
21
+ ${e.diagnostics.map(t=>{const n=t.dependsOn&&t.dependsOn.length>0?`<div class="chips">${t.dependsOn.map(o=>`<span class="chip">dependsOn: ${p(o)}</span>`).join("")}</div>`:"";return`<article class="card issue severity-${p(t.severity)}">
22
+ <h3>${p(t.code)}</h3>
23
+ <p><strong>severity:</strong> ${p(t.severity)} · <strong>component:</strong> ${p(t.componentId)}</p>
24
+ <p>${p(t.message)}</p>
25
+ ${t.cause?`<p><strong>cause:</strong> ${p(t.cause)}</p>`:""}
26
+ ${t.fixHint?`<p><strong>fix hint:</strong> ${p(t.fixHint)}</p>`:""}
27
+ ${t.docsUrl?X(t.docsUrl):""}
28
+ ${n}
29
+ </article>`}).join("")}
30
+ </div>`:'<p class="muted">No platform snapshot loaded.</p>'}const j=document.querySelector("#app");if(!j)throw new Error("App root not found.");const Q=j,W=["ready","degraded","not-ready"],Z=["error","warning","info"],d={filter:{query:"",readinessStatuses:[],severities:[]}};function ee(e,t){const n=new Blob([t],{type:"application/json"}),o=URL.createObjectURL(n),s=document.createElement("a");s.href=o,s.download=e,s.click(),URL.revokeObjectURL(o)}async function N(e){if(!navigator.clipboard)throw new Error("Clipboard API is unavailable.");await navigator.clipboard.writeText(e)}function C(e,t){return e.includes(t)?e.filter(n=>n!==t):[...e,t]}function D(e){return e instanceof HTMLInputElement||e instanceof HTMLTextAreaElement}function te(){const e=document.activeElement;if(!(!D(e)||!e.id))return{id:e.id,selectionEnd:e.selectionEnd,selectionStart:e.selectionStart}}function ne(e){if(!e)return;const t=document.getElementById(e.id);D(t)&&(t.focus({preventScroll:!0}),e.selectionStart!==null&&e.selectionEnd!==null&&t.setSelectionRange(e.selectionStart,e.selectionEnd))}function T(e,t){return!e||e.components.length===0?void 0:(t?e.components.find(o=>o.id===t):void 0)??e.components[0]}function x(){var n;const e=(n=d.payload)==null?void 0:n.snapshot;if(!e){d.filteredSnapshot=void 0,d.selectedComponentId=void 0;return}d.filteredSnapshot=U(e,d.filter);const t=T(d.filteredSnapshot,d.selectedComponentId);d.selectedComponentId=t==null?void 0:t.id}function re(e){if(!e)return'<p class="muted">No component selected.</p>';const t=e.dependencies.length>0?e.dependencies.map(n=>`<span class="chip">dependsOn: ${p(n)}</span>`).join(""):'<span class="chip">dependsOn: none</span>';return`
31
+ <h3>${p(e.id)}</h3>
32
+ <p class="muted">kind: <strong>${p(e.kind)}</strong> · state: <strong>${p(e.state)}</strong></p>
33
+ <div class="chips">
34
+ <span class="chip">readiness: ${p(e.readiness.status)}</span>
35
+ <span class="chip">critical: ${e.readiness.critical?"true":"false"}</span>
36
+ <span class="chip">health: ${p(e.health.status)}</span>
37
+ <span class="chip">ownership: owns=${e.ownership.ownsResources?"true":"false"}/external=${e.ownership.externallyManaged?"true":"false"}</span>
38
+ ${t}
39
+ </div>
40
+ <p class="muted">telemetry namespace: <code>${p(e.telemetry.namespace)}</code></p>
41
+ <h4>Sanitized details</h4>
42
+ <pre>${p(JSON.stringify(e.details,null,2))}</pre>
43
+ `}function se(){var t;const e=(t=d.payload)==null?void 0:t.timing;return e?`
44
+ <p><strong>Total:</strong> ${e.totalMs.toFixed(3)}ms</p>
45
+ <table>
46
+ <thead>
47
+ <tr><th>phase</th><th>duration (ms)</th></tr>
48
+ </thead>
49
+ <tbody>
50
+ ${e.phases.map(n=>`<tr><td>${p(n.name)}</td><td>${n.durationMs.toFixed(3)}</td></tr>`).join("")}
51
+ </tbody>
52
+ </table>
53
+ `:'<p class="muted">Timing not collected.</p>'}function oe(e){if(!e)return'<p class="muted">No platform snapshot loaded.</p>';const t={degraded:e.components.filter(n=>n.readiness.status==="degraded").length,notReady:e.components.filter(n=>n.readiness.status==="not-ready").length,ready:e.components.filter(n=>n.readiness.status==="ready").length};return`
54
+ <div class="chips">
55
+ <span class="chip">generatedAt: ${p(e.generatedAt)}</span>
56
+ <span class="chip">aggregate readiness: ${p(e.readiness.status)}</span>
57
+ <span class="chip">aggregate health: ${p(e.health.status)}</span>
58
+ <span class="chip">components: ${String(e.components.length)}</span>
59
+ <span class="chip">diagnostics: ${String(e.diagnostics.length)}</span>
60
+ <span class="chip">ready/degraded/not-ready: ${t.ready}/${t.degraded}/${t.notReady}</span>
61
+ </div>
62
+ `}function g(e={}){const t=typeof e=="string"?e:e.message,n=typeof e=="string"||!e.preserveFocus?void 0:te(),o=d.filteredSnapshot,s=T(o,d.selectedComponentId),r=o?I(o):"",c=o&&o.components.length>0?G(o,s==null?void 0:s.id):'<p class="muted">No platform components loaded.</p>';Q.innerHTML=`
63
+ <main>
64
+ <header>
65
+ <h1>Fluo Studio Platform Snapshot Viewer</h1>
66
+ <p>Load JSON exported by <code>fluo inspect --json</code> (shared platform snapshot/diagnostic schema) and optionally timing JSON from <code>--timing</code>.</p>
67
+ </header>
68
+
69
+ <section class="card uploader" id="drop-zone">
70
+ <h2>Diagnostics file input</h2>
71
+ <p>Drag & drop a JSON file, or choose one manually.</p>
72
+ <input type="file" id="file-input" accept="application/json" />
73
+ <div class="actions">
74
+ <button id="download-json" ${d.rawJson?"":"disabled"}>Download loaded JSON</button>
75
+ <button id="copy-json" ${d.rawJson?"":"disabled"}>Copy loaded JSON</button>
76
+ <button id="copy-mermaid" ${o?"":"disabled"}>Copy Mermaid</button>
77
+ </div>
78
+ ${t?`<p class="notice">${p(t)}</p>`:""}
79
+ </section>
80
+
81
+ <section class="card">
82
+ <h2>Snapshot summary</h2>
83
+ ${oe(o)}
84
+ </section>
85
+
86
+ <section class="split-grid">
87
+ <div class="card">
88
+ <h2>Search and filtering</h2>
89
+ <label>
90
+ Search component/diagnostic
91
+ <input type="text" id="search" value="${p(d.filter.query)}" placeholder="e.g. redis.default or QUEUE_DEPENDENCY_NOT_READY" />
92
+ </label>
93
+
94
+ <div class="filter-row">
95
+ <span>Component readiness</span>
96
+ ${W.map(a=>`<label><input type="checkbox" id="readiness-${a}" data-readiness="${a}" ${d.filter.readinessStatuses.includes(a)?"checked":""}/> ${a}</label>`).join("")}
97
+ </div>
98
+
99
+ <div class="filter-row">
100
+ <span>Diagnostic severity</span>
101
+ ${Z.map(a=>`<label><input type="checkbox" id="severity-${a}" data-severity="${a}" ${d.filter.severities.includes(a)?"checked":""}/> ${a}</label>`).join("")}
102
+ </div>
103
+ </div>
104
+
105
+ <div class="card">
106
+ <h2>Timing</h2>
107
+ ${se()}
108
+ </div>
109
+ </section>
110
+
111
+ <section class="card">
112
+ <h2>Platform dependency graph</h2>
113
+ <p class="muted">Component dependencies are rendered directly from the shared platform snapshot schema.</p>
114
+ <div id="graph-host">${c}</div>
115
+ </section>
116
+
117
+ <section class="split-grid">
118
+ <div class="card" id="details-panel">
119
+ <h2>Component details</h2>
120
+ ${re(s)}
121
+ </div>
122
+ <div class="card">
123
+ <h2>Mermaid output</h2>
124
+ <pre>${p(r||"No snapshot loaded.")}</pre>
125
+ </div>
126
+ </section>
127
+
128
+ <section class="card">
129
+ <h2>Diagnostics issues</h2>
130
+ <p class="muted">Fix hints and dependency chains are rendered from <code>diagnostics.fixHint</code> and <code>diagnostics.dependsOn</code>.</p>
131
+ ${K(o)}
132
+ </section>
133
+ </main>
134
+ `;const l=document.querySelector("#file-input"),i=document.querySelector("#drop-zone"),m=document.querySelector("#search"),f=document.querySelector("#copy-json"),u=document.querySelector("#download-json"),v=document.querySelector("#copy-mermaid"),S=async a=>{const w=await a.text();try{const y=P(w);d.payload=y.payload,d.rawJson=y.rawJson,x(),g("Diagnostics file loaded successfully.")}catch(y){d.payload=void 0,d.filteredSnapshot=void 0,d.selectedComponentId=void 0,d.rawJson=void 0,g(y instanceof Error?y.message:"Failed to parse diagnostics file.")}};l==null||l.addEventListener("change",async a=>{var E;const y=(E=a.target.files)==null?void 0:E[0];y&&await S(y)}),i==null||i.addEventListener("dragover",a=>{a.preventDefault(),i.classList.add("drag-active")}),i==null||i.addEventListener("dragleave",()=>{i.classList.remove("drag-active")}),i==null||i.addEventListener("drop",async a=>{var y,E;a.preventDefault(),i.classList.remove("drag-active");const w=(E=(y=a.dataTransfer)==null?void 0:y.files)==null?void 0:E[0];w&&await S(w)}),m==null||m.addEventListener("input",a=>{const w=a.target;d.filter.query=w.value,x(),g({preserveFocus:!0})}),document.querySelectorAll("input[data-readiness]").forEach(a=>{a.addEventListener("change",()=>{d.filter.readinessStatuses=C(d.filter.readinessStatuses,a.dataset.readiness),x(),g({preserveFocus:!0})})}),document.querySelectorAll("input[data-severity]").forEach(a=>{a.addEventListener("change",()=>{d.filter.severities=C(d.filter.severities,a.dataset.severity),x(),g({preserveFocus:!0})})}),f==null||f.addEventListener("click",async()=>{if(d.rawJson)try{await N(d.rawJson),g("Loaded JSON copied to clipboard.")}catch(a){g(a instanceof Error?a.message:"Failed to copy JSON.")}}),u==null||u.addEventListener("click",()=>{d.rawJson&&(ee("fluo-diagnostics.json",d.rawJson),g("Loaded JSON downloaded."))}),v==null||v.addEventListener("click",async()=>{if(o)try{await N(I(o)),g("Mermaid copied to clipboard.")}catch(a){g(a instanceof Error?a.message:"Failed to copy Mermaid text.")}}),document.querySelectorAll("[data-component]").forEach(a=>{a.addEventListener("click",()=>{const w=a.dataset.component;w&&(d.selectedComponentId=w,g())})}),ne(n)}x();g();
@@ -1 +1 @@
1
- {"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,0BAA0B,EAC1B,uBAAuB,EACvB,qBAAqB,EACrB,gBAAgB,EACjB,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAEtF;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAAG,gBAAgB,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,CAAC;AAE9E;;GAEG;AACH,MAAM,MAAM,0BAA0B,GAAG,uBAAuB,CAAC,UAAU,CAAC,CAAC;AAE7E;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,qBAAqB,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC;IACxD,eAAe,EAAE,qBAAqB,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC9D,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,qBAAqB,CAAC;IAChC,OAAO,EAAE,mBAAmB,CAAC;IAC7B,MAAM,EAAE,0BAA0B,CAAC;IACnC,OAAO,EAAE,CAAC,CAAC;CACZ;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,QAAQ,CAAC,EAAE,qBAAqB,CAAC;IACjC,MAAM,CAAC,EAAE,0BAA0B,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,iBAAiB,EAAE,uBAAuB,EAAE,CAAC;IAC7C,UAAU,EAAE,0BAA0B,EAAE,CAAC;CAC1C;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,aAAa,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;CACjB;AAkLD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CA4BjE;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,qBAAqB,EAAE,MAAM,EAAE,WAAW,GAAG,qBAAqB,CAwCxG;AAyBD;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,qBAAqB,GAAG,MAAM,CAsErE"}
1
+ {"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,0BAA0B,EAC1B,uBAAuB,EACvB,qBAAqB,EACrB,gBAAgB,EACjB,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAEtF;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAAG,gBAAgB,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,CAAC;AAE9E;;GAEG;AACH,MAAM,MAAM,0BAA0B,GAAG,uBAAuB,CAAC,UAAU,CAAC,CAAC;AAE7E;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,qBAAqB,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC;IACxD,eAAe,EAAE,qBAAqB,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC9D,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,qBAAqB,CAAC;IAChC,OAAO,EAAE,mBAAmB,CAAC;IAC7B,MAAM,EAAE,0BAA0B,CAAC;IACnC,OAAO,EAAE,CAAC,CAAC;CACZ;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,QAAQ,CAAC,EAAE,qBAAqB,CAAC;IACjC,MAAM,CAAC,EAAE,0BAA0B,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,iBAAiB,EAAE,uBAAuB,EAAE,CAAC;IAC7C,UAAU,EAAE,0BAA0B,EAAE,CAAC;CAC1C;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,aAAa,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;CACjB;AAkLD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CA4BjE;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,qBAAqB,EAAE,MAAM,EAAE,WAAW,GAAG,qBAAqB,CAwCxG;AA8BD;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,qBAAqB,GAAG,MAAM,CAsErE"}
package/dist/contracts.js CHANGED
@@ -201,7 +201,7 @@ export function applyFilters(snapshot, filter) {
201
201
  };
202
202
  }
203
203
  function escapeMermaidText(value) {
204
- return value.replaceAll('"', '\\"');
204
+ return value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('\r\n', '\\n').replaceAll('\r', '\\n').replaceAll('\n', '\\n');
205
205
  }
206
206
  function sanitizeMermaidNodeIdSegment(value) {
207
207
  return value.replaceAll(/[^a-zA-Z0-9_]/g, '_');
package/dist/index.html CHANGED
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Fluo Studio</title>
7
- <script type="module" crossorigin src="/assets/index-B_CD1DMS.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-BTqPuJus.css">
7
+ <script type="module" crossorigin src="/assets/index-DbteVtYa.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-CQ_tQzq9.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="app"></div>
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "module-graph",
10
10
  "devtools"
11
11
  ],
12
- "version": "1.0.0",
12
+ "version": "1.0.2",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -41,7 +41,7 @@
41
41
  "dist"
42
42
  ],
43
43
  "dependencies": {
44
- "@fluojs/runtime": "^1.0.0"
44
+ "@fluojs/runtime": "^1.1.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "happy-dom": "^20.9.0",
@@ -1,128 +0,0 @@
1
- (function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))d(s);new MutationObserver(s=>{for(const o of s)if(o.type==="childList")for(const l of o.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&d(l)}).observe(document,{childList:!0,subtree:!0});function n(s){const o={};return s.integrity&&(o.integrity=s.integrity),s.referrerPolicy&&(o.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?o.credentials="include":s.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function d(s){if(s.ep)return;s.ep=!0;const o=n(s);fetch(s.href,o)}})();function f(e){return typeof e=="object"&&e!==null}function S(e,t){return Object.hasOwn(e,t)}function A(e){return Array.isArray(e)&&e.every(t=>typeof t=="string")}function L(e){return e==="ready"||e==="not-ready"||e==="degraded"}function O(e){return e==="healthy"||e==="unhealthy"||e==="degraded"}function k(e){return e==="error"||e==="warning"||e==="info"}function D(e){if(!f(e))return null;if(typeof e.generatedAt!="string"||!f(e.readiness)||!f(e.health)||!Array.isArray(e.components)||!Array.isArray(e.diagnostics))throw new Error("Invalid platform snapshot payload.");if(!L(e.readiness.status)||typeof e.readiness.critical!="boolean")throw new Error("Invalid aggregate readiness in platform snapshot payload.");if(!O(e.health.status))throw new Error("Invalid aggregate health in platform snapshot payload.");for(const t of e.components){if(!f(t))throw new Error("Invalid component entry in platform snapshot payload.");if(typeof t.id!="string"||typeof t.kind!="string"||typeof t.state!="string"||!f(t.readiness)||!f(t.health)||!A(t.dependencies)||!f(t.telemetry)||!f(t.ownership)||!f(t.details))throw new Error("Invalid component shape in platform snapshot payload.");if(!L(t.readiness.status)||typeof t.readiness.critical!="boolean")throw new Error("Invalid component readiness in platform snapshot payload.");if(!O(t.health.status))throw new Error("Invalid component health in platform snapshot payload.");if(typeof t.telemetry.namespace!="string"||!f(t.telemetry.tags)||typeof t.ownership.ownsResources!="boolean"||typeof t.ownership.externallyManaged!="boolean")throw new Error("Invalid component telemetry/ownership in platform snapshot payload.")}for(const t of e.diagnostics){if(!f(t))throw new Error("Invalid diagnostics issue entry in platform snapshot payload.");if(typeof t.code!="string"||!k(t.severity)||typeof t.componentId!="string"||typeof t.message!="string")throw new Error("Invalid diagnostics issue shape in platform snapshot payload.");if(t.cause!==void 0&&typeof t.cause!="string"||t.fixHint!==void 0&&typeof t.fixHint!="string"||t.docsUrl!==void 0&&typeof t.docsUrl!="string"||t.dependsOn!==void 0&&!A(t.dependsOn))throw new Error("Invalid optional diagnostics issue fields in platform snapshot payload.")}return e}function F(e){if(!f(e))return null;if(e.version!==1)throw new Error("Unsupported bootstrap timing version. Expected version: 1.");if(typeof e.totalMs!="number"||!Array.isArray(e.phases))throw new Error("Invalid bootstrap timing payload.");for(const t of e.phases)if(!f(t)||typeof t.name!="string"||typeof t.durationMs!="number")throw new Error("Invalid phase entry in bootstrap timing payload.");return e}function J(e){if(!f(e))throw new Error("Invalid inspect report summary payload.");if(typeof e.componentCount!="number"||typeof e.diagnosticCount!="number"||typeof e.errorCount!="number"||!O(e.healthStatus)||!L(e.readinessStatus)||typeof e.timingTotalMs!="number"||typeof e.warningCount!="number")throw new Error("Invalid inspect report summary payload.");return e}function R(e){return e.summary!==void 0||S(e,"snapshot")&&S(e,"timing")&&(S(e,"generatedAt")||S(e,"version"))}function q(e,t,n){if(!f(e)||!R(e))return null;if(e.summary===void 0||e.version!==1||typeof e.generatedAt!="string"||!t||!n)throw new Error("Invalid inspect report artifact payload.");return{generatedAt:e.generatedAt,snapshot:t,summary:J(e.summary),timing:n,version:1}}function U(e){const t=JSON.parse(e),n=f(t)?t:void 0,d=n!==void 0&&S(n,"snapshot"),s=n!==void 0&&S(n,"timing"),o=n!==void 0&&!d&&!s&&S(n,"version")&&S(n,"totalMs")&&S(n,"phases"),l=D(d?n.snapshot:o?void 0:t),c=F(s?n.timing:l?void 0:t),a=q(t,l,c);if(!l&&!c)throw new Error("Unsupported file format. Expected platform snapshot JSON or timing JSON.");return{payload:{...a?{report:a}:{},...l?{snapshot:l}:{},...c?{timing:c}:{}},rawJson:e}}function P(e,t){const n=t.query.trim().toLowerCase(),d=e.components.filter(o=>t.readinessStatuses.length>0&&!t.readinessStatuses.includes(o.readiness.status)?!1:n?o.id.toLowerCase().includes(n)||o.kind.toLowerCase().includes(n)||o.dependencies.some(l=>l.toLowerCase().includes(n)):!0),s=e.diagnostics.filter(o=>{var l,c,a,y;return t.severities.length>0&&!t.severities.includes(o.severity)?!1:n?o.code.toLowerCase().includes(n)||o.componentId.toLowerCase().includes(n)||o.message.toLowerCase().includes(n)||(((l=o.cause)==null?void 0:l.toLowerCase().includes(n))??!1)||(((c=o.fixHint)==null?void 0:c.toLowerCase().includes(n))??!1)||(((a=o.docsUrl)==null?void 0:a.toLowerCase().includes(n))??!1)||(((y=o.dependsOn)==null?void 0:y.some(h=>h.toLowerCase().includes(n)))??!1):!0});return{...e,components:d,diagnostics:s}}function x(e){return e.replaceAll('"','\\"')}function H(e){return e.replaceAll(/[^a-zA-Z0-9_]/g,"_")}function _(e){let t=2166136261;for(const n of e)t^=n.codePointAt(0)??0,t=Math.imul(t,16777619)>>>0;return t.toString(16).padStart(8,"0")}function z(e){return`EXT_${H(e)}_${_(e)}`}function C(e){const t=["graph TD"],n=new Map,d=new Map;if(e.components.length===0)return t.push(' EMPTY["No registered platform components"]'),t.join(`
2
- `);for(const[l,c]of e.components.entries()){const a=`C${String(l+1)}`;n.set(c.id,a),t.push(` ${a}["${x(c.id)}\\nkind: ${x(c.kind)}\\nreadiness: ${c.readiness.status}\\nhealth: ${c.health.status}"]`)}for(const l of e.components){const c=n.get(l.id);if(c)for(const a of l.dependencies){const y=n.get(a);if(y){t.push(` ${c} --> ${y}`);continue}let h=d.get(a);h||(h=z(a),d.set(a,h),t.push(` ${h}["${x(a)}"]`)),t.push(` ${c} --> ${h}`)}}const s=[],o=[];for(const l of e.components){const c=n.get(l.id);c&&(l.readiness.status==="degraded"&&s.push(c),l.readiness.status==="not-ready"&&o.push(c))}return s.length>0&&(t.push(` class ${s.join(",")} degraded`),t.push(" classDef degraded stroke:#f59e0b,stroke-width:2px")),o.length>0&&(t.push(` class ${o.join(",")} notReady`),t.push(" classDef notReady stroke:#ef4444,stroke-width:2px")),t.join(`
3
- `)}const M=document.querySelector("#app");if(!M)throw new Error("App root not found.");const Y=M,B=["ready","degraded","not-ready"],X=["error","warning","info"],i={filter:{query:"",readinessStatuses:[],severities:[]}};function p(e){return e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&#039;")}function V(e,t){const n=new Blob([t],{type:"application/json"}),d=URL.createObjectURL(n),s=document.createElement("a");s.href=d,s.download=e,s.click(),URL.revokeObjectURL(d)}async function I(e){if(!navigator.clipboard)throw new Error("Clipboard API is unavailable.");await navigator.clipboard.writeText(e)}function N(e,t){return e.includes(t)?e.filter(n=>n!==t):[...e,t]}function j(e){return e instanceof HTMLInputElement||e instanceof HTMLTextAreaElement}function G(){const e=document.activeElement;if(!(!j(e)||!e.id))return{id:e.id,selectionEnd:e.selectionEnd,selectionStart:e.selectionStart}}function K(e){if(!e)return;const t=document.getElementById(e.id);j(t)&&(t.focus({preventScroll:!0}),e.selectionStart!==null&&e.selectionEnd!==null&&t.setSelectionRange(e.selectionStart,e.selectionEnd))}function T(e,t){return!e||e.components.length===0?void 0:(t?e.components.find(d=>d.id===t):void 0)??e.components[0]}function b(){var n;const e=(n=i.payload)==null?void 0:n.snapshot;if(!e){i.filteredSnapshot=void 0,i.selectedComponentId=void 0;return}i.filteredSnapshot=P(e,i.filter);const t=T(i.filteredSnapshot,i.selectedComponentId);i.selectedComponentId=t==null?void 0:t.id}function Q(e,t){const s=Math.min(900,460)/2-70,o=900/2,l=460/2,c=e.components,a=new Map;c.forEach((u,m)=>{const $=Math.PI*2*m/Math.max(c.length,1);a.set(u.id,{x:o+s*Math.cos($),y:l+s*Math.sin($)})});const y=c.flatMap(u=>u.dependencies.map(m=>{const $=a.get(u.id),r=a.get(m);return!$||!r?"":`<line x1="${$.x}" y1="${$.y}" x2="${r.x}" y2="${r.y}" class="edge-line" marker-end="url(#arrow)" />`})).join(""),h=c.map(u=>{const m=a.get(u.id);if(!m)return"";const r=["module-node",u.readiness.status==="not-ready"?"component-not-ready":u.readiness.status==="degraded"?"component-degraded":"component-ready",u.id===t?"module-selected":""].filter(Boolean).join(" ");return`<g>
4
- <circle cx="${m.x}" cy="${m.y}" r="34" class="${r}" data-component="${p(u.id)}" />
5
- <text x="${m.x}" y="${m.y+4}" text-anchor="middle" class="module-label">${p(u.id)}</text>
6
- </g>`}).join("");return`<svg viewBox="0 0 900 460" role="img" aria-label="Platform component dependency graph">
7
- <defs>
8
- <marker id="arrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
9
- <polygon points="0 0, 10 3.5, 0 7" class="edge-arrow" />
10
- </marker>
11
- </defs>
12
- ${y}
13
- ${h}
14
- </svg>`}function W(e){if(!e)return'<p class="muted">No component selected.</p>';const t=e.dependencies.length>0?e.dependencies.map(n=>`<span class="chip">dependsOn: ${p(n)}</span>`).join(""):'<span class="chip">dependsOn: none</span>';return`
15
- <h3>${p(e.id)}</h3>
16
- <p class="muted">kind: <strong>${p(e.kind)}</strong> · state: <strong>${p(e.state)}</strong></p>
17
- <div class="chips">
18
- <span class="chip">readiness: ${p(e.readiness.status)}</span>
19
- <span class="chip">critical: ${e.readiness.critical?"true":"false"}</span>
20
- <span class="chip">health: ${p(e.health.status)}</span>
21
- <span class="chip">ownership: owns=${e.ownership.ownsResources?"true":"false"}/external=${e.ownership.externallyManaged?"true":"false"}</span>
22
- ${t}
23
- </div>
24
- <p class="muted">telemetry namespace: <code>${p(e.telemetry.namespace)}</code></p>
25
- <h4>Sanitized details</h4>
26
- <pre>${p(JSON.stringify(e.details,null,2))}</pre>
27
- `}function Z(){var t;const e=(t=i.payload)==null?void 0:t.timing;return e?`
28
- <p><strong>Total:</strong> ${e.totalMs.toFixed(3)}ms</p>
29
- <table>
30
- <thead>
31
- <tr><th>phase</th><th>duration (ms)</th></tr>
32
- </thead>
33
- <tbody>
34
- ${e.phases.map(n=>`<tr><td>${p(n.name)}</td><td>${n.durationMs.toFixed(3)}</td></tr>`).join("")}
35
- </tbody>
36
- </table>
37
- `:'<p class="muted">Timing not collected.</p>'}function ee(e){if(!e)return'<p class="muted">No platform snapshot loaded.</p>';const t={degraded:e.components.filter(n=>n.readiness.status==="degraded").length,notReady:e.components.filter(n=>n.readiness.status==="not-ready").length,ready:e.components.filter(n=>n.readiness.status==="ready").length};return`
38
- <div class="chips">
39
- <span class="chip">generatedAt: ${p(e.generatedAt)}</span>
40
- <span class="chip">aggregate readiness: ${p(e.readiness.status)}</span>
41
- <span class="chip">aggregate health: ${p(e.health.status)}</span>
42
- <span class="chip">components: ${String(e.components.length)}</span>
43
- <span class="chip">diagnostics: ${String(e.diagnostics.length)}</span>
44
- <span class="chip">ready/degraded/not-ready: ${t.ready}/${t.degraded}/${t.notReady}</span>
45
- </div>
46
- `}function te(e){return e?e.diagnostics.length===0?'<p class="muted">No diagnostics issues.</p>':`<div class="diagnostics-list">
47
- ${e.diagnostics.map(t=>{const n=t.dependsOn&&t.dependsOn.length>0?`<div class="chips">${t.dependsOn.map(d=>`<span class="chip">dependsOn: ${p(d)}</span>`).join("")}</div>`:"";return`<article class="card issue severity-${p(t.severity)}">
48
- <h3>${p(t.code)}</h3>
49
- <p><strong>severity:</strong> ${p(t.severity)} · <strong>component:</strong> ${p(t.componentId)}</p>
50
- <p>${p(t.message)}</p>
51
- ${t.cause?`<p><strong>cause:</strong> ${p(t.cause)}</p>`:""}
52
- ${t.fixHint?`<p><strong>fix hint:</strong> ${p(t.fixHint)}</p>`:""}
53
- ${t.docsUrl?`<p><strong>docs:</strong> <a href="${p(t.docsUrl)}" target="_blank" rel="noreferrer">${p(t.docsUrl)}</a></p>`:""}
54
- ${n}
55
- </article>`}).join("")}
56
- </div>`:'<p class="muted">No platform snapshot loaded.</p>'}function g(e={}){const t=typeof e=="string"?e:e.message,n=typeof e=="string"||!e.preserveFocus?void 0:G(),d=i.filteredSnapshot,s=T(d,i.selectedComponentId),o=d?C(d):"",l=d&&d.components.length>0?Q(d,s==null?void 0:s.id):'<p class="muted">No platform components loaded.</p>';Y.innerHTML=`
57
- <main>
58
- <header>
59
- <h1>Fluo Studio Platform Snapshot Viewer</h1>
60
- <p>Load JSON exported by <code>fluo inspect --json</code> (shared platform snapshot/diagnostic schema) and optionally timing JSON from <code>--timing</code>.</p>
61
- </header>
62
-
63
- <section class="card uploader" id="drop-zone">
64
- <h2>Diagnostics file input</h2>
65
- <p>Drag & drop a JSON file, or choose one manually.</p>
66
- <input type="file" id="file-input" accept="application/json" />
67
- <div class="actions">
68
- <button id="download-json" ${i.rawJson?"":"disabled"}>Download loaded JSON</button>
69
- <button id="copy-json" ${i.rawJson?"":"disabled"}>Copy loaded JSON</button>
70
- <button id="copy-mermaid" ${d?"":"disabled"}>Copy Mermaid</button>
71
- </div>
72
- ${t?`<p class="notice">${p(t)}</p>`:""}
73
- </section>
74
-
75
- <section class="card">
76
- <h2>Snapshot summary</h2>
77
- ${ee(d)}
78
- </section>
79
-
80
- <section class="split-grid">
81
- <div class="card">
82
- <h2>Search and filtering</h2>
83
- <label>
84
- Search component/diagnostic
85
- <input type="text" id="search" value="${p(i.filter.query)}" placeholder="e.g. redis.default or QUEUE_DEPENDENCY_NOT_READY" />
86
- </label>
87
-
88
- <div class="filter-row">
89
- <span>Component readiness</span>
90
- ${B.map(r=>`<label><input type="checkbox" id="readiness-${r}" data-readiness="${r}" ${i.filter.readinessStatuses.includes(r)?"checked":""}/> ${r}</label>`).join("")}
91
- </div>
92
-
93
- <div class="filter-row">
94
- <span>Diagnostic severity</span>
95
- ${X.map(r=>`<label><input type="checkbox" id="severity-${r}" data-severity="${r}" ${i.filter.severities.includes(r)?"checked":""}/> ${r}</label>`).join("")}
96
- </div>
97
- </div>
98
-
99
- <div class="card">
100
- <h2>Timing</h2>
101
- ${Z()}
102
- </div>
103
- </section>
104
-
105
- <section class="card">
106
- <h2>Platform dependency graph</h2>
107
- <p class="muted">Component dependencies are rendered directly from the shared platform snapshot schema.</p>
108
- <div id="graph-host">${l}</div>
109
- </section>
110
-
111
- <section class="split-grid">
112
- <div class="card" id="details-panel">
113
- <h2>Component details</h2>
114
- ${W(s)}
115
- </div>
116
- <div class="card">
117
- <h2>Mermaid output</h2>
118
- <pre>${p(o||"No snapshot loaded.")}</pre>
119
- </div>
120
- </section>
121
-
122
- <section class="card">
123
- <h2>Diagnostics issues</h2>
124
- <p class="muted">Fix hints and dependency chains are rendered from <code>diagnostics.fixHint</code> and <code>diagnostics.dependsOn</code>.</p>
125
- ${te(d)}
126
- </section>
127
- </main>
128
- `;const c=document.querySelector("#file-input"),a=document.querySelector("#drop-zone"),y=document.querySelector("#search"),h=document.querySelector("#copy-json"),u=document.querySelector("#download-json"),m=document.querySelector("#copy-mermaid"),$=async r=>{const v=await r.text();try{const w=U(v);i.payload=w.payload,i.rawJson=w.rawJson,b(),g("Diagnostics file loaded successfully.")}catch(w){i.payload=void 0,i.filteredSnapshot=void 0,i.selectedComponentId=void 0,i.rawJson=void 0,g(w instanceof Error?w.message:"Failed to parse diagnostics file.")}};c==null||c.addEventListener("change",async r=>{var E;const w=(E=r.target.files)==null?void 0:E[0];w&&await $(w)}),a==null||a.addEventListener("dragover",r=>{r.preventDefault(),a.classList.add("drag-active")}),a==null||a.addEventListener("dragleave",()=>{a.classList.remove("drag-active")}),a==null||a.addEventListener("drop",async r=>{var w,E;r.preventDefault(),a.classList.remove("drag-active");const v=(E=(w=r.dataTransfer)==null?void 0:w.files)==null?void 0:E[0];v&&await $(v)}),y==null||y.addEventListener("input",r=>{const v=r.target;i.filter.query=v.value,b(),g({preserveFocus:!0})}),document.querySelectorAll("input[data-readiness]").forEach(r=>{r.addEventListener("change",()=>{i.filter.readinessStatuses=N(i.filter.readinessStatuses,r.dataset.readiness),b(),g({preserveFocus:!0})})}),document.querySelectorAll("input[data-severity]").forEach(r=>{r.addEventListener("change",()=>{i.filter.severities=N(i.filter.severities,r.dataset.severity),b(),g({preserveFocus:!0})})}),h==null||h.addEventListener("click",async()=>{if(i.rawJson)try{await I(i.rawJson),g("Loaded JSON copied to clipboard.")}catch(r){g(r instanceof Error?r.message:"Failed to copy JSON.")}}),u==null||u.addEventListener("click",()=>{i.rawJson&&(V("fluo-diagnostics.json",i.rawJson),g("Loaded JSON downloaded."))}),m==null||m.addEventListener("click",async()=>{if(d)try{await I(C(d)),g("Mermaid copied to clipboard.")}catch(r){g(r instanceof Error?r.message:"Failed to copy Mermaid text.")}}),document.querySelectorAll("[data-component]").forEach(r=>{r.addEventListener("click",()=>{const v=r.dataset.component;v&&(i.selectedComponentId=v,g())})}),K(n)}b();g();