diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2bef310..b7a1e96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,8 +11,6 @@ jobs: steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: - version: 9 - uses: actions/setup-node@v4 with: node-version: 22 diff --git a/.gitignore b/.gitignore index 16d2e27..4c52698 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ node_modules/ # Build output dist/ *.tsbuildinfo +packages/ink/public/dashboard/ +packages/dashboard/vite.config.js +packages/dashboard/vite.config.d.ts # Test & coverage coverage/ diff --git a/.oxlintrc.json b/.oxlintrc.json index c3895f3..c798449 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -7,8 +7,9 @@ }, "rules": { "curly": "error", - "typescript/no-explicit-any": "error", - "typescript/no-unsafe-type-assertion": "off" + "typescript/no-explicit-any": "warn", + "typescript/no-unsafe-type-assertion": "off", + "no-await-in-loop": "warn" }, "ignorePatterns": [ "dist/", diff --git a/knip.config.ts b/knip.config.ts index 90ece5a..23ef371 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -57,6 +57,10 @@ const config: KnipConfig = { entry: ["src/index.ts"], ignoreDependencies: ["@openoctopus/shared", "@openoctopus/core"], }, + "packages/dashboard": { + entry: ["src/main.tsx"], + ignoreDependencies: ["@openoctopus/shared", "autoprefixer", "postcss"], + }, }, ignore: [ "**/*.test.ts", diff --git a/package.json b/package.json index 6104e1d..769432b 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,8 @@ "start": "tsx packages/tentacle/src/cli.ts start", "chat": "tsx packages/tentacle/src/cli.ts chat", "restart": "pnpm build && tsx packages/tentacle/src/cli.ts start", + "dashboard:dev": "pnpm --filter @openoctopus/dashboard dev", + "dashboard:build": "pnpm --filter @openoctopus/dashboard build", "clean": "pnpm -r exec rm -rf dist", "doctor": "pnpm check && pnpm test:unit" }, diff --git a/packages/core/src/cross-realm-reactor.ts b/packages/core/src/cross-realm-reactor.ts index e706e59..8d205b0 100644 --- a/packages/core/src/cross-realm-reactor.ts +++ b/packages/core/src/cross-realm-reactor.ts @@ -46,7 +46,7 @@ export class CrossRealmReactor { const { sourceRealmId, userMessage, assistantResponse, onReaction } = params; const activeSummoned = this.summonEngine.listActive(); - if (activeSummoned.length === 0) return; + if (activeSummoned.length === 0) {return;} const realms = this.realmManager.list(); const combinedText = `${userMessage} ${assistantResponse}`; @@ -56,10 +56,10 @@ export class CrossRealmReactor { for (const summoned of activeSummoned) { // Skip agents in the source realm - if (summoned.entity.realmId === sourceRealmId) continue; + if (summoned.entity.realmId === sourceRealmId) {continue;} const targetRealm = realms.find(r => r.id === summoned.entity.realmId); - if (!targetRealm) continue; + if (!targetRealm) {continue;} const score = this.computeRelevanceScore(combinedText, targetRealm); if (score > 0 && (!bestMatch || score > bestMatch.score)) { @@ -72,7 +72,7 @@ export class CrossRealmReactor { } } - if (!bestMatch) return; + if (!bestMatch) {return;} // Generate reaction from the best-matching agent try { @@ -108,7 +108,7 @@ export class CrossRealmReactor { let score = 0; for (const kw of keywords) { - if (lowered.includes(kw)) score++; + if (lowered.includes(kw)) {score++;} } return score; @@ -121,7 +121,7 @@ export class CrossRealmReactor { sourceRealmId: string, conversationSummary: string, ): Promise { - if (!this.llmRegistry.hasRealProvider()) return null; + if (!this.llmRegistry.hasRealProvider()) {return null;} try { const provider = this.llmRegistry.getProvider(); @@ -143,7 +143,7 @@ If nothing truly relevant from your domain's perspective, respond with exactly " }); const content = result.content.trim(); - if (content === "SKIP" || content.length < 5) return null; + if (content === "SKIP" || content.length < 5) {return null;} return content; } catch (err) { diff --git a/packages/core/src/directory-scanner.test.ts b/packages/core/src/directory-scanner.test.ts index c21fa24..169dcc6 100644 --- a/packages/core/src/directory-scanner.test.ts +++ b/packages/core/src/directory-scanner.test.ts @@ -160,7 +160,7 @@ describe("DirectoryScanner", () => { it("should not call distributeFromText in dryRun mode", async () => { fs.writeFileSync(path.join(tmpDir, "notes.md"), "some content"); - const result = await scanner.scanDirectory(tmpDir, { dryRun: true }); + await scanner.scanDirectory(tmpDir, { dryRun: true }); expect(mockKnowledgeDistributor.distributeFromText).not.toHaveBeenCalled(); }); diff --git a/packages/core/src/directory-scanner.ts b/packages/core/src/directory-scanner.ts index 4549c79..e27faf8 100644 --- a/packages/core/src/directory-scanner.ts +++ b/packages/core/src/directory-scanner.ts @@ -178,22 +178,22 @@ export class DirectoryScanner { resolvedPath, { recursive: true, persistent: false }, (eventType, filename) => { - if (!filename || closed) return; + if (!filename || closed) {return;} // Check extension const ext = path.extname(filename).toLowerCase(); - if (!extensions.includes(ext)) return; + if (!extensions.includes(ext)) {return;} const filePath = path.join(resolvedPath, filename); // Debounce: clear any pending scan for this file const existing = pendingScans.get(filePath); - if (existing) clearTimeout(existing); + if (existing) {clearTimeout(existing);} // Schedule new scan const timer = setTimeout(() => { pendingScans.delete(filePath); - if (closed) return; + if (closed) {return;} // Scan the file (fire-and-forget) this.scanFile(filePath).catch((err) => { @@ -239,9 +239,9 @@ export class DirectoryScanner { for (const entry of entries) { // Skip hidden files/dirs - if (entry.name.startsWith(".")) continue; + if (entry.name.startsWith(".")) {continue;} // Skip node_modules, etc. - if (entry.name === "node_modules" || entry.name === "__pycache__") continue; + if (entry.name === "node_modules" || entry.name === "__pycache__") {continue;} const fullPath = path.join(dirPath, entry.name); diff --git a/packages/core/src/embedding/embedding-registry.ts b/packages/core/src/embedding/embedding-registry.ts index 5fa5d71..589f3c6 100644 --- a/packages/core/src/embedding/embedding-registry.ts +++ b/packages/core/src/embedding/embedding-registry.ts @@ -80,7 +80,7 @@ export class EmbeddingProviderRegistry { // Try exact match const provider = this.providers.get(providerName); - if (provider) return provider; + if (provider) {return provider;} // Try first configured provider if (this.providerOrder.length > 0) { diff --git a/packages/core/src/knowledge-distributor.ts b/packages/core/src/knowledge-distributor.ts index 889e1bb..f963d79 100644 --- a/packages/core/src/knowledge-distributor.ts +++ b/packages/core/src/knowledge-distributor.ts @@ -151,7 +151,7 @@ export class KnowledgeDistributor { const classification = await this.classifyToRealm(fact, realms); // Skip if the fact belongs to the source realm (already handled) - if (!classification || classification.realmId === sourceRealmId) continue; + if (!classification || classification.realmId === sourceRealmId) {continue;} this.memoryRepo.create({ realmId: classification.realmId, @@ -224,7 +224,7 @@ Write facts in the same language as the input.`, .filter(item => item.fact && item.realm) .map((item): ExtractedFact | null => { const matchedRealm = realms.find(r => r.name.toLowerCase() === item.realm.toLowerCase()); - if (!matchedRealm) return null; + if (!matchedRealm) {return null;} return { content: item.fact, realmId: matchedRealm.id, @@ -252,7 +252,7 @@ Write facts in the same language as the input.`, const keywords = REALM_KEYWORDS[realm.name.toLowerCase()] ?? []; let score = 0; for (const kw of keywords) { - if (lowered.includes(kw)) score++; + if (lowered.includes(kw)) {score++;} } if (score > bestScore) { bestScore = score; @@ -288,7 +288,7 @@ Write facts in the same language as the input.`, const keywords = REALM_KEYWORDS[realm.name.toLowerCase()] ?? []; let score = 0; for (const kw of keywords) { - if (lowered.includes(kw)) score++; + if (lowered.includes(kw)) {score++;} } if (score > bestScore) { bestScore = score; @@ -304,11 +304,11 @@ Write facts in the same language as the input.`, } private async createMissingEntity(fact: ExtractedFact): Promise { - if (!fact.entityName) return; + if (!fact.entityName) {return;} try { const existing = this.entityManager.findByNameInRealm(fact.realmId, fact.entityName); - if (existing) return; + if (existing) {return;} const validTypes = ["living", "asset", "organization", "abstract"] as const; const entityType = validTypes.includes(fact.entityType as typeof validTypes[number]) diff --git a/packages/core/src/maturity-scanner.test.ts b/packages/core/src/maturity-scanner.test.ts index 12c53ef..78eb2b0 100644 --- a/packages/core/src/maturity-scanner.test.ts +++ b/packages/core/src/maturity-scanner.test.ts @@ -269,11 +269,11 @@ describe("MaturityScanner", () => { { id: "r2", name: "finance" }, ]); mockEntityManager.listByRealm.mockImplementation((realmId: string) => { - if (realmId === "r1") return [{ id: "e1", name: "Luna", realmId: "r1", attributes: { a: 1, b: 2, c: 3 }, summonStatus: "dormant" }]; + if (realmId === "r1") {return [{ id: "e1", name: "Luna", realmId: "r1", attributes: { a: 1, b: 2, c: 3 }, summonStatus: "dormant" }];} return [{ id: "e2", name: "Account", realmId: "r2", attributes: { x: 1 }, summonStatus: "dormant" }]; }); mockEntityManager.get.mockImplementation((id: string) => { - if (id === "e1") return { id: "e1", name: "Luna", realmId: "r1", attributes: { a: 1, b: 2, c: 3 }, summonStatus: "dormant" }; + if (id === "e1") {return { id: "e1", name: "Luna", realmId: "r1", attributes: { a: 1, b: 2, c: 3 }, summonStatus: "dormant" };} return { id: "e2", name: "Account", realmId: "r2", attributes: { x: 1 }, summonStatus: "dormant" }; }); mockRealmManager.get.mockImplementation((id: string) => ({ id, name: id === "r1" ? "pet" : "finance" })); diff --git a/packages/core/src/maturity-scanner.ts b/packages/core/src/maturity-scanner.ts index 7a0f2ce..ecdc278 100644 --- a/packages/core/src/maturity-scanner.ts +++ b/packages/core/src/maturity-scanner.ts @@ -36,7 +36,7 @@ export class MaturityScanner { computeEntityMaturity(entityId: string): MaturityScore { const entity = this.entityManager.get(entityId); - const realm = this.realmManager.get(entity.realmId); + const _realm = this.realmManager.get(entity.realmId); // Attribute completeness (30%): non-empty attributes / total defined attributes const attrs = entity.attributes; diff --git a/packages/core/src/memory-extractor.test.ts b/packages/core/src/memory-extractor.test.ts index 9deaa5d..b47cee6 100644 --- a/packages/core/src/memory-extractor.test.ts +++ b/packages/core/src/memory-extractor.test.ts @@ -34,7 +34,7 @@ const mockEmbeddingRegistry = { /** Helper: create a unit vector pointing along a single dimension */ function basisVector(dim: number, size = 128): number[] { - const v = new Array(size).fill(0); + const v = Array.from({ length: size }, () => 0); v[dim] = 1; return v; } @@ -42,7 +42,7 @@ function basisVector(dim: number, size = 128): number[] { /** Helper: create a vector that is `similarity` cosine-similar to a basis vector at dim */ function vectorWithSimilarity(baseDim: number, similarity: number, size = 128): number[] { // Start with the basis vector scaled by similarity, add orthogonal component - const v = new Array(size).fill(0); + const v = Array.from({ length: size }, () => 0); v[baseDim] = similarity; // Add orthogonal component in the next dimension to make it a unit vector const orthDim = (baseDim + 1) % size; diff --git a/packages/core/src/memory-extractor.ts b/packages/core/src/memory-extractor.ts index 8d306f1..1c46bae 100644 --- a/packages/core/src/memory-extractor.ts +++ b/packages/core/src/memory-extractor.ts @@ -86,7 +86,7 @@ export class MemoryExtractor { try { const extraction = await this.extractFacts(params.userMessage, params.assistantMessage); const { facts, importance, relations } = extraction; - if (facts.length === 0) return { memories: [], attributeUpdates: [] }; + if (facts.length === 0) {return { memories: [], attributeUpdates: [] };} const entries: MemoryEntry[] = []; for (let i = 0; i < facts.length; i++) { @@ -296,7 +296,7 @@ export class MemoryExtractor { relations: ExtractionResult["relations"], realmId: string, ): Promise { - if (!this.knowledgeGraphRepo || relations.length === 0) return; + if (!this.knowledgeGraphRepo || relations.length === 0) {return;} for (const rel of relations) { try { diff --git a/packages/core/src/memory-health-manager.ts b/packages/core/src/memory-health-manager.ts index 59009a2..78079b0 100644 --- a/packages/core/src/memory-health-manager.ts +++ b/packages/core/src/memory-health-manager.ts @@ -117,7 +117,6 @@ export class MemoryHealthManager { for (const issue of dups) { if (issue.memoryIds.length >= 2) { // Keep first, delete rest - const toDelete = issue.memoryIds.slice(1); deduplicatedCount += await this.deduplicate(realmId, [issue.memoryIds]); } } @@ -337,7 +336,7 @@ If no contradictions found, output [].`, function normalizedLevenshtein(a: string, b: string): number { if (a === b) { return 0; } const maxLen = Math.max(a.length, b.length); - if (maxLen === 0) return 0; + if (maxLen === 0) {return 0;} // For performance, skip very long strings if (maxLen > 500) { @@ -346,7 +345,7 @@ function normalizedLevenshtein(a: string, b: string): number { const setB = new Set(b.toLowerCase().split(/\s+/)); let overlap = 0; for (const w of setA) { - if (setB.has(w)) overlap++; + if (setB.has(w)) {overlap++;} } const union = new Set([...setA, ...setB]).size; return union > 0 ? 1 - overlap / union : 1; @@ -356,8 +355,8 @@ function normalizedLevenshtein(a: string, b: string): number { const n = b.length; const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0) as number[]); - for (let i = 0; i <= m; i++) dp[i][0] = i; - for (let j = 0; j <= n; j++) dp[0][j] = j; + for (let i = 0; i <= m; i++) {dp[i][0] = i;} + for (let j = 0; j <= n; j++) {dp[0][j] = j;} for (let i = 1; i <= m; i++) { for (let j = 1; j <= n; j++) { diff --git a/packages/core/src/scheduler.ts b/packages/core/src/scheduler.ts index 6e5eea7..41f3bec 100644 --- a/packages/core/src/scheduler.ts +++ b/packages/core/src/scheduler.ts @@ -25,7 +25,7 @@ const TRIGGER_MAP: Record = { /** Simple cron expression validator (5-field format) */ const CRON_REGEX = - /^(\*|[0-9,\-\/]+)\s+(\*|[0-9,\-\/]+)\s+(\*|[0-9,\-\/]+)\s+(\*|[0-9,\-\/]+)\s+(\*|[0-9,\-\/]+)$/; + /^(\*|[0-9,\-/]+)\s+(\*|[0-9,\-/]+)\s+(\*|[0-9,\-/]+)\s+(\*|[0-9,\-/]+)\s+(\*|[0-9,\-/]+)$/; /** Match "every day Xam" or "every day Xpm" */ const TIME_REGEX = /^every\s+day\s+(\d{1,2})(am|pm)$/i; @@ -33,18 +33,18 @@ const TIME_REGEX = /^every\s+day\s+(\d{1,2})(am|pm)$/i; /** Convert cron expression to milliseconds interval (MVP approximation) */ function cronToInterval(cron: string): number { const parts = cron.split(/\s+/); - if (parts.length !== 5) return 24 * 60 * 60 * 1000; // default: daily + if (parts.length !== 5) {return 24 * 60 * 60 * 1000;} // default: daily const [_minute, hour, dayOfMonth, , dayOfWeek] = parts; // "0 * * * *" → hourly - if (hour === "*") return 60 * 60 * 1000; + if (hour === "*") {return 60 * 60 * 1000;} // "0 9 * * 1" → weekly (day of week specified) - if (dayOfWeek !== "*") return 7 * 24 * 60 * 60 * 1000; + if (dayOfWeek !== "*") {return 7 * 24 * 60 * 60 * 1000;} // "0 9 1 * *" → monthly (day of month specified, not *) - if (dayOfMonth !== "*") return 30 * 24 * 60 * 60 * 1000; + if (dayOfMonth !== "*") {return 30 * 24 * 60 * 60 * 1000;} // Default: daily return 24 * 60 * 60 * 1000; @@ -59,23 +59,23 @@ export class Scheduler { /** Parse a human-readable trigger or cron expression */ static parseTrigger(trigger: string): string | null { const trimmed = trigger.trim().toLowerCase(); - if (!trimmed) return null; + if (!trimmed) {return null;} // Check exact matches in map - if (TRIGGER_MAP[trimmed]) return TRIGGER_MAP[trimmed]; + if (TRIGGER_MAP[trimmed]) {return TRIGGER_MAP[trimmed];} // Check "every day Xam/pm" pattern const timeMatch = trimmed.match(TIME_REGEX); if (timeMatch) { let hour = parseInt(timeMatch[1], 10); const period = timeMatch[2].toLowerCase(); - if (period === "pm" && hour !== 12) hour += 12; - if (period === "am" && hour === 12) hour = 0; + if (period === "pm" && hour !== 12) {hour += 12;} + if (period === "am" && hour === 12) {hour = 0;} return `0 ${hour} * * *`; } // Check raw cron expression - if (CRON_REGEX.test(trimmed)) return trimmed; + if (CRON_REGEX.test(trimmed)) {return trimmed;} return null; } @@ -124,7 +124,7 @@ export class Scheduler { } start(): void { - if (this.running) return; + if (this.running) {return;} this.running = true; for (const rule of this.rules) { diff --git a/packages/dashboard/index.html b/packages/dashboard/index.html new file mode 100644 index 0000000..ef0d859 --- /dev/null +++ b/packages/dashboard/index.html @@ -0,0 +1,13 @@ + + + + + + OpenOctopus - 家庭管家 + + + +
+ + + diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json new file mode 100644 index 0000000..e362697 --- /dev/null +++ b/packages/dashboard/package.json @@ -0,0 +1,31 @@ +{ + "name": "@openoctopus/dashboard", + "version": "2026.3.10", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@xyflow/react": "^12.0.0", + "i18next": "^24.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-i18next": "^15.0.0", + "react-router": "^7.0.0", + "zustand": "^5.0.0" + }, + "devDependencies": { + "@openoctopus/shared": "workspace:*", + "@tailwindcss/vite": "^4.2.1", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.0.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.7.0", + "vite": "^6.0.0" + } +} diff --git a/packages/dashboard/src/App.tsx b/packages/dashboard/src/App.tsx new file mode 100644 index 0000000..c0936c2 --- /dev/null +++ b/packages/dashboard/src/App.tsx @@ -0,0 +1,32 @@ +import { createBrowserRouter, RouterProvider } from "react-router"; +import { ErrorBoundary } from "./components/layout/ErrorBoundary"; +import { Shell } from "./components/layout/Shell"; +import { Entities } from "./pages/Entities"; +import { Home } from "./pages/Home"; +import { Members } from "./pages/Members"; +import { RouteView } from "./pages/RouteView"; +import { Settings } from "./pages/Settings"; + +const router = createBrowserRouter( + [ + { + element: , + children: [ + { index: true, element: }, + { path: "route", element: }, + { path: "members", element: }, + { path: "entities", element: }, + { path: "settings", element: }, + ], + }, + ], + { basename: "/dashboard" }, +); + +export function App() { + return ( + + + + ); +} diff --git a/packages/dashboard/src/components/entity/EntityCard.tsx b/packages/dashboard/src/components/entity/EntityCard.tsx new file mode 100644 index 0000000..5760e6b --- /dev/null +++ b/packages/dashboard/src/components/entity/EntityCard.tsx @@ -0,0 +1,44 @@ +interface EntityCardProps { + name: string; + type: string; + realm: string; + icon: string; + attributes: string[]; + memoryCount: number; + healthScore: number; + onEdit?: () => void; +} + +export function EntityCard({ name, icon, type, realm, attributes, memoryCount, healthScore, onEdit }: EntityCardProps) { + return ( +
+
+
+ {icon} +
+

{name}

+

{type} · {realm}

+
+
+
+

{memoryCount} 条记忆

+

= 80 ? "text-green-600" : "text-orange-500"}> + 健康分 {healthScore} +

+
+
+
+ {attributes.map((attr) => ( + + {attr} + + ))} +
+ {onEdit && ( + + )} +
+ ); +} diff --git a/packages/dashboard/src/components/entity/SoulEditor.tsx b/packages/dashboard/src/components/entity/SoulEditor.tsx new file mode 100644 index 0000000..f556484 --- /dev/null +++ b/packages/dashboard/src/components/entity/SoulEditor.tsx @@ -0,0 +1,43 @@ +import { useState } from "react"; + +interface SoulEditorProps { + name: string; + tone: string; + traits: string[]; + onSave?: (data: { tone: string; traits: string[] }) => void; +} + +export function SoulEditor({ name, tone: initialTone, traits: initialTraits, onSave }: SoulEditorProps) { + const [tone, setTone] = useState(initialTone); + const [traits, setTraits] = useState(initialTraits.join(", ")); + + return ( +
+

✨ {name} 的性格设置

+
+ + setTone(e.target.value)} + className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm" + placeholder="例:活泼好动、偶尔撒娇" + /> +
+
+ + setTraits(e.target.value)} + className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm" + placeholder="例:贪吃, 怕打雷, 爱游泳" + /> +
+ +
+ ); +} diff --git a/packages/dashboard/src/components/family/MemberCard.tsx b/packages/dashboard/src/components/family/MemberCard.tsx new file mode 100644 index 0000000..582a4e8 --- /dev/null +++ b/packages/dashboard/src/components/family/MemberCard.tsx @@ -0,0 +1,43 @@ +interface MemberCardProps { + name: string; + icon: string; + role: string; + channels: string[]; + watchedRealms: string[]; +} + +const ROLE_LABELS: Record = { + owner: "管理员", + adult: "成人", + child: "儿童", + elder: "老人", +}; + +export function MemberCard({ name, icon, role, channels, watchedRealms }: MemberCardProps) { + return ( +
+
+ {icon} +
+

{name}

+

{ROLE_LABELS[role] ?? role}

+
+
+
+
+ 通道: + {channels.map((c) => ( + + {c} ✅ + + ))} +
+
+ 关注域: + {watchedRealms.join("、")} +
+
+ +
+ ); +} diff --git a/packages/dashboard/src/components/family/TopologyGraph.tsx b/packages/dashboard/src/components/family/TopologyGraph.tsx new file mode 100644 index 0000000..72efe1b --- /dev/null +++ b/packages/dashboard/src/components/family/TopologyGraph.tsx @@ -0,0 +1,80 @@ +import { ReactFlow, type Node, type Edge, Position, Background } from "@xyflow/react"; +import "@xyflow/react/dist/style.css"; + +interface TopologyProps { + members: Array<{ id: string; name: string; icon: string }>; + routes?: Array<{ from: string; to: string; relevance: "high" | "medium" | "low"; pushed: boolean }>; +} + +export function TopologyGraph({ members, routes = [] }: TopologyProps) { + const centerX = 300; + const centerY = 200; + const radius = 150; + + const hubNode: Node = { + id: "hub", + data: { label: "\u{1F419} \u7BA1\u5BB6" }, + position: { x: centerX - 40, y: centerY - 20 }, + style: { + background: "#00D4AA", + color: "white", + borderRadius: "16px", + padding: "8px 16px", + fontWeight: "bold", + border: "none", + }, + sourcePosition: Position.Right, + targetPosition: Position.Left, + }; + + const memberNodes: Node[] = members.map((m, i) => { + const angle = (2 * Math.PI * i) / members.length - Math.PI / 2; + return { + id: m.id, + data: { label: `${m.icon} ${m.name}` }, + position: { + x: centerX + radius * Math.cos(angle) - 40, + y: centerY + radius * Math.sin(angle) - 15, + }, + style: { + background: "white", + borderRadius: "12px", + padding: "6px 12px", + border: "1px solid #e5e7eb", + fontSize: "13px", + }, + }; + }); + + const edges: Edge[] = members.map((m) => { + const route = routes.find((r) => r.to === m.id); + return { + id: `hub-${m.id}`, + source: "hub", + target: m.id, + style: { + stroke: route?.pushed ? (route.relevance === "high" ? "#00D4AA" : "#94a3b8") : "#e5e7eb", + strokeWidth: route?.pushed ? 2 : 1, + strokeDasharray: route?.pushed ? undefined : "5,5", + }, + animated: route?.pushed ?? false, + }; + }); + + return ( +
+ + + +
+ ); +} diff --git a/packages/dashboard/src/components/layout/ErrorBoundary.tsx b/packages/dashboard/src/components/layout/ErrorBoundary.tsx new file mode 100644 index 0000000..27c48fa --- /dev/null +++ b/packages/dashboard/src/components/layout/ErrorBoundary.tsx @@ -0,0 +1,38 @@ +import { Component, type ReactNode } from "react"; + +interface Props { + children: ReactNode; +} + +interface State { + error: Error | null; +} + +export class ErrorBoundary extends Component { + state: State = { error: null }; + + static getDerivedStateFromError(error: Error): State { + return { error }; + } + + render() { + if (this.state.error) { + return ( +
+
+

🐙

+

页面出错了

+

{this.state.error.message}

+ +
+
+ ); + } + return this.props.children; + } +} diff --git a/packages/dashboard/src/components/layout/MobileNav.tsx b/packages/dashboard/src/components/layout/MobileNav.tsx new file mode 100644 index 0000000..f76140a --- /dev/null +++ b/packages/dashboard/src/components/layout/MobileNav.tsx @@ -0,0 +1,24 @@ +import { NavLink } from "react-router"; +import { NAV_ITEMS } from "./nav-items"; + +export function MobileNav() { + return ( + + ); +} diff --git a/packages/dashboard/src/components/layout/Shell.tsx b/packages/dashboard/src/components/layout/Shell.tsx new file mode 100644 index 0000000..d4f8af9 --- /dev/null +++ b/packages/dashboard/src/components/layout/Shell.tsx @@ -0,0 +1,29 @@ +import { Outlet } from "react-router"; +import { useGateway } from "../../hooks/use-gateway"; +import { useRealms } from "../../hooks/use-realms"; +import { useRouting } from "../../hooks/use-routing"; +import { useGatewayStore } from "../../stores/gateway"; +import { Sidebar } from "./Sidebar"; +import { MobileNav } from "./MobileNav"; + +export function Shell() { + const clientRef = useGateway(); + useRealms(clientRef); + useRouting(clientRef); + const status = useGatewayStore((s) => s.status); + + return ( +
+ +
+ {status !== "connected" && ( +
+ {status === "connecting" ? "正在连接网关..." : "未连接到网关"} +
+ )} + +
+ +
+ ); +} diff --git a/packages/dashboard/src/components/layout/Sidebar.tsx b/packages/dashboard/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..1335056 --- /dev/null +++ b/packages/dashboard/src/components/layout/Sidebar.tsx @@ -0,0 +1,30 @@ +import { NavLink } from "react-router"; +import { NAV_ITEMS } from "./nav-items"; + +export function Sidebar() { + return ( + + ); +} diff --git a/packages/dashboard/src/components/layout/nav-items.ts b/packages/dashboard/src/components/layout/nav-items.ts new file mode 100644 index 0000000..5d9648f --- /dev/null +++ b/packages/dashboard/src/components/layout/nav-items.ts @@ -0,0 +1,14 @@ +export interface NavItem { + path: string; + icon: string; + label: string; + shortLabel: string; +} + +export const NAV_ITEMS: NavItem[] = [ + { path: "/", icon: "🏠", label: "家庭总览", shortLabel: "总览" }, + { path: "/route", icon: "🔀", label: "路由视图", shortLabel: "路由" }, + { path: "/members", icon: "👥", label: "家庭成员", shortLabel: "成员" }, + { path: "/entities", icon: "🎯", label: "实体管理", shortLabel: "实体" }, + { path: "/settings", icon: "⚙️", label: "设置", shortLabel: "设置" }, +]; diff --git a/packages/dashboard/src/components/realm/RealmCard.tsx b/packages/dashboard/src/components/realm/RealmCard.tsx new file mode 100644 index 0000000..2273799 --- /dev/null +++ b/packages/dashboard/src/components/realm/RealmCard.tsx @@ -0,0 +1,27 @@ +interface RealmCardProps { + name: string; + icon: string; + lines: string[]; + alert?: boolean; +} + +export function RealmCard({ name, icon, lines, alert }: RealmCardProps) { + return ( +
+
+ {icon} + {name} + {alert && ⚠️} +
+
+ {lines.map((line, i) => ( +

{line}

+ ))} +
+
+ ); +} diff --git a/packages/dashboard/src/components/realm/RealmGrid.tsx b/packages/dashboard/src/components/realm/RealmGrid.tsx new file mode 100644 index 0000000..6093af2 --- /dev/null +++ b/packages/dashboard/src/components/realm/RealmGrid.tsx @@ -0,0 +1,37 @@ +import { useRealmsStore } from "../../stores/realms"; +import { RealmCard } from "./RealmCard"; + +const PLACEHOLDER_REALMS = [ + { name: "健康", icon: "🏥", lines: ["爷爷膝盖待复查", "降压药剩 5 天"], alert: true }, + { name: "财务", icon: "💰", lines: ["本月 ¥8,240", "预算剩 ¥3,760"] }, + { name: "宠物", icon: "🐱", lines: ["橘子:驱虫 3/24", "体重 5.2kg 正常"] }, + { name: "教育", icon: "📚", lines: ["春游费 ✅ 已交", "舞蹈课 周二"] }, + { name: "车辆", icon: "🚗", lines: ["保养还剩 1200km", "车险 4/15 到期"] }, + { name: "家务", icon: "🏠", lines: ["洗衣液快用完", "猫粮剩 3 天"] }, +]; + +const REALM_ICONS: Record = { + health: "🏥", finance: "💰", pet: "🐱", education: "📚", + vehicle: "🚗", household: "🏠", legal: "⚖️", work: "💼", +}; + +export function RealmGrid() { + const storeRealms = useRealmsStore((s) => s.realms); + + const realms = storeRealms.length > 0 + ? storeRealms.map((r) => ({ + name: r.name, + icon: r.icon ?? REALM_ICONS[r.name.toLowerCase()] ?? "📦", + lines: [`${r.entityCount ?? 0} 个实体`, `健康分 ${r.healthScore ?? "--"}`], + alert: (r.healthScore ?? 100) < 70, + })) + : PLACEHOLDER_REALMS; + + return ( +
+ {realms.map((r) => ( + + ))} +
+ ); +} diff --git a/packages/dashboard/src/components/realm/Timeline.tsx b/packages/dashboard/src/components/realm/Timeline.tsx new file mode 100644 index 0000000..f17e179 --- /dev/null +++ b/packages/dashboard/src/components/realm/Timeline.tsx @@ -0,0 +1,65 @@ +import { useFamilyStore } from "../../stores/family"; + +interface TimelineEvent { + time: string; + text: string; +} + +const PLACEHOLDER_EVENTS: Array<{ date: string; events: TimelineEvent[] }> = [ + { + date: "今天", + events: [ + { time: "10:32", text: "爷爷说膝盖疼 → 已通知爸爸(就医建议)、妈妈(采购)" }, + { time: "08:00", text: "晨间简报已推送给全家" }, + ], + }, + { + date: "昨天", + events: [ + { time: "16:20", text: "女儿春游费 ¥180 → 妈妈已确认转账" }, + { time: "09:15", text: "橘子体重 5.2kg → 正常范围(自动记录)" }, + ], + }, +]; + +function formatRouteEvent(ev: { source: { message: string }; targets: Array<{ memberId: string; summary: string }> }): string { + const targetTexts = ev.targets.map((t) => `${t.memberId}(${t.summary})`).join("、"); + return `${ev.source.message} → ${targetTexts}`; +} + +export function Timeline() { + const routeEvents = useFamilyStore((s) => s.routeEvents); + + const liveGroups = routeEvents.length > 0 + ? [{ + date: "最近", + events: routeEvents.slice(0, 10).map((ev) => ({ + time: new Date(ev.timestamp).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }), + text: formatRouteEvent(ev), + })), + }] + : null; + + const groups = liveGroups ?? PLACEHOLDER_EVENTS; + + return ( +
+

📋 家庭时间线

+
+ {groups.map((group) => ( +
+

{group.date}

+
+ {group.events.map((event, i) => ( +
+ {event.time} + {event.text} +
+ ))} +
+
+ ))} +
+
+ ); +} diff --git a/packages/dashboard/src/gateway/client.test.ts b/packages/dashboard/src/gateway/client.test.ts new file mode 100644 index 0000000..5890fea --- /dev/null +++ b/packages/dashboard/src/gateway/client.test.ts @@ -0,0 +1,116 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { GatewayClient } from "./client"; + +// Mock WebSocket with addEventListener support +class MockWebSocket { + static CONNECTING = 0; + static OPEN = 1; + static CLOSING = 2; + static CLOSED = 3; + + readyState = MockWebSocket.CONNECTING; + sent: string[] = []; + + private eventHandlers = new Map void>>(); + + addEventListener(event: string, handler: (e: unknown) => void) { + if (!this.eventHandlers.has(event)) { + this.eventHandlers.set(event, []); + } + this.eventHandlers.get(event)!.push(handler); + } + + send(data: string) { + this.sent.push(data); + } + + close() { + this.readyState = MockWebSocket.CLOSED; + this.dispatch("close", undefined); + } + + // Test helpers + simulateOpen() { + this.readyState = MockWebSocket.OPEN; + this.dispatch("open", undefined); + } + + simulateMessage(data: unknown) { + this.dispatch("message", { data: JSON.stringify(data) }); + } + + private dispatch(event: string, detail: unknown) { + for (const handler of this.eventHandlers.get(event) ?? []) { + handler(detail); + } + } +} + +describe("GatewayClient", () => { + let mockWs: MockWebSocket; + let client: GatewayClient; + + beforeEach(() => { + mockWs = new MockWebSocket(); + const MockWSConstructor = vi.fn(function () { + return mockWs; + }); + Object.assign(MockWSConstructor, { + CONNECTING: 0, + OPEN: 1, + CLOSING: 2, + CLOSED: 3, + }); + vi.stubGlobal("WebSocket", MockWSConstructor); + client = new GatewayClient("ws://localhost:19789"); + }); + + afterEach(() => { + client.disconnect(); + vi.unstubAllGlobals(); + }); + + it("connects to gateway", () => { + client.connect(); + expect(vi.mocked(WebSocket)).toHaveBeenCalledWith("ws://localhost:19789"); + }); + + it("sends RPC request and receives response", async () => { + client.connect(); + mockWs.simulateOpen(); + + const promise = client.request("realm.list", {}); + + const sent = JSON.parse(mockWs.sent[0]); + expect(sent.method).toBe("realm.list"); + + mockWs.simulateMessage({ id: sent.id, result: [{ id: "realm_1", name: "pet" }] }); + + const result = await promise; + expect(result).toEqual([{ id: "realm_1", name: "pet" }]); + }); + + it("handles RPC error response", async () => { + client.connect(); + mockWs.simulateOpen(); + + const promise = client.request("realm.get", { id: "nonexistent" }); + + const sent = JSON.parse(mockWs.sent[0]); + mockWs.simulateMessage({ id: sent.id, error: { code: 404, message: "Not found" } }); + + await expect(promise).rejects.toThrow("Not found"); + }); + + it("subscribes to events", () => { + client.connect(); + mockWs.simulateOpen(); + + const handler = vi.fn(); + client.on("crossrealm.reaction", handler); + + mockWs.simulateMessage({ event: "crossrealm.reaction", data: { realmId: "realm_1" } }); + + expect(handler).toHaveBeenCalledWith({ realmId: "realm_1" }); + }); +}); diff --git a/packages/dashboard/src/gateway/client.ts b/packages/dashboard/src/gateway/client.ts new file mode 100644 index 0000000..453dc23 --- /dev/null +++ b/packages/dashboard/src/gateway/client.ts @@ -0,0 +1,151 @@ +import type { RpcRequest, RpcResponse, RpcEvent } from "./types"; + +type EventHandler = (data: unknown) => void; +type PendingRequest = { + resolve: (value: unknown) => void; + reject: (error: Error) => void; + timer: ReturnType; +}; + +export class GatewayClient { + private ws: WebSocket | null = null; + private pending = new Map(); + private listeners = new Map>(); + private url: string; + private reconnectDelay = 800; + private maxReconnectDelay = 15000; + private shouldReconnect = false; + private reconnectTimer: ReturnType | null = null; + + constructor(url: string) { + this.url = url; + } + + connect(): void { + this.shouldReconnect = true; + this.reconnectDelay = 800; + this.createConnection(); + } + + disconnect(): void { + this.shouldReconnect = false; + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + if (this.ws) { + this.ws.close(); + this.ws = null; + } + // Reject all pending requests + for (const [id, pending] of this.pending) { + clearTimeout(pending.timer); + pending.reject(new Error("Client disconnected")); + this.pending.delete(id); + } + } + + get connected(): boolean { + return this.ws?.readyState === WebSocket.OPEN; + } + + async request(method: string, params: Record = {}): Promise { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + throw new Error("Not connected to gateway"); + } + + const id = `rpc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; + const request: RpcRequest = { id, method, params }; + + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + this.pending.delete(id); + reject(new Error(`RPC timeout: ${method}`)); + }, 30000); + + this.pending.set(id, { resolve, reject, timer }); + this.ws!.send(JSON.stringify(request)); + }); + } + + on(event: string, handler: EventHandler): () => void { + if (!this.listeners.has(event)) { + this.listeners.set(event, new Set()); + } + this.listeners.get(event)!.add(handler); + + // Return unsubscribe function + return () => { + this.listeners.get(event)?.delete(handler); + }; + } + + private createConnection(): void { + this.ws = new WebSocket(this.url); + + this.ws.addEventListener("open", () => { + this.reconnectDelay = 800; + this.emit("_connected", null); + }); + + this.ws.addEventListener("close", () => { + this.emit("_disconnected", null); + if (this.shouldReconnect) { + this.scheduleReconnect(); + } + }); + + this.ws.addEventListener("error", () => { + // close event will fire after this + }); + + this.ws.addEventListener("message", (e) => { + try { + const data = JSON.parse(e.data as string); + this.handleMessage(data); + } catch { + // Ignore malformed messages + } + }); + } + + private handleMessage(data: RpcResponse | RpcEvent): void { + // RPC response (has id) + if ("id" in data && typeof data.id === "string") { + const response = data as RpcResponse; + const pending = this.pending.get(response.id); + if (pending) { + clearTimeout(pending.timer); + this.pending.delete(response.id); + if (response.error) { + pending.reject(new Error(response.error.message)); + } else { + pending.resolve(response.result); + } + } + return; + } + + // RPC event (has event) + if ("event" in data && typeof data.event === "string") { + const event = data as RpcEvent; + this.emit(event.event, event.data); + } + } + + private emit(event: string, data: unknown): void { + const handlers = this.listeners.get(event); + if (handlers) { + for (const handler of handlers) { + handler(data); + } + } + } + + private scheduleReconnect(): void { + this.reconnectTimer = setTimeout(() => { + this.createConnection(); + }, this.reconnectDelay); + this.reconnectDelay = Math.min(this.reconnectDelay * 1.5, this.maxReconnectDelay); + } +} diff --git a/packages/dashboard/src/gateway/types.ts b/packages/dashboard/src/gateway/types.ts new file mode 100644 index 0000000..1c87287 --- /dev/null +++ b/packages/dashboard/src/gateway/types.ts @@ -0,0 +1,53 @@ +// Re-export shared RPC types for browser usage +// Note: we can't import @openoctopus/shared directly in browser +// (it targets NodeNext), so we re-declare the protocol types here. + +export interface RpcRequest { + id: string; + method: string; + params: Record; +} + +export interface RpcResponse { + id: string; + result?: unknown; + error?: { code: number; message: string }; +} + +export interface RpcEvent { + event: string; + requestId?: string; + data: unknown; +} + +// Mirror RPC_METHODS from @openoctopus/shared/rpc-protocol +export const RPC_METHODS = { + CHAT_SEND: "chat.send", + REALM_LIST: "realm.list", + REALM_GET: "realm.get", + REALM_CREATE: "realm.create", + REALM_UPDATE: "realm.update", + REALM_DELETE: "realm.delete", + ENTITY_LIST: "entity.list", + ENTITY_GET: "entity.get", + ENTITY_CREATE: "entity.create", + ENTITY_UPDATE: "entity.update", + ENTITY_DELETE: "entity.delete", + SUMMON_INVOKE: "summon.invoke", + SUMMON_RELEASE: "summon.release", + SUMMON_LIST: "summon.list", + STATUS_HEALTH: "status.health", + STATUS_INFO: "status.info", + HEALTH_REPORT: "health.report", +} as const; + +export const RPC_EVENTS = { + TOKEN: "chat.token", + DONE: "chat.done", + PROACTIVE: "proactive", + CHANNEL_MESSAGE: "channel.message", + REALM_UPDATE: "realm.update", + CROSS_REALM_REACTION: "crossrealm.reaction", + HEALTH_ALERT: "health.alert", + MATURITY_SUGGESTION: "maturity.suggestion", +} as const; diff --git a/packages/dashboard/src/hooks/use-gateway.ts b/packages/dashboard/src/hooks/use-gateway.ts new file mode 100644 index 0000000..6854f2c --- /dev/null +++ b/packages/dashboard/src/hooks/use-gateway.ts @@ -0,0 +1,30 @@ +import { useEffect, useRef } from "react"; +import { GatewayClient } from "../gateway/client"; +import { useGatewayStore } from "../stores/gateway"; + +export function useGateway() { + const clientRef = useRef(null); + const { url, setStatus } = useGatewayStore(); + + useEffect(() => { + // Read gateway URL from query params or store + const params = new URLSearchParams(window.location.search); + const gatewayUrl = params.get("gatewayUrl") ?? url; + + const client = new GatewayClient(gatewayUrl); + clientRef.current = client; + + client.on("_connected", () => setStatus("connected")); + client.on("_disconnected", () => setStatus("connecting")); + + setStatus("connecting"); + client.connect(); + + return () => { + client.disconnect(); + setStatus("disconnected"); + }; + }, [url, setStatus]); + + return clientRef; +} diff --git a/packages/dashboard/src/hooks/use-realms.ts b/packages/dashboard/src/hooks/use-realms.ts new file mode 100644 index 0000000..fe43038 --- /dev/null +++ b/packages/dashboard/src/hooks/use-realms.ts @@ -0,0 +1,36 @@ +import { useEffect } from "react"; +import { useGatewayStore } from "../stores/gateway"; +import { useRealmsStore } from "../stores/realms"; +import { GatewayClient } from "../gateway/client"; +import { RPC_METHODS } from "../gateway/types"; + +export function useRealms(clientRef: React.RefObject) { + const status = useGatewayStore((s) => s.status); + const { setRealms, setEntities } = useRealmsStore(); + + useEffect(() => { + if (status !== "connected" || !clientRef.current) { + return; + } + + const client = clientRef.current; + + async function fetchData() { + try { + const realms = await client.request(RPC_METHODS.REALM_LIST); + if (Array.isArray(realms)) { + setRealms(realms); + } + + const entities = await client.request(RPC_METHODS.ENTITY_LIST); + if (Array.isArray(entities)) { + setEntities(entities); + } + } catch { + // Gateway may not support these methods yet + } + } + + fetchData(); + }, [status, clientRef, setRealms, setEntities]); +} diff --git a/packages/dashboard/src/hooks/use-routing.ts b/packages/dashboard/src/hooks/use-routing.ts new file mode 100644 index 0000000..d77e5a2 --- /dev/null +++ b/packages/dashboard/src/hooks/use-routing.ts @@ -0,0 +1,24 @@ +import { useEffect } from "react"; +import { useFamilyStore } from "../stores/family"; +import { useGatewayStore } from "../stores/gateway"; +import type { GatewayClient } from "../gateway/client"; +import { RPC_EVENTS } from "../gateway/types"; + +export function useRouting(clientRef: React.RefObject) { + const status = useGatewayStore((s) => s.status); + const addRouteEvent = useFamilyStore((s) => s.addRouteEvent); + + useEffect(() => { + if (status !== "connected" || !clientRef.current) { + return; + } + + const unsubReaction = clientRef.current.on(RPC_EVENTS.CROSS_REALM_REACTION, (data) => { + addRouteEvent(data as Parameters[0]); + }); + + return () => { + unsubReaction(); + }; + }, [status, clientRef, addRouteEvent]); +} diff --git a/packages/dashboard/src/i18n/en.json b/packages/dashboard/src/i18n/en.json new file mode 100644 index 0000000..ce06091 --- /dev/null +++ b/packages/dashboard/src/i18n/en.json @@ -0,0 +1,9 @@ +{ + "nav.home": "Family Overview", + "nav.route": "Route View", + "nav.members": "Members", + "nav.entities": "Entities", + "nav.settings": "Settings", + "home.title": "Family Butler", + "home.timeline": "Family Timeline" +} diff --git a/packages/dashboard/src/i18n/index.ts b/packages/dashboard/src/i18n/index.ts new file mode 100644 index 0000000..d61bcf7 --- /dev/null +++ b/packages/dashboard/src/i18n/index.ts @@ -0,0 +1,13 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import zh from "./zh.json"; +import en from "./en.json"; + +i18n.use(initReactI18next).init({ + resources: { zh: { translation: zh }, en: { translation: en } }, + lng: localStorage.getItem("oo-lang") ?? "zh", + fallbackLng: "zh", + interpolation: { escapeValue: false }, +}); + +export default i18n; diff --git a/packages/dashboard/src/i18n/zh.json b/packages/dashboard/src/i18n/zh.json new file mode 100644 index 0000000..b6fc0f6 --- /dev/null +++ b/packages/dashboard/src/i18n/zh.json @@ -0,0 +1,9 @@ +{ + "nav.home": "家庭总览", + "nav.route": "路由视图", + "nav.members": "家庭成员", + "nav.entities": "实体管理", + "nav.settings": "设置", + "home.title": "家庭管家", + "home.timeline": "家庭时间线" +} diff --git a/packages/dashboard/src/main.tsx b/packages/dashboard/src/main.tsx new file mode 100644 index 0000000..7020020 --- /dev/null +++ b/packages/dashboard/src/main.tsx @@ -0,0 +1,11 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "./App"; +import "./i18n"; +import "./styles/globals.css"; + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/packages/dashboard/src/pages/Entities.tsx b/packages/dashboard/src/pages/Entities.tsx new file mode 100644 index 0000000..fb57ce7 --- /dev/null +++ b/packages/dashboard/src/pages/Entities.tsx @@ -0,0 +1,83 @@ +import { useState } from "react"; +import { useRealmsStore } from "../stores/realms"; +import { EntityCard } from "../components/entity/EntityCard"; +import { SoulEditor } from "../components/entity/SoulEditor"; + +const PLACEHOLDER_REALMS = ["全部", "宠物", "健康", "车辆", "教育"]; +const PLACEHOLDER_ENTITIES = [ + { name: "橘子", icon: "🐱", type: "living", realm: "宠物", attributes: ["英短", "3岁", "5.2kg"], memoryCount: 23, healthScore: 85 }, + { name: "爷爷的膝盖", icon: "🦵", type: "abstract", realm: "健康", attributes: ["左膝", "复查中"], memoryCount: 8, healthScore: 60 }, + { name: "家用车", icon: "🚗", type: "asset", realm: "车辆", attributes: ["荣威", "2022款"], memoryCount: 12, healthScore: 75 }, +]; + +const REALM_NAMES: Record = { + health: "健康", finance: "财务", pet: "宠物", education: "教育", + vehicle: "车辆", household: "家务", legal: "法务", work: "工作", +}; + +export function Entities() { + const [filter, setFilter] = useState("全部"); + const [editingEntity, setEditingEntity] = useState(null); + + const storeRealms = useRealmsStore((s) => s.realms); + const storeEntities = useRealmsStore((s) => s.entities); + + const realmFilters = storeRealms.length > 0 + ? ["全部", ...storeRealms.map((r) => r.name)] + : PLACEHOLDER_REALMS; + + const entities = storeEntities.length > 0 + ? storeEntities.map((e) => { + const realm = storeRealms.find((r) => r.id === e.realmId); + return { + name: e.name, + icon: e.type === "living" ? "🐾" : e.type === "asset" ? "📦" : "💡", + type: e.type, + realm: realm?.name ?? REALM_NAMES[e.realmId] ?? e.realmId, + attributes: Object.values(e.attributes ?? {}).map(String), + memoryCount: 0, + healthScore: 0, + }; + }) + : PLACEHOLDER_ENTITIES; + + const filtered = filter === "全部" ? entities : entities.filter((e) => e.realm === filter); + + return ( +
+
+

🎯 实体管理

+ +
+ +
+ {realmFilters.map((r) => ( + + ))} +
+ +
+ {filtered.map((e) => ( +
+ setEditingEntity(editingEntity === e.name ? null : e.name)} /> + {editingEntity === e.name && ( +
+ +
+ )} +
+ ))} +
+
+ ); +} diff --git a/packages/dashboard/src/pages/Home.tsx b/packages/dashboard/src/pages/Home.tsx new file mode 100644 index 0000000..f9b6d5b --- /dev/null +++ b/packages/dashboard/src/pages/Home.tsx @@ -0,0 +1,14 @@ +import { RealmGrid } from "../components/realm/RealmGrid"; +import { Timeline } from "../components/realm/Timeline"; + +export function Home() { + return ( +
+
+

🐙 王家 · 家庭管家

+
+ + +
+ ); +} diff --git a/packages/dashboard/src/pages/Members.tsx b/packages/dashboard/src/pages/Members.tsx new file mode 100644 index 0000000..37d7d33 --- /dev/null +++ b/packages/dashboard/src/pages/Members.tsx @@ -0,0 +1,39 @@ +import { useFamilyStore } from "../stores/family"; +import { MemberCard } from "../components/family/MemberCard"; + +const PLACEHOLDER_MEMBERS = [ + { name: "爸爸(王明)", icon: "👨", role: "adult", channels: ["微信", "Telegram"], watchedRealms: ["健康", "财务", "车辆", "工作"] }, + { name: "妈妈(李雪)", icon: "👩", role: "owner", channels: ["微信"], watchedRealms: ["全部"] }, + { name: "爷爷(王德)", icon: "👴", role: "elder", channels: ["微信"], watchedRealms: ["健康"] }, + { name: "女儿(王小雪)", icon: "👧", role: "child", channels: ["微信"], watchedRealms: ["教育"] }, +]; + +export function Members() { + const storeMembers = useFamilyStore((s) => s.members); + + const members = storeMembers.length > 0 + ? storeMembers.map((m) => ({ + name: m.name, + icon: m.avatar ?? "👤", + role: m.role, + channels: m.channels, + watchedRealms: m.watchedRealms, + })) + : PLACEHOLDER_MEMBERS; + + return ( +
+
+

👥 家庭成员

+ +
+
+ {members.map((m) => ( + + ))} +
+
+ ); +} diff --git a/packages/dashboard/src/pages/RouteView.tsx b/packages/dashboard/src/pages/RouteView.tsx new file mode 100644 index 0000000..2339f46 --- /dev/null +++ b/packages/dashboard/src/pages/RouteView.tsx @@ -0,0 +1,75 @@ +import { useFamilyStore } from "../stores/family"; +import { TopologyGraph } from "../components/family/TopologyGraph"; + +const PLACEHOLDER_MEMBERS = [ + { id: "grandpa", name: "爷爷", icon: "👴" }, + { id: "dad", name: "爸爸", icon: "👨" }, + { id: "mom", name: "妈妈", icon: "👩" }, + { id: "daughter", name: "女儿", icon: "👧" }, +]; + +const PLACEHOLDER_ROUTES = [ + { from: "grandpa", to: "dad", relevance: "high" as const, pushed: true }, + { from: "grandpa", to: "mom", relevance: "medium" as const, pushed: true }, + { from: "grandpa", to: "daughter", relevance: "low" as const, pushed: false }, +]; + +const RELEVANCE_LABEL = { high: "高相关", medium: "中相关", low: "低相关" } as const; + +export function RouteView() { + const storeMembers = useFamilyStore((s) => s.members); + const routeEvents = useFamilyStore((s) => s.routeEvents); + + const members = storeMembers.length > 0 + ? storeMembers.map((m) => ({ id: m.id, name: m.name, icon: m.avatar ?? "👤" })) + : PLACEHOLDER_MEMBERS; + + const routes = routeEvents.length > 0 + ? routeEvents.flatMap((ev) => + ev.targets.map((t) => ({ + from: ev.source.memberId, + to: t.memberId, + relevance: t.relevance, + pushed: t.pushed, + })), + ) + : PLACEHOLDER_ROUTES; + + return ( +
+

🔀 消息路由

+ + +
+

最近路由

+
+ {routeEvents.length > 0 ? ( + routeEvents.slice(0, 5).map((ev) => ( +
+

+ [{ev.source.message}] {ev.realms.join(", ")} +

+ {ev.targets.map((t) => ( +

+ → {t.memberId}:{t.summary}({RELEVANCE_LABEL[t.relevance]}) + {!t.pushed && " — 未推送"} +

+ ))} +
+ )) + ) : ( +
+

[爷爷膝盖疼] Health → Finance, Calendar

+

→ 👨 爸爸:就医建议(高相关)

+

→ 👩 妈妈:采购止痛贴(中相关)

+

→ 👧 女儿:未推送(低相关)

+
+ )} +
+
+
+ ); +} diff --git a/packages/dashboard/src/pages/Settings.tsx b/packages/dashboard/src/pages/Settings.tsx new file mode 100644 index 0000000..840f89e --- /dev/null +++ b/packages/dashboard/src/pages/Settings.tsx @@ -0,0 +1,120 @@ +import { useGatewayStore } from "../stores/gateway"; +import { useRealmsStore } from "../stores/realms"; +import { useFamilyStore } from "../stores/family"; + +const STATUS_LABEL = { + connected: "✅ 已连接", + connecting: "🔄 连接中…", + disconnected: "⬜ 未连接", + error: "❌ 连接错误", +} as const; + +const PLACEHOLDER_CHANNELS = [ + { name: "微信小程序", status: true }, + { name: "Telegram Bot", status: true }, + { name: "Discord", status: false }, +]; + +export function Settings() { + const { status, url, error } = useGatewayStore(); + const realmCount = useRealmsStore((s) => s.realms.length); + const entityCount = useRealmsStore((s) => s.entities.length); + const memberCount = useFamilyStore((s) => s.members.length); + + return ( +
+

⚙️ 设置

+ + {/* Gateway Connection */} +
+

🌐 网关连接

+
+
+ 网关地址 + {url} +
+
+ 状态 + + {STATUS_LABEL[status]} + +
+ {error && ( +
{error}
+ )} +
+ 数据概览 + + {realmCount} 个领域 · {entityCount} 个实体 · {memberCount} 个成员 + +
+
+
+ + {/* AI Model */} +
+

🤖 AI 模型

+
+
+ 当前模型 + Claude Sonnet 4 +
+
+ API Key + sk-ant-•••••••el3 +
+
+
+ + {/* Channels */} +
+

📱 通道连接

+
+ {PLACEHOLDER_CHANNELS.map((ch) => ( +
+ {ch.name} + + {ch.status ? "✅ 已连接" : "⬜ 未连接"} + +
+ ))} +
+
+ + {/* Notifications */} +
+

🔔 通知策略

+
+
+ 晨间简报 + 每天 08:00 +
+
+ 最大推送频率 + 每小时 3 条 +
+
+ 免打扰时段 + 22:00 - 07:00 +
+
+ 紧急事件 + 始终推送 +
+
+
+ + {/* Data */} +
+

💾 数据

+
+
+ 存储位置 + 本地 (SQLite) +
+ +
+
+
+ ); +} diff --git a/packages/dashboard/src/stores/family.ts b/packages/dashboard/src/stores/family.ts new file mode 100644 index 0000000..7c2cd90 --- /dev/null +++ b/packages/dashboard/src/stores/family.ts @@ -0,0 +1,40 @@ +import { create } from "zustand"; + +interface FamilyMember { + id: string; + name: string; + role: "owner" | "adult" | "child" | "elder"; + avatar?: string; + channels: string[]; + watchedRealms: string[]; +} + +interface RouteEvent { + id: string; + timestamp: string; + source: { memberId: string; message: string }; + realms: string[]; + targets: Array<{ + memberId: string; + relevance: "high" | "medium" | "low"; + pushed: boolean; + summary: string; + }>; +} + +interface FamilyState { + members: FamilyMember[]; + routeEvents: RouteEvent[]; + setMembers: (members: FamilyMember[]) => void; + addRouteEvent: (event: RouteEvent) => void; +} + +export const useFamilyStore = create((set) => ({ + members: [], + routeEvents: [], + setMembers: (members) => set({ members }), + addRouteEvent: (event) => + set((state) => ({ + routeEvents: [event, ...state.routeEvents].slice(0, 100), + })), +})); diff --git a/packages/dashboard/src/stores/gateway.test.ts b/packages/dashboard/src/stores/gateway.test.ts new file mode 100644 index 0000000..a70d660 --- /dev/null +++ b/packages/dashboard/src/stores/gateway.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { useGatewayStore } from "./gateway"; + +describe("useGatewayStore", () => { + beforeEach(() => { + useGatewayStore.setState({ + status: "disconnected", + url: "ws://localhost:19789", + error: null, + }); + }); + + it("has correct initial state", () => { + const state = useGatewayStore.getState(); + expect(state.status).toBe("disconnected"); + expect(state.url).toBe("ws://localhost:19789"); + expect(state.error).toBeNull(); + }); + + it("updates status", () => { + useGatewayStore.getState().setStatus("connected"); + expect(useGatewayStore.getState().status).toBe("connected"); + }); + + it("clears error on connected", () => { + useGatewayStore.getState().setError("connection refused"); + expect(useGatewayStore.getState().error).toBe("connection refused"); + + useGatewayStore.getState().setStatus("connected"); + expect(useGatewayStore.getState().error).toBeNull(); + }); + + it("preserves existing error when status changes to non-connected", () => { + useGatewayStore.getState().setError("timeout"); + useGatewayStore.getState().setStatus("connecting"); + expect(useGatewayStore.getState().error).toBe("timeout"); + }); +}); diff --git a/packages/dashboard/src/stores/gateway.ts b/packages/dashboard/src/stores/gateway.ts new file mode 100644 index 0000000..cac9650 --- /dev/null +++ b/packages/dashboard/src/stores/gateway.ts @@ -0,0 +1,21 @@ +import { create } from "zustand"; + +type ConnectionStatus = "disconnected" | "connecting" | "connected" | "error"; + +interface GatewayState { + status: ConnectionStatus; + url: string; + error: string | null; + setStatus: (status: ConnectionStatus) => void; + setUrl: (url: string) => void; + setError: (error: string | null) => void; +} + +export const useGatewayStore = create((set) => ({ + status: "disconnected", + url: "ws://localhost:19789", + error: null, + setStatus: (status) => set((state) => ({ status, error: status === "connected" ? null : state.error })), + setUrl: (url) => set({ url }), + setError: (error) => set({ error, status: error ? "error" : "disconnected" }), +})); diff --git a/packages/dashboard/src/stores/realms.ts b/packages/dashboard/src/stores/realms.ts new file mode 100644 index 0000000..0a349fd --- /dev/null +++ b/packages/dashboard/src/stores/realms.ts @@ -0,0 +1,37 @@ +import { create } from "zustand"; + +interface Realm { + id: string; + name: string; + icon?: string; + status?: string; + entityCount?: number; + healthScore?: number; +} + +interface Entity { + id: string; + name: string; + type: "living" | "asset" | "organization" | "abstract"; + realmId: string; + attributes?: Record; +} + +interface RealmsState { + realms: Realm[]; + entities: Entity[]; + setRealms: (realms: Realm[]) => void; + setEntities: (entities: Entity[]) => void; + updateRealm: (id: string, data: Partial) => void; +} + +export const useRealmsStore = create((set) => ({ + realms: [], + entities: [], + setRealms: (realms) => set({ realms }), + setEntities: (entities) => set({ entities }), + updateRealm: (id, data) => + set((state) => ({ + realms: state.realms.map((r) => (r.id === id ? { ...r, ...data } : r)), + })), +})); diff --git a/packages/dashboard/src/styles/globals.css b/packages/dashboard/src/styles/globals.css new file mode 100644 index 0000000..f2d43ce --- /dev/null +++ b/packages/dashboard/src/styles/globals.css @@ -0,0 +1,11 @@ +@import "tailwindcss"; + +@theme { + --color-ocean: #1E3A5F; + --color-cyan: #00D4AA; + --color-purple: #6C3FA0; + --color-abyss: #0D1117; + --color-surface: #F6F8FA; + --radius-card: 16px; + --font-sans: "Noto Sans SC", "Inter", system-ui, sans-serif; +} diff --git a/packages/dashboard/src/vite-env.d.ts b/packages/dashboard/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/packages/dashboard/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/dashboard/tsconfig.json b/packages/dashboard/tsconfig.json new file mode 100644 index 0000000..d8d6212 --- /dev/null +++ b/packages/dashboard/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "noEmit": true, + "isolatedModules": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/packages/dashboard/tsconfig.node.json b/packages/dashboard/tsconfig.node.json new file mode 100644 index 0000000..a4b6edb --- /dev/null +++ b/packages/dashboard/tsconfig.node.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "composite": true, + "isolatedModules": true, + "skipLibCheck": true + }, + "include": ["vite.config.ts"] +} diff --git a/packages/dashboard/vite.config.ts b/packages/dashboard/vite.config.ts new file mode 100644 index 0000000..bdfae7a --- /dev/null +++ b/packages/dashboard/vite.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; +import path from "node:path"; + +export default defineConfig({ + plugins: [react(), tailwindcss()], + base: "/dashboard/", + resolve: { + alias: { + "@": path.resolve(import.meta.dirname, "src"), + }, + }, + build: { + outDir: "../ink/public/dashboard", + emptyOutDir: true, + }, + server: { + port: 5173, + proxy: { + "/api": "http://localhost:19790", + }, + }, +}); diff --git a/packages/ink/src/chat-pipeline.ts b/packages/ink/src/chat-pipeline.ts index f202c7f..485adbd 100644 --- a/packages/ink/src/chat-pipeline.ts +++ b/packages/ink/src/chat-pipeline.ts @@ -260,7 +260,7 @@ async function handleSystemAction( switch (action) { case "summon": { const entityName = args.entityName; - if (!entityName) return "Please specify which entity to summon."; + if (!entityName) {return "Please specify which entity to summon.";} // Find entity by name across all realms (case-insensitive exact match) const realms = services.realmManager.list(); @@ -277,7 +277,7 @@ async function handleSystemAction( } // Try fuzzy/case-insensitive matching - const allEntities: Array<{ entity: Entity; realm: Realm }> = []; + const allEntities: Array<{ entity: Entity; realm: RealmState }> = []; for (const realm of realms) { const entities = services.entityManager.listByRealm(realm.id); for (const entity of entities) { @@ -317,7 +317,7 @@ async function handleSystemAction( case "unsummon": { const entityName = args.entityName; - if (!entityName) return "Please specify which entity to unsummon."; + if (!entityName) {return "Please specify which entity to unsummon.";} const active = services.summonEngine.listActive(); const match = active.find(a => a.entity.name.toLowerCase() === entityName.toLowerCase()); @@ -330,7 +330,7 @@ async function handleSystemAction( case "list_realms": { const realms = services.realmManager.list(); - if (realms.length === 0) return "No realms configured yet."; + if (realms.length === 0) {return "No realms configured yet.";} const lines = realms.map(r => { const icon = r.icon ? `${r.icon} ` : ""; @@ -354,13 +354,13 @@ async function handleSystemAction( } } } - if (lines.length === 0) return "No entities found across any realm."; + if (lines.length === 0) {return "No entities found across any realm.";} return `Entities:${lines.join("\n")}`; } case "switch_realm": { const realmName = args.realmName; - if (!realmName) return "Please specify which realm to switch to."; + if (!realmName) {return "Please specify which realm to switch to.";} const realm = services.realmManager.findByName(realmName); if (realm) { @@ -411,7 +411,7 @@ function inferPreviousRealmFromTranscript( ): string | undefined { // Look at the last few messages for realm-related content const recent = messages.slice(-4); - if (recent.length === 0) return undefined; + if (recent.length === 0) {return undefined;} // Check if any recent message metadata contains realm info // For now, use keyword matching on recent messages @@ -473,7 +473,7 @@ function inferRealmFromMessage(message: string, services: RpcServices): string | const keywords = realmKeywords[realm.name.toLowerCase()] ?? []; let score = 0; for (const kw of keywords) { - if (lowered.includes(kw)) score++; + if (lowered.includes(kw)) {score++;} } if (score > bestScore) { bestScore = score; diff --git a/packages/ink/src/server.ts b/packages/ink/src/server.ts index d138af6..8c0f63f 100644 --- a/packages/ink/src/server.ts +++ b/packages/ink/src/server.ts @@ -135,6 +135,23 @@ export async function createServer(options: InkServerOptions = {}): Promise { + res.sendFile(path.join(dashboardDir, "index.html")); + }); + log.info(`Dashboard serving from ${dashboardDir}`); + break; + } + } + app.use((err: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => { const response = toErrorResponse(err); res.status(response.status).json(response); diff --git a/packages/storage/src/repos/memory-repo.test.ts b/packages/storage/src/repos/memory-repo.test.ts index 1c3ac28..3cff97b 100644 --- a/packages/storage/src/repos/memory-repo.test.ts +++ b/packages/storage/src/repos/memory-repo.test.ts @@ -5,6 +5,8 @@ import { MemoryRepo } from "./memory-repo.js"; import { RealmRepo } from "./realm-repo.js"; import { EntityRepo } from "./entity-repo.js"; +const mockEmbed = async (texts: string[]) => texts.map(() => [1, 0, 0]); + let db: Database.Database; let repo: MemoryRepo; let realmRepo: RealmRepo; @@ -253,7 +255,6 @@ describe("MemoryRepo", () => { it("backfillEmbeddings processes memories without embeddings", async () => { repo.create({ realmId: "realm_sem", tier: "archival", content: "fact1" }); repo.create({ realmId: "realm_sem", tier: "archival", content: "fact2" }); - const mockEmbed = async (texts: string[]) => texts.map(() => [1, 0, 0]); const result = await repo.backfillEmbeddings(mockEmbed); expect(result.processed).toBe(2); expect(result.skipped).toBe(0); @@ -263,7 +264,6 @@ describe("MemoryRepo", () => { const e = repo.create({ realmId: "realm_sem", tier: "archival", content: "has_emb" }); repo.updateEmbedding(e.id, [0.5, 0.5, 0.5]); repo.create({ realmId: "realm_sem", tier: "archival", content: "no_emb" }); - const mockEmbed = async (texts: string[]) => texts.map(() => [1, 0, 0]); const result = await repo.backfillEmbeddings(mockEmbed); expect(result.processed).toBe(1); expect(result.skipped).toBe(1); diff --git a/packages/storage/src/repos/memory-repo.ts b/packages/storage/src/repos/memory-repo.ts index 9ca2be3..e636e1a 100644 --- a/packages/storage/src/repos/memory-repo.ts +++ b/packages/storage/src/repos/memory-repo.ts @@ -161,7 +161,7 @@ export class MemoryRepo { } deleteMany(ids: string[]): number { - if (ids.length === 0) return 0; + if (ids.length === 0) {return 0;} const placeholders = ids.map(() => "?").join(","); const result = this.db.prepare(`DELETE FROM memories WHERE id IN (${placeholders})`).run(...ids); return result.changes; @@ -179,7 +179,7 @@ export class MemoryRepo { getById(id: string): MemoryEntry { const row = this.db.prepare("SELECT * FROM memories WHERE id = ?").get(id) as MemoryRow | undefined; - if (!row) throw new Error(`Memory ${id} not found`); + if (!row) {throw new Error(`Memory ${id} not found`);} return rowToMemory(row); } @@ -193,18 +193,18 @@ export class MemoryRepo { for (const row of rows) { const entry = rowToMemory(row); - if (!entry.embedding) continue; + if (!entry.embedding) {continue;} // Filter by matching dimension count const embeddingDim = (entry.metadata as Record).embeddingDim as number | undefined; - if (embeddingDim !== undefined && embeddingDim !== queryDim) continue; - if (entry.embedding.length !== queryDim) continue; + if (embeddingDim !== undefined && embeddingDim !== queryDim) {continue;} + if (entry.embedding.length !== queryDim) {continue;} const score = cosineSimilarity(queryVector, entry.embedding); scored.push({ entry, score }); } return scored - .sort((a, b) => b.score - a.score) + .toSorted((a, b) => b.score - a.score) .slice(0, topK) .map(s => s.entry); } diff --git a/packages/storage/src/repos/scanned-file-repo.test.ts b/packages/storage/src/repos/scanned-file-repo.test.ts index a1577f9..f72a1ee 100644 --- a/packages/storage/src/repos/scanned-file-repo.test.ts +++ b/packages/storage/src/repos/scanned-file-repo.test.ts @@ -49,7 +49,7 @@ describe("ScannedFileRepo", () => { it("upsert overwrites on conflict", () => { repo.upsert({ path: "/dup.md", fileHash: "h1", factsExtracted: 1 }); repo.upsert({ path: "/dup.md", fileHash: "h2", factsExtracted: 2 }); - const all = repo.listByRealm("realm_any"); + const _all = repo.listByRealm("realm_any"); // Both entries have no realmId matching "realm_any", so this returns 0 // Instead verify by findByPath that there is only one record const file = repo.findByPath("/dup.md"); diff --git a/packages/tentacle/src/tui/commands.ts b/packages/tentacle/src/tui/commands.ts index c2558df..4476075 100644 --- a/packages/tentacle/src/tui/commands.ts +++ b/packages/tentacle/src/tui/commands.ts @@ -291,7 +291,7 @@ async function handleInject(client: WsRpcClient, text?: string): Promise { +async function handleMaturity(client: WsRpcClient, realmName?: string, _state?: TuiState): Promise { try { if (realmName) { const listResponse = await client.call("realm.list"); diff --git a/packages/tentacle/src/tui/renderer.ts b/packages/tentacle/src/tui/renderer.ts index 8ea3ede..131c306 100644 --- a/packages/tentacle/src/tui/renderer.ts +++ b/packages/tentacle/src/tui/renderer.ts @@ -297,9 +297,9 @@ export interface HealthReportData { } function healthIcon(score: number): string { - if (score >= 80) return pc.green("\u2705"); - if (score >= 50) return pc.yellow("\u26A0\uFE0F"); - if (score >= 20) return pc.red("\u274C"); + if (score >= 80) {return pc.green("\u2705");} + if (score >= 50) {return pc.yellow("\u26A0\uFE0F");} + if (score >= 20) {return pc.red("\u274C");} return "\u{1F4A4}"; } @@ -326,7 +326,7 @@ export function renderHealthReport(report: HealthReportData): string { } export function renderHealthDashboard(reports: HealthReportData[]): string { - if (reports.length === 0) return pc.dim("No realms to report on."); + if (reports.length === 0) {return pc.dim("No realms to report on.");} const lines: string[] = []; lines.push(pc.bold(" Knowledge Health Report")); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eafe96e..c517c8f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,7 +75,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.0 - version: 4.0.18(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) packages/channels: dependencies: @@ -98,6 +98,55 @@ importers: specifier: ^2.7.0 version: 2.8.2 + packages/dashboard: + dependencies: + '@xyflow/react': + specifier: ^12.0.0 + version: 12.10.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + i18next: + specifier: ^24.0.0 + version: 24.2.3(typescript@5.9.3) + react: + specifier: ^19.0.0 + version: 19.2.4 + react-dom: + specifier: ^19.0.0 + version: 19.2.4(react@19.2.4) + react-i18next: + specifier: ^15.0.0 + version: 15.7.4(i18next@24.2.3(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + react-router: + specifier: ^7.0.0 + version: 7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + zustand: + specifier: ^5.0.0 + version: 5.0.12(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) + devDependencies: + '@openoctopus/shared': + specifier: workspace:* + version: link:../shared + '@tailwindcss/vite': + specifier: ^4.2.1 + version: 4.2.1(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + '@types/react': + specifier: ^19.0.0 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.0.0 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^4.0.0 + version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + tailwindcss: + specifier: ^4.0.0 + version: 4.2.1 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + vite: + specifier: ^6.0.0 + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + packages/ink: dependencies: '@openoctopus/channels': @@ -199,23 +248,110 @@ importers: packages: + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + '@babel/generator@8.0.0-rc.2': resolution: {integrity: sha512-oCQ1IKPwkzCeJzAPb7Fv8rQ9k5+1sG8mf2uoHiMInPYvkRfrDJxbTIbH51U+jstlkghus0vAi3EBvkfvEsYNLQ==} engines: {node: ^20.19.0 || >=22.12.0} + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@8.0.0-rc.2': resolution: {integrity: sha512-noLx87RwlBEMrTzncWd/FvTxoJ9+ycHNg0n8yyYydIoDsLZuxknKgWRJUqcrVkNrJ74uGyhWQzQaS3q8xfGAhQ==} engines: {node: ^20.19.0 || >=22.12.0} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@8.0.0-rc.2': resolution: {integrity: sha512-xExUBkuXWJjVuIbO7z6q7/BA9bgfJDEhVL0ggrggLMbg0IzCUWGT1hZGE8qUH7Il7/RD/a6cZ3AAFrrlp1LF/A==} engines: {node: ^20.19.0 || >=22.12.0} + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/parser@8.0.0-rc.2': resolution: {integrity: sha512-29AhEtcq4x8Dp3T72qvUMZHx0OMXCj4Jy/TEReQa+KWLln524Cj1fWb3QFi0l/xSpptQBR6y9RNEXuxpFvwiUQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + '@babel/types@8.0.0-rc.2': resolution: {integrity: sha512-91gAaWRznDwSX4E2tZ1YjBuIfnQVOFDCQ2r0Toby0gu4XEbyF623kXLMA8d4ZbCu+fINcrudkmEcwSUHgDDkNw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -235,156 +371,312 @@ packages: '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.27.3': resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.27.3': resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.27.3': resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.27.3': resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.27.3': resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.27.3': resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.27.3': resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.27.3': resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.27.3': resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.27.3': resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.27.3': resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.27.3': resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.27.3': resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.27.3': resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.27.3': resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.27.3': resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.27.3': resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-arm64@0.27.3': resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.27.3': resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-arm64@0.27.3': resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.27.3': resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/openharmony-arm64@0.27.3': resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.27.3': resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.27.3': resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.27.3': resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.27.3': resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} engines: {node: '>=18'} @@ -397,6 +689,9 @@ packages: '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -875,6 +1170,9 @@ packages: cpu: [x64] os: [win32] + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rolldown/pluginutils@1.0.0-rc.8': resolution: {integrity: sha512-wzJwL82/arVfeSP3BLr1oTy40XddjtEdrdgtJ4lLRBu06mP3q/8HGM6K0JRlQuTA3XB0pNJx2so/nmpY4xyOew==} @@ -1019,9 +1317,115 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tailwindcss/node@4.2.1': + resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} + + '@tailwindcss/oxide-android-arm64@4.2.1': + resolution: {integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + resolution: {integrity: sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.1': + resolution: {integrity: sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + resolution: {integrity: sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + resolution: {integrity: sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + resolution: {integrity: sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + resolution: {integrity: sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.1': + resolution: {integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.2.1': + resolution: {integrity: sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/better-sqlite3@7.6.13': resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} @@ -1034,6 +1438,24 @@ packages: '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -1061,6 +1483,14 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/send@1.2.1': resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} @@ -1070,6 +1500,12 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/expect@4.0.18': resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} @@ -1099,6 +1535,15 @@ packages: '@vitest/utils@4.0.18': resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + '@xyflow/react@12.10.1': + resolution: {integrity: sha512-5eSWtIK/+rkldOuFbOOz44CRgQRjtS9v5nufk77DV+XBnfCGL9HAQ8PG00o2ZYKqkEU/Ak6wrKC95Tu+2zuK3Q==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + + '@xyflow/system@0.0.75': + resolution: {integrity: sha512-iXs+AGFLi8w/VlAoc/iSxk+CxfT6o64Uw/k0CKASOPqjqz6E0rb5jFZgJtXGZCpfQI6OQpu5EnumP5fGxQheaQ==} + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -1126,6 +1571,11 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.10.8: + resolution: {integrity: sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==} + engines: {node: '>=6.0.0'} + hasBin: true + better-sqlite3@11.10.0: resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==} @@ -1146,6 +1596,11 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -1165,6 +1620,9 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + caniuse-lite@1.0.30001780: + resolution: {integrity: sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -1175,6 +1633,9 @@ packages: citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + classcat@5.0.5: + resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} + consola@3.4.2: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} @@ -1187,6 +1648,9 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -1195,6 +1659,51 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1239,6 +1748,9 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.313: + resolution: {integrity: sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==} + empathic@2.0.0: resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} engines: {node: '>=14'} @@ -1250,6 +1762,10 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.20.1: + resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + engines: {node: '>=10.13.0'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -1265,11 +1781,20 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + esbuild@0.27.3: resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -1350,6 +1875,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-east-asian-width@1.5.0: resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} engines: {node: '>=18'} @@ -1376,6 +1905,9 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + grammy@1.41.1: resolution: {integrity: sha512-wcHAQ1e7svL3fJMpDchcQVcWUmywhuepOOjHUHmMmWAwUJEIyK5ea5sbSjZd+Gy1aMpZeP8VYJa+4tP+j1YptQ==} engines: {node: ^12.20.0 || >=14.13.1} @@ -1391,10 +1923,21 @@ packages: hookable@6.0.1: resolution: {integrity: sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw==} + html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + i18next@24.2.3: + resolution: {integrity: sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==} + peerDependencies: + typescript: ^5 + peerDependenciesMeta: + typescript: + optional: true + iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -1435,11 +1978,19 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} hasBin: true + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + knip@5.86.0: resolution: {integrity: sha512-tGpRCbP+L+VysXnAp1bHTLQ0k/SdC3M3oX18+Cpiqax1qdS25iuCPzpK8LVmAKARZv0Ijri81Wq09Rzk0JTl+Q==} engines: {node: '>=18.18.0'} @@ -1448,6 +1999,83 @@ packages: '@types/node': '>=18' typescript: '>=5.0.4 <7' + lightningcss-android-arm64@1.31.1: + resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.31.1: + resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.31.1: + resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.31.1: + resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.31.1: + resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.31.1: + resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.31.1: + resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.31.1: + resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.31.1: + resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.31.1: + resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.31.1: + resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.31.1: + resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} + engines: {node: '>= 12.0.0'} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1517,6 +2145,9 @@ packages: encoding: optional: true + node-releases@2.0.36: + resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -1609,6 +2240,45 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + peerDependencies: + react: ^19.2.4 + + react-i18next@15.7.4: + resolution: {integrity: sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==} + peerDependencies: + i18next: '>= 23.4.0' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + typescript: ^5 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + typescript: + optional: true + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-router@7.13.1: + resolution: {integrity: sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + engines: {node: '>=0.10.0'} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -1662,6 +2332,13 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} @@ -1675,6 +2352,9 @@ packages: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -1743,6 +2423,13 @@ packages: resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} engines: {node: '>=14.16'} + tailwindcss@4.2.1: + resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + tar-fs@2.1.4: resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} @@ -1856,6 +2543,17 @@ packages: synckit: optional: true + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -1863,19 +2561,19 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - vite@7.3.1: - resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} - engines: {node: ^20.19.0 || >=22.12.0} + vite@6.4.1: + resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 jiti: '>=1.21.0' - less: ^4.0.0 + less: '*' lightningcss: ^1.21.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' terser: ^5.16.0 tsx: ^4.8.1 yaml: ^2.4.2 @@ -1937,6 +2635,10 @@ packages: jsdom: optional: true + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + walk-up-path@4.0.0: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} @@ -1967,6 +2669,9 @@ packages: utf-8-validate: optional: true + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.8.2: resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} @@ -1978,7 +2683,76 @@ packages: zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} -snapshots: + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + + zustand@5.0.12: + resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + +snapshots: + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 '@babel/generator@8.0.0-rc.2': dependencies: @@ -1989,14 +2763,92 @@ snapshots: '@types/jsesc': 2.5.1 jsesc: 3.1.0 + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-string-parser@8.0.0-rc.2': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@8.0.0-rc.2': {} + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + '@babel/parser@8.0.0-rc.2': dependencies: '@babel/types': 8.0.0-rc.2 + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/runtime@7.29.2': {} + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/types@8.0.0-rc.2': dependencies: '@babel/helper-string-parser': 8.0.0-rc.2 @@ -2029,81 +2881,159 @@ snapshots: tslib: 2.8.1 optional: true + '@esbuild/aix-ppc64@0.25.12': + optional: true + '@esbuild/aix-ppc64@0.27.3': optional: true + '@esbuild/android-arm64@0.25.12': + optional: true + '@esbuild/android-arm64@0.27.3': optional: true + '@esbuild/android-arm@0.25.12': + optional: true + '@esbuild/android-arm@0.27.3': optional: true + '@esbuild/android-x64@0.25.12': + optional: true + '@esbuild/android-x64@0.27.3': optional: true + '@esbuild/darwin-arm64@0.25.12': + optional: true + '@esbuild/darwin-arm64@0.27.3': optional: true + '@esbuild/darwin-x64@0.25.12': + optional: true + '@esbuild/darwin-x64@0.27.3': optional: true + '@esbuild/freebsd-arm64@0.25.12': + optional: true + '@esbuild/freebsd-arm64@0.27.3': optional: true + '@esbuild/freebsd-x64@0.25.12': + optional: true + '@esbuild/freebsd-x64@0.27.3': optional: true + '@esbuild/linux-arm64@0.25.12': + optional: true + '@esbuild/linux-arm64@0.27.3': optional: true + '@esbuild/linux-arm@0.25.12': + optional: true + '@esbuild/linux-arm@0.27.3': optional: true + '@esbuild/linux-ia32@0.25.12': + optional: true + '@esbuild/linux-ia32@0.27.3': optional: true + '@esbuild/linux-loong64@0.25.12': + optional: true + '@esbuild/linux-loong64@0.27.3': optional: true + '@esbuild/linux-mips64el@0.25.12': + optional: true + '@esbuild/linux-mips64el@0.27.3': optional: true + '@esbuild/linux-ppc64@0.25.12': + optional: true + '@esbuild/linux-ppc64@0.27.3': optional: true + '@esbuild/linux-riscv64@0.25.12': + optional: true + '@esbuild/linux-riscv64@0.27.3': optional: true + '@esbuild/linux-s390x@0.25.12': + optional: true + '@esbuild/linux-s390x@0.27.3': optional: true + '@esbuild/linux-x64@0.25.12': + optional: true + '@esbuild/linux-x64@0.27.3': optional: true + '@esbuild/netbsd-arm64@0.25.12': + optional: true + '@esbuild/netbsd-arm64@0.27.3': optional: true + '@esbuild/netbsd-x64@0.25.12': + optional: true + '@esbuild/netbsd-x64@0.27.3': optional: true + '@esbuild/openbsd-arm64@0.25.12': + optional: true + '@esbuild/openbsd-arm64@0.27.3': optional: true + '@esbuild/openbsd-x64@0.25.12': + optional: true + '@esbuild/openbsd-x64@0.27.3': optional: true + '@esbuild/openharmony-arm64@0.25.12': + optional: true + '@esbuild/openharmony-arm64@0.27.3': optional: true + '@esbuild/sunos-x64@0.25.12': + optional: true + '@esbuild/sunos-x64@0.27.3': optional: true + '@esbuild/win32-arm64@0.25.12': + optional: true + '@esbuild/win32-arm64@0.27.3': optional: true + '@esbuild/win32-ia32@0.25.12': + optional: true + '@esbuild/win32-ia32@0.27.3': optional: true + '@esbuild/win32-x64@0.25.12': + optional: true + '@esbuild/win32-x64@0.27.3': optional: true @@ -2114,6 +3044,11 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} @@ -2371,6 +3306,8 @@ snapshots: '@rolldown/binding-win32-x64-msvc@1.0.0-rc.8': optional: true + '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rolldown/pluginutils@1.0.0-rc.8': {} '@rollup/rollup-android-arm-eabi@4.59.0': @@ -2450,11 +3387,100 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@tailwindcss/node@4.2.1': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.20.1 + jiti: 2.6.1 + lightningcss: 1.31.1 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.1 + + '@tailwindcss/oxide-android-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide@4.2.1': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-x64': 4.2.1 + '@tailwindcss/oxide-freebsd-x64': 4.2.1 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.1 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.1 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-x64-musl': 4.2.1 + '@tailwindcss/oxide-wasm32-wasi': 4.2.1 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.1 + + '@tailwindcss/vite@4.2.1(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@tailwindcss/node': 4.2.1 + '@tailwindcss/oxide': 4.2.1 + tailwindcss: 4.2.1 + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 optional: true + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + '@types/better-sqlite3@7.6.13': dependencies: '@types/node': 22.19.15 @@ -2473,6 +3499,27 @@ snapshots: dependencies: '@types/node': 22.19.15 + '@types/d3-color@3.1.3': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.8': {} @@ -2502,6 +3549,14 @@ snapshots: '@types/range-parser@1.2.7': {} + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + '@types/send@1.2.1': dependencies: '@types/node': 22.19.15 @@ -2515,6 +3570,18 @@ snapshots: dependencies: '@types/node': 22.19.15 + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + transitivePeerDependencies: + - supports-color + '@vitest/expect@4.0.18': dependencies: '@standard-schema/spec': 1.1.0 @@ -2524,13 +3591,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@4.0.18(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) '@vitest/pretty-format@4.0.18': dependencies: @@ -2554,6 +3621,29 @@ snapshots: '@vitest/pretty-format': 4.0.18 tinyrainbow: 3.0.3 + '@xyflow/react@12.10.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@xyflow/system': 0.0.75 + classcat: 5.0.5 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + zustand: 4.5.7(@types/react@19.2.14)(react@19.2.4) + transitivePeerDependencies: + - '@types/react' + - immer + + '@xyflow/system@0.0.75': + dependencies: + '@types/d3-drag': 3.0.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -2577,6 +3667,8 @@ snapshots: base64-js@1.5.1: {} + baseline-browser-mapping@2.10.8: {} + better-sqlite3@11.10.0: dependencies: bindings: 1.5.0 @@ -2612,6 +3704,14 @@ snapshots: dependencies: fill-range: 7.1.1 + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.10.8 + caniuse-lite: 1.0.30001780 + electron-to-chromium: 1.5.313 + node-releases: 2.0.36 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -2631,6 +3731,8 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + caniuse-lite@1.0.30001780: {} + chai@6.2.2: {} chownr@1.1.4: {} @@ -2639,16 +3741,60 @@ snapshots: dependencies: consola: 3.4.2 + classcat@5.0.5: {} + consola@3.4.2: {} content-disposition@1.0.1: {} content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} cookie@0.7.2: {} + cookie@1.1.1: {} + + csstype@3.2.3: {} + + d3-color@3.1.0: {} + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-ease@3.0.1: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-selection@3.0.0: {} + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + debug@4.4.3: dependencies: ms: 2.1.3 @@ -2677,6 +3823,8 @@ snapshots: ee-first@1.1.1: {} + electron-to-chromium@1.5.313: {} + empathic@2.0.0: {} encodeurl@2.0.0: {} @@ -2685,6 +3833,11 @@ snapshots: dependencies: once: 1.4.0 + enhanced-resolve@5.20.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -2695,6 +3848,35 @@ snapshots: dependencies: es-errors: 1.3.0 + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + esbuild@0.27.3: optionalDependencies: '@esbuild/aix-ppc64': 0.27.3 @@ -2724,6 +3906,8 @@ snapshots: '@esbuild/win32-ia32': 0.27.3 '@esbuild/win32-x64': 0.27.3 + escalade@3.2.0: {} + escape-html@1.0.3: {} estree-walker@3.0.3: @@ -2823,6 +4007,8 @@ snapshots: function-bind@1.1.2: {} + gensync@1.0.0-beta.2: {} + get-east-asian-width@1.5.0: {} get-intrinsic@1.3.0: @@ -2855,6 +4041,8 @@ snapshots: gopd@1.2.0: {} + graceful-fs@4.2.11: {} + grammy@1.41.1: dependencies: '@grammyjs/types': 3.25.0 @@ -2873,6 +4061,10 @@ snapshots: hookable@6.0.1: {} + html-parse-stringify@3.0.1: + dependencies: + void-elements: 3.1.0 + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -2881,6 +4073,12 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + i18next@24.2.3(typescript@5.9.3): + dependencies: + '@babel/runtime': 7.29.2 + optionalDependencies: + typescript: 5.9.3 + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -2907,8 +4105,12 @@ snapshots: jiti@2.6.1: {} + js-tokens@4.0.0: {} + jsesc@3.1.0: {} + json5@2.2.3: {} + knip@5.86.0(@types/node@22.19.15)(typescript@5.9.3): dependencies: '@nodelib/fs.walk': 1.2.8 @@ -2927,6 +4129,59 @@ snapshots: yaml: 2.8.2 zod: 4.3.6 + lightningcss-android-arm64@1.31.1: + optional: true + + lightningcss-darwin-arm64@1.31.1: + optional: true + + lightningcss-darwin-x64@1.31.1: + optional: true + + lightningcss-freebsd-x64@1.31.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.31.1: + optional: true + + lightningcss-linux-arm64-gnu@1.31.1: + optional: true + + lightningcss-linux-arm64-musl@1.31.1: + optional: true + + lightningcss-linux-x64-gnu@1.31.1: + optional: true + + lightningcss-linux-x64-musl@1.31.1: + optional: true + + lightningcss-win32-arm64-msvc@1.31.1: + optional: true + + lightningcss-win32-x64-msvc@1.31.1: + optional: true + + lightningcss@1.31.1: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.31.1 + lightningcss-darwin-arm64: 1.31.1 + lightningcss-darwin-x64: 1.31.1 + lightningcss-freebsd-x64: 1.31.1 + lightningcss-linux-arm-gnueabihf: 1.31.1 + lightningcss-linux-arm64-gnu: 1.31.1 + lightningcss-linux-arm64-musl: 1.31.1 + lightningcss-linux-x64-gnu: 1.31.1 + lightningcss-linux-x64-musl: 1.31.1 + lightningcss-win32-arm64-msvc: 1.31.1 + lightningcss-win32-x64-msvc: 1.31.1 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2972,6 +4227,8 @@ snapshots: dependencies: whatwg-url: 5.0.0 + node-releases@2.0.36: {} + object-inspect@1.13.4: {} obug@2.1.1: {} @@ -3120,6 +4377,33 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 + react-dom@19.2.4(react@19.2.4): + dependencies: + react: 19.2.4 + scheduler: 0.27.0 + + react-i18next@15.7.4(i18next@24.2.3(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3): + dependencies: + '@babel/runtime': 7.29.2 + html-parse-stringify: 3.0.1 + i18next: 24.2.3(typescript@5.9.3) + react: 19.2.4 + optionalDependencies: + react-dom: 19.2.4(react@19.2.4) + typescript: 5.9.3 + + react-refresh@0.17.0: {} + + react-router@7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + cookie: 1.1.1 + react: 19.2.4 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.4(react@19.2.4) + + react@19.2.4: {} + readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -3217,6 +4501,10 @@ snapshots: safer-buffer@2.1.2: {} + scheduler@0.27.0: {} + + semver@6.3.1: {} + semver@7.7.4: {} send@1.2.1: @@ -3244,6 +4532,8 @@ snapshots: transitivePeerDependencies: - supports-color + set-cookie-parser@2.7.2: {} + setprototypeof@1.2.0: {} side-channel-list@1.0.0: @@ -3313,6 +4603,10 @@ snapshots: strip-json-comments@5.0.3: {} + tailwindcss@4.2.1: {} + + tapable@2.3.0: {} + tar-fs@2.1.4: dependencies: chownr: 1.1.4 @@ -3415,13 +4709,23 @@ snapshots: dependencies: rolldown: 1.0.0-rc.8 + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-sync-external-store@1.6.0(react@19.2.4): + dependencies: + react: 19.2.4 + util-deprecate@1.0.2: {} vary@1.1.2: {} - vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2): + vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: - esbuild: 0.27.3 + esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.8 @@ -3431,13 +4735,14 @@ snapshots: '@types/node': 22.19.15 fsevents: 2.3.3 jiti: 2.6.1 + lightningcss: 1.31.1 tsx: 4.21.0 yaml: 2.8.2 - vitest@4.0.18(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2): + vitest@4.0.18(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 4.0.18(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -3454,7 +4759,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.15 @@ -3471,6 +4776,8 @@ snapshots: - tsx - yaml + void-elements@3.1.0: {} + walk-up-path@4.0.0: {} webidl-conversions@3.0.1: {} @@ -3489,8 +4796,23 @@ snapshots: ws@8.19.0: {} + yallist@3.1.1: {} + yaml@2.8.2: {} zod@3.25.76: {} zod@4.3.6: {} + + zustand@4.5.7(@types/react@19.2.14)(react@19.2.4): + dependencies: + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + react: 19.2.4 + + zustand@5.0.12(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): + optionalDependencies: + '@types/react': 19.2.14 + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) diff --git a/tsconfig.json b/tsconfig.json index ae3d389..2d071b1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ { "path": "packages/channels" }, { "path": "packages/ink" }, { "path": "packages/tentacle" }, - { "path": "packages/realmhub" } + { "path": "packages/realmhub" }, + { "path": "packages/dashboard" } ] } diff --git a/vitest.config.ts b/vitest.config.ts index 8de6439..075e05b 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -19,6 +19,7 @@ export default defineConfig({ "@openoctopus/ink": path.join(packages, "ink/src/index.ts"), "@openoctopus/tentacle": path.join(packages, "tentacle/src/index.ts"), "@openoctopus/realmhub": path.join(packages, "realmhub/src/index.ts"), + "@openoctopus/dashboard": path.join(packages, "dashboard/src/main.tsx"), }, }, test: {