@jagilber-org/index-server 1.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1218 -0
- package/CODE_OF_CONDUCT.md +49 -0
- package/CONTRIBUTING.md +75 -0
- package/LICENSE +21 -0
- package/README.md +523 -0
- package/SECURITY.md +50 -0
- package/dist/config/configUtils.d.ts +11 -0
- package/dist/config/configUtils.js +87 -0
- package/dist/config/dashboardConfig.d.ts +45 -0
- package/dist/config/dashboardConfig.js +63 -0
- package/dist/config/defaultValues.d.ts +61 -0
- package/dist/config/defaultValues.js +70 -0
- package/dist/config/dirConstants.d.ts +17 -0
- package/dist/config/dirConstants.js +28 -0
- package/dist/config/featureConfig.d.ts +61 -0
- package/dist/config/featureConfig.js +121 -0
- package/dist/config/runtimeConfig.d.ts +145 -0
- package/dist/config/runtimeConfig.js +334 -0
- package/dist/config/serverConfig.d.ts +90 -0
- package/dist/config/serverConfig.js +164 -0
- package/dist/dashboard/analytics/AnalyticsEngine.d.ts +142 -0
- package/dist/dashboard/analytics/AnalyticsEngine.js +373 -0
- package/dist/dashboard/analytics/BusinessIntelligence.d.ts +187 -0
- package/dist/dashboard/analytics/BusinessIntelligence.js +594 -0
- package/dist/dashboard/client/admin.html +2150 -0
- package/dist/dashboard/client/chunks/mermaid-layout-elk.esm.min/chunk-SP2CHFBE.mjs +1 -0
- package/dist/dashboard/client/chunks/mermaid-layout-elk.esm.min/render-T6MDALS3.mjs +27 -0
- package/dist/dashboard/client/css/admin.css +1466 -0
- package/dist/dashboard/client/js/admin.boot.js +359 -0
- package/dist/dashboard/client/js/admin.config.js +196 -0
- package/dist/dashboard/client/js/admin.embeddings.js +425 -0
- package/dist/dashboard/client/js/admin.graph.js +583 -0
- package/dist/dashboard/client/js/admin.instances.js +120 -0
- package/dist/dashboard/client/js/admin.instructions.js +552 -0
- package/dist/dashboard/client/js/admin.logs.js +113 -0
- package/dist/dashboard/client/js/admin.maintenance.js +354 -0
- package/dist/dashboard/client/js/admin.messaging.js +635 -0
- package/dist/dashboard/client/js/admin.monitor.js +181 -0
- package/dist/dashboard/client/js/admin.overview.js +221 -0
- package/dist/dashboard/client/js/admin.performance.js +61 -0
- package/dist/dashboard/client/js/admin.sessions.js +293 -0
- package/dist/dashboard/client/js/admin.sqlite.js +366 -0
- package/dist/dashboard/client/js/admin.utils.js +49 -0
- package/dist/dashboard/client/js/chart.umd.js +14 -0
- package/dist/dashboard/client/js/elk.bundled.js +6696 -0
- package/dist/dashboard/client/js/marked.umd.js +74 -0
- package/dist/dashboard/client/js/mermaid.min.js +3022 -0
- package/dist/dashboard/client/mermaid-layout-elk.esm.min.mjs +1 -0
- package/dist/dashboard/export/DataExporter.d.ts +169 -0
- package/dist/dashboard/export/DataExporter.js +737 -0
- package/dist/dashboard/export/exporters/csvExporter.d.ts +11 -0
- package/dist/dashboard/export/exporters/csvExporter.js +46 -0
- package/dist/dashboard/export/exporters/exportTypes.d.ts +89 -0
- package/dist/dashboard/export/exporters/exportTypes.js +5 -0
- package/dist/dashboard/export/exporters/jsonExporter.d.ts +7 -0
- package/dist/dashboard/export/exporters/jsonExporter.js +22 -0
- package/dist/dashboard/export/exporters/xmlExporter.d.ts +17 -0
- package/dist/dashboard/export/exporters/xmlExporter.js +175 -0
- package/dist/dashboard/integration/APIIntegration.d.ts +41 -0
- package/dist/dashboard/integration/APIIntegration.js +95 -0
- package/dist/dashboard/security/SecurityMonitor.d.ts +167 -0
- package/dist/dashboard/security/SecurityMonitor.js +559 -0
- package/dist/dashboard/server/AdminPanel.d.ts +183 -0
- package/dist/dashboard/server/AdminPanel.js +792 -0
- package/dist/dashboard/server/AdminPanelConfig.d.ts +42 -0
- package/dist/dashboard/server/AdminPanelConfig.js +80 -0
- package/dist/dashboard/server/AdminPanelState.d.ts +47 -0
- package/dist/dashboard/server/AdminPanelState.js +214 -0
- package/dist/dashboard/server/ApiRoutes.d.ts +17 -0
- package/dist/dashboard/server/ApiRoutes.js +149 -0
- package/dist/dashboard/server/DashboardServer.d.ts +49 -0
- package/dist/dashboard/server/DashboardServer.js +159 -0
- package/dist/dashboard/server/FileMetricsStorage.d.ts +49 -0
- package/dist/dashboard/server/FileMetricsStorage.js +195 -0
- package/dist/dashboard/server/HttpTransport.d.ts +23 -0
- package/dist/dashboard/server/HttpTransport.js +116 -0
- package/dist/dashboard/server/InstanceManager.d.ts +53 -0
- package/dist/dashboard/server/InstanceManager.js +284 -0
- package/dist/dashboard/server/KnowledgeStore.d.ts +35 -0
- package/dist/dashboard/server/KnowledgeStore.js +105 -0
- package/dist/dashboard/server/LeaderElection.d.ts +81 -0
- package/dist/dashboard/server/LeaderElection.js +268 -0
- package/dist/dashboard/server/MetricsCollector.d.ts +200 -0
- package/dist/dashboard/server/MetricsCollector.js +803 -0
- package/dist/dashboard/server/SessionPersistenceManager.d.ts +88 -0
- package/dist/dashboard/server/SessionPersistenceManager.js +457 -0
- package/dist/dashboard/server/ThinClient.d.ts +64 -0
- package/dist/dashboard/server/ThinClient.js +237 -0
- package/dist/dashboard/server/WebSocketManager.d.ts +161 -0
- package/dist/dashboard/server/WebSocketManager.js +463 -0
- package/dist/dashboard/server/httpLifecycle.d.ts +17 -0
- package/dist/dashboard/server/httpLifecycle.js +35 -0
- package/dist/dashboard/server/legacyDashboardHtml.d.ts +9 -0
- package/dist/dashboard/server/legacyDashboardHtml.js +618 -0
- package/dist/dashboard/server/legacyDashboardStyles.d.ts +5 -0
- package/dist/dashboard/server/legacyDashboardStyles.js +490 -0
- package/dist/dashboard/server/metricsAggregation.d.ts +252 -0
- package/dist/dashboard/server/metricsAggregation.js +206 -0
- package/dist/dashboard/server/metricsSerializer.d.ts +25 -0
- package/dist/dashboard/server/metricsSerializer.js +195 -0
- package/dist/dashboard/server/routes/admin.routes.d.ts +16 -0
- package/dist/dashboard/server/routes/admin.routes.js +596 -0
- package/dist/dashboard/server/routes/alerts.routes.d.ts +7 -0
- package/dist/dashboard/server/routes/alerts.routes.js +93 -0
- package/dist/dashboard/server/routes/api.feedback.routes.d.ts +73 -0
- package/dist/dashboard/server/routes/api.feedback.routes.js +171 -0
- package/dist/dashboard/server/routes/api.instructions.routes.d.ts +101 -0
- package/dist/dashboard/server/routes/api.instructions.routes.js +213 -0
- package/dist/dashboard/server/routes/api.usage.routes.d.ts +57 -0
- package/dist/dashboard/server/routes/api.usage.routes.js +374 -0
- package/dist/dashboard/server/routes/embeddings.routes.d.ts +6 -0
- package/dist/dashboard/server/routes/embeddings.routes.js +246 -0
- package/dist/dashboard/server/routes/graph.routes.d.ts +6 -0
- package/dist/dashboard/server/routes/graph.routes.js +280 -0
- package/dist/dashboard/server/routes/index.d.ts +38 -0
- package/dist/dashboard/server/routes/index.js +194 -0
- package/dist/dashboard/server/routes/instances.routes.d.ts +6 -0
- package/dist/dashboard/server/routes/instances.routes.js +35 -0
- package/dist/dashboard/server/routes/instructions.routes.d.ts +8 -0
- package/dist/dashboard/server/routes/instructions.routes.js +336 -0
- package/dist/dashboard/server/routes/knowledge.routes.d.ts +6 -0
- package/dist/dashboard/server/routes/knowledge.routes.js +82 -0
- package/dist/dashboard/server/routes/logs.routes.d.ts +6 -0
- package/dist/dashboard/server/routes/logs.routes.js +164 -0
- package/dist/dashboard/server/routes/messaging.routes.d.ts +16 -0
- package/dist/dashboard/server/routes/messaging.routes.js +293 -0
- package/dist/dashboard/server/routes/metrics.routes.d.ts +10 -0
- package/dist/dashboard/server/routes/metrics.routes.js +346 -0
- package/dist/dashboard/server/routes/scripts.routes.d.ts +9 -0
- package/dist/dashboard/server/routes/scripts.routes.js +84 -0
- package/dist/dashboard/server/routes/sqlite.routes.d.ts +9 -0
- package/dist/dashboard/server/routes/sqlite.routes.js +569 -0
- package/dist/dashboard/server/routes/status.routes.d.ts +7 -0
- package/dist/dashboard/server/routes/status.routes.js +183 -0
- package/dist/dashboard/server/routes/synthetic.routes.d.ts +7 -0
- package/dist/dashboard/server/routes/synthetic.routes.js +195 -0
- package/dist/dashboard/server/routes/tools.routes.d.ts +6 -0
- package/dist/dashboard/server/routes/tools.routes.js +46 -0
- package/dist/dashboard/server/routes/usage.routes.d.ts +6 -0
- package/dist/dashboard/server/routes/usage.routes.js +25 -0
- package/dist/dashboard/server/wsInit.d.ts +16 -0
- package/dist/dashboard/server/wsInit.js +35 -0
- package/dist/externalClientLib.d.ts +1 -0
- package/dist/externalClientLib.js +2 -0
- package/dist/minimal/index.d.ts +1 -0
- package/dist/minimal/index.js +140 -0
- package/dist/models/SessionPersistence.d.ts +115 -0
- package/dist/models/SessionPersistence.js +66 -0
- package/dist/models/instruction.d.ts +45 -0
- package/dist/models/instruction.js +2 -0
- package/dist/perf/benchmark.d.ts +1 -0
- package/dist/perf/benchmark.js +50 -0
- package/dist/portableClientWrapper.d.ts +1 -0
- package/dist/portableClientWrapper.js +2 -0
- package/dist/schemas/index.d.ts +128 -0
- package/dist/schemas/index.js +371 -0
- package/dist/scripts/runPerformanceBaseline.d.ts +1 -0
- package/dist/scripts/runPerformanceBaseline.js +17 -0
- package/dist/server/handshakeManager.d.ts +25 -0
- package/dist/server/handshakeManager.js +472 -0
- package/dist/server/index-server.d.ts +56 -0
- package/dist/server/index-server.js +822 -0
- package/dist/server/registry.d.ts +44 -0
- package/dist/server/registry.js +236 -0
- package/dist/server/sdkServer.d.ts +8 -0
- package/dist/server/sdkServer.js +299 -0
- package/dist/server/shutdownGuard.d.ts +41 -0
- package/dist/server/shutdownGuard.js +52 -0
- package/dist/server/thin-client.d.ts +22 -0
- package/dist/server/thin-client.js +111 -0
- package/dist/server/transport.d.ts +41 -0
- package/dist/server/transport.js +312 -0
- package/dist/server/transportFactory.d.ts +21 -0
- package/dist/server/transportFactory.js +429 -0
- package/dist/services/atomicFs.d.ts +22 -0
- package/dist/services/atomicFs.js +103 -0
- package/dist/services/auditLog.d.ts +38 -0
- package/dist/services/auditLog.js +142 -0
- package/dist/services/autoBackup.d.ts +14 -0
- package/dist/services/autoBackup.js +171 -0
- package/dist/services/autoSplit.d.ts +32 -0
- package/dist/services/autoSplit.js +113 -0
- package/dist/services/backupZip.d.ts +25 -0
- package/dist/services/backupZip.js +110 -0
- package/dist/services/bootstrapGating.d.ts +123 -0
- package/dist/services/bootstrapGating.js +221 -0
- package/dist/services/canonical.d.ts +23 -0
- package/dist/services/canonical.js +65 -0
- package/dist/services/categoryRules.d.ts +7 -0
- package/dist/services/categoryRules.js +37 -0
- package/dist/services/classificationService.d.ts +42 -0
- package/dist/services/classificationService.js +168 -0
- package/dist/services/embeddingService.d.ts +62 -0
- package/dist/services/embeddingService.js +259 -0
- package/dist/services/errors.d.ts +22 -0
- package/dist/services/errors.js +31 -0
- package/dist/services/featureFlags.d.ts +25 -0
- package/dist/services/featureFlags.js +89 -0
- package/dist/services/features.d.ts +13 -0
- package/dist/services/features.js +35 -0
- package/dist/services/handlers/instructions.add.d.ts +1 -0
- package/dist/services/handlers/instructions.add.js +496 -0
- package/dist/services/handlers/instructions.groom.d.ts +1 -0
- package/dist/services/handlers/instructions.groom.js +523 -0
- package/dist/services/handlers/instructions.import.d.ts +1 -0
- package/dist/services/handlers/instructions.import.js +173 -0
- package/dist/services/handlers/instructions.patch.d.ts +1 -0
- package/dist/services/handlers/instructions.patch.js +167 -0
- package/dist/services/handlers/instructions.query.d.ts +163 -0
- package/dist/services/handlers/instructions.query.js +522 -0
- package/dist/services/handlers/instructions.reload.d.ts +1 -0
- package/dist/services/handlers/instructions.reload.js +13 -0
- package/dist/services/handlers/instructions.remove.d.ts +1 -0
- package/dist/services/handlers/instructions.remove.js +118 -0
- package/dist/services/handlers/instructions.shared.d.ts +31 -0
- package/dist/services/handlers/instructions.shared.js +124 -0
- package/dist/services/handlers.activation.d.ts +1 -0
- package/dist/services/handlers.activation.js +203 -0
- package/dist/services/handlers.bootstrap.d.ts +1 -0
- package/dist/services/handlers.bootstrap.js +38 -0
- package/dist/services/handlers.dashboardConfig.d.ts +34 -0
- package/dist/services/handlers.dashboardConfig.js +108 -0
- package/dist/services/handlers.diagnostics.d.ts +1 -0
- package/dist/services/handlers.diagnostics.js +64 -0
- package/dist/services/handlers.feedback.d.ts +15 -0
- package/dist/services/handlers.feedback.js +378 -0
- package/dist/services/handlers.gates.d.ts +1 -0
- package/dist/services/handlers.gates.js +46 -0
- package/dist/services/handlers.graph.d.ts +53 -0
- package/dist/services/handlers.graph.js +231 -0
- package/dist/services/handlers.help.d.ts +1 -0
- package/dist/services/handlers.help.js +119 -0
- package/dist/services/handlers.instructionSchema.d.ts +1 -0
- package/dist/services/handlers.instructionSchema.js +227 -0
- package/dist/services/handlers.instructions.d.ts +8 -0
- package/dist/services/handlers.instructions.js +14 -0
- package/dist/services/handlers.instructionsDiagnostics.d.ts +1 -0
- package/dist/services/handlers.instructionsDiagnostics.js +14 -0
- package/dist/services/handlers.integrity.d.ts +1 -0
- package/dist/services/handlers.integrity.js +35 -0
- package/dist/services/handlers.manifest.d.ts +1 -0
- package/dist/services/handlers.manifest.js +24 -0
- package/dist/services/handlers.messaging.d.ts +12 -0
- package/dist/services/handlers.messaging.js +203 -0
- package/dist/services/handlers.metrics.d.ts +1 -0
- package/dist/services/handlers.metrics.js +43 -0
- package/dist/services/handlers.promote.d.ts +1 -0
- package/dist/services/handlers.promote.js +306 -0
- package/dist/services/handlers.prompt.d.ts +1 -0
- package/dist/services/handlers.prompt.js +7 -0
- package/dist/services/handlers.search.d.ts +69 -0
- package/dist/services/handlers.search.js +645 -0
- package/dist/services/handlers.testPrimitive.d.ts +1 -0
- package/dist/services/handlers.testPrimitive.js +5 -0
- package/dist/services/handlers.trace.d.ts +1 -0
- package/dist/services/handlers.trace.js +31 -0
- package/dist/services/handlers.usage.d.ts +1 -0
- package/dist/services/handlers.usage.js +11 -0
- package/dist/services/hotScore.d.ts +137 -0
- package/dist/services/hotScore.js +244 -0
- package/dist/services/indexContext.d.ts +117 -0
- package/dist/services/indexContext.js +968 -0
- package/dist/services/indexLoader.d.ts +44 -0
- package/dist/services/indexLoader.js +921 -0
- package/dist/services/indexRepository.d.ts +32 -0
- package/dist/services/indexRepository.js +71 -0
- package/dist/services/indexingService.d.ts +1 -0
- package/dist/services/indexingService.js +2 -0
- package/dist/services/instructions.dispatcher.d.ts +1 -0
- package/dist/services/instructions.dispatcher.js +231 -0
- package/dist/services/logPrefix.d.ts +1 -0
- package/dist/services/logPrefix.js +30 -0
- package/dist/services/logger.d.ts +52 -0
- package/dist/services/logger.js +268 -0
- package/dist/services/manifestManager.d.ts +82 -0
- package/dist/services/manifestManager.js +200 -0
- package/dist/services/messaging/agentMailbox.d.ts +60 -0
- package/dist/services/messaging/agentMailbox.js +353 -0
- package/dist/services/messaging/messagingPersistence.d.ts +20 -0
- package/dist/services/messaging/messagingPersistence.js +111 -0
- package/dist/services/messaging/messagingTypes.d.ts +150 -0
- package/dist/services/messaging/messagingTypes.js +66 -0
- package/dist/services/ownershipService.d.ts +1 -0
- package/dist/services/ownershipService.js +38 -0
- package/dist/services/performanceBaseline.d.ts +19 -0
- package/dist/services/performanceBaseline.js +210 -0
- package/dist/services/preflight.d.ts +12 -0
- package/dist/services/preflight.js +79 -0
- package/dist/services/promptReviewService.d.ts +44 -0
- package/dist/services/promptReviewService.js +101 -0
- package/dist/services/responseEnvelope.d.ts +6 -0
- package/dist/services/responseEnvelope.js +25 -0
- package/dist/services/seedBootstrap.d.ts +34 -0
- package/dist/services/seedBootstrap.js +427 -0
- package/dist/services/storage/factory.d.ts +17 -0
- package/dist/services/storage/factory.js +35 -0
- package/dist/services/storage/hashUtils.d.ts +11 -0
- package/dist/services/storage/hashUtils.js +35 -0
- package/dist/services/storage/index.d.ts +12 -0
- package/dist/services/storage/index.js +18 -0
- package/dist/services/storage/jsonFileStore.d.ts +32 -0
- package/dist/services/storage/jsonFileStore.js +241 -0
- package/dist/services/storage/migrationEngine.d.ts +35 -0
- package/dist/services/storage/migrationEngine.js +93 -0
- package/dist/services/storage/sqliteMessageStore.d.ts +53 -0
- package/dist/services/storage/sqliteMessageStore.js +146 -0
- package/dist/services/storage/sqliteSchema.d.ts +12 -0
- package/dist/services/storage/sqliteSchema.js +122 -0
- package/dist/services/storage/sqliteStore.d.ts +41 -0
- package/dist/services/storage/sqliteStore.js +339 -0
- package/dist/services/storage/sqliteUsageStore.d.ts +35 -0
- package/dist/services/storage/sqliteUsageStore.js +94 -0
- package/dist/services/storage/types.d.ts +171 -0
- package/dist/services/storage/types.js +12 -0
- package/dist/services/toolHandlers.d.ts +23 -0
- package/dist/services/toolHandlers.js +50 -0
- package/dist/services/toolRegistry.d.ts +20 -0
- package/dist/services/toolRegistry.js +490 -0
- package/dist/services/toolRegistry.zod.d.ts +10 -0
- package/dist/services/toolRegistry.zod.js +323 -0
- package/dist/services/tracing.d.ts +26 -0
- package/dist/services/tracing.js +260 -0
- package/dist/services/usageBuckets.d.ts +161 -0
- package/dist/services/usageBuckets.js +364 -0
- package/dist/services/validationService.d.ts +38 -0
- package/dist/services/validationService.js +125 -0
- package/dist/utils/BufferRing.d.ts +203 -0
- package/dist/utils/BufferRing.js +551 -0
- package/dist/utils/BufferRingExamples.d.ts +55 -0
- package/dist/utils/BufferRingExamples.js +188 -0
- package/dist/utils/envUtils.d.ts +42 -0
- package/dist/utils/envUtils.js +80 -0
- package/dist/utils/memoryMonitor.d.ts +83 -0
- package/dist/utils/memoryMonitor.js +275 -0
- package/dist/versioning/schemaVersion.d.ts +6 -0
- package/dist/versioning/schemaVersion.js +93 -0
- package/package.json +134 -0
- package/schemas/README.md +13 -0
- package/schemas/feedback-entry.schema.json +27 -0
- package/schemas/graph-export-v2.schema.json +60 -0
- package/schemas/index-server.code-schema.json +38477 -0
- package/schemas/instruction.schema.json +262 -0
- package/schemas/json-schema/SessionPersistence-persisted-admin-session.schema.json +54 -0
- package/schemas/json-schema/SessionPersistence-persisted-session-history-entry.schema.json +51 -0
- package/schemas/json-schema/SessionPersistence-persisted-web-socket-connection.schema.json +54 -0
- package/schemas/json-schema/SessionPersistence-session-persistence-config.schema.json +110 -0
- package/schemas/json-schema/SessionPersistence-session-persistence-data.schema.json +229 -0
- package/schemas/json-schema/SessionPersistence-session-persistence-manifest.schema.json +109 -0
- package/schemas/json-schema/SessionPersistence-session-persistence-metadata.schema.json +55 -0
- package/schemas/json-schema/instruction-audience-scope.schema.json +14 -0
- package/schemas/json-schema/instruction-content-type.schema.json +17 -0
- package/schemas/json-schema/instruction-instruction-entry.schema.json +206 -0
- package/schemas/json-schema/instruction-requirement-level.schema.json +16 -0
- package/schemas/manifest.json +78 -0
- package/schemas/manifest.schema.json +33 -0
- package/schemas/usage-batch.schema.json +16 -0
- package/schemas/usage-buckets.schema.json +30 -0
- package/schemas/usage-event.schema.json +17 -0
- package/scripts/copy-dashboard-assets.mjs +170 -0
- package/scripts/setup-hooks.cjs +28 -0
|
@@ -0,0 +1,2150 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta name="dashboard-build-version" content="1.19.1-e48e1b0d">
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Index Server Admin</title>
|
|
8
|
+
<link rel="stylesheet" href="css/admin.css?v=1.19.1-e48e1b0d">
|
|
9
|
+
<script defer src="js/admin.utils.js?v=1.19.1-e48e1b0d"></script>
|
|
10
|
+
<script defer src="js/admin.overview.js?v=1.19.1-e48e1b0d"></script>
|
|
11
|
+
<script defer src="js/admin.sessions.js?v=1.19.1-e48e1b0d"></script>
|
|
12
|
+
<script defer src="js/admin.monitor.js?v=1.19.1-e48e1b0d"></script>
|
|
13
|
+
<script defer src="js/admin.graph.js?v=1.19.1-e48e1b0d"></script>
|
|
14
|
+
<script defer src="js/marked.umd.js"></script>
|
|
15
|
+
<script defer src="js/admin.instructions.js?v=1.19.1-e48e1b0d"></script>
|
|
16
|
+
<script defer src="js/admin.logs.js?v=1.19.1-e48e1b0d"></script>
|
|
17
|
+
<script defer src="js/admin.maintenance.js?v=1.19.1-e48e1b0d"></script>
|
|
18
|
+
<script defer src="js/admin.config.js?v=1.19.1-e48e1b0d"></script>
|
|
19
|
+
<script defer src="js/admin.performance.js?v=1.19.1-e48e1b0d"></script>
|
|
20
|
+
<script defer src="js/admin.instances.js?v=1.19.1-e48e1b0d"></script>
|
|
21
|
+
<script defer src="js/admin.embeddings.js?v=1.19.1-e48e1b0d"></script>
|
|
22
|
+
<script defer src="js/admin.messaging.js?v=1.19.1-e48e1b0d"></script>
|
|
23
|
+
<script defer src="js/admin.sqlite.js?v=1.19.1-e48e1b0d"></script>
|
|
24
|
+
<script defer src="js/admin.boot.js?v=1.19.1-e48e1b0d"></script>
|
|
25
|
+
</head>
|
|
26
|
+
<body>
|
|
27
|
+
<div class="admin-container admin-root">
|
|
28
|
+
<div class="admin-header">
|
|
29
|
+
<h1>Index Server</h1>
|
|
30
|
+
<span id="storage-badge" class="storage-badge" style="display:none;margin-left:8px;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;letter-spacing:0.5px"></span>
|
|
31
|
+
<div id="instances-widget" class="instances-widget" style="display:none;">
|
|
32
|
+
<button id="instances-badge" class="instances-badge" title="Running instances">
|
|
33
|
+
<span id="instances-count">1</span> instance<span id="instances-plural">s</span>
|
|
34
|
+
</button>
|
|
35
|
+
<div id="instances-dropdown" class="instances-dropdown hidden">
|
|
36
|
+
<div class="instances-dropdown-header">Running Instances</div>
|
|
37
|
+
<div id="instances-list" class="instances-list"></div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<div id="buildMeta" class="build-meta">Loading build metadata…</div>
|
|
41
|
+
<div class="admin-nav">
|
|
42
|
+
<!-- Added explicit data-section attributes so JS can reliably map buttons to sections after HTML refactor -->
|
|
43
|
+
<!-- Redundant inline onclick fallback keeps basic navigation working even if JS wiring changes -->
|
|
44
|
+
<button class="nav-btn active" data-section="overview" onclick="window.showSection && window.showSection('overview')">Overview</button>
|
|
45
|
+
<button class="nav-btn" data-section="config" onclick="window.showSection && window.showSection('config')">Configuration</button>
|
|
46
|
+
<button id="nav-sessions" class="nav-btn" data-section="sessions" onclick="window.showSection && window.showSection('sessions')">Sessions</button>
|
|
47
|
+
<button class="nav-btn" data-section="maintenance" onclick="window.showSection && window.showSection('maintenance')">Maintenance</button>
|
|
48
|
+
<button class="nav-btn" data-section="monitoring" onclick="window.showSection && window.showSection('monitoring')">Monitoring</button>
|
|
49
|
+
<button class="nav-btn" data-section="instructions" onclick="window.showSection && window.showSection('instructions')">Instructions</button>
|
|
50
|
+
<button class="nav-btn" data-section="graph" onclick="window.showSection && window.showSection('graph')">Graph</button>
|
|
51
|
+
<button class="nav-btn" data-section="embeddings" onclick="window.showSection && window.showSection('embeddings')">Embeddings</button>
|
|
52
|
+
<button class="nav-btn" data-section="messaging" onclick="window.showSection && window.showSection('messaging')">Messaging</button>
|
|
53
|
+
<button class="nav-btn" data-section="sqlite" id="nav-sqlite" onclick="window.showSection && window.showSection('sqlite')" style="display:none">SQLite</button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<!-- Overview Section -->
|
|
58
|
+
<div id="overview-section" class="admin-section">
|
|
59
|
+
<div class="admin-grid">
|
|
60
|
+
<div class="admin-card">
|
|
61
|
+
<div class="card-header">
|
|
62
|
+
<div class="card-icon">📊</div>
|
|
63
|
+
<div class="card-title">System Statistics <a class="doc-link" href="/api/docs/overview" target="_blank" title="Panel documentation">?</a></div>
|
|
64
|
+
</div>
|
|
65
|
+
<div id="system-stats" class="loading metrics-list">Loading system statistics...</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div class="admin-card">
|
|
69
|
+
<div class="card-header">
|
|
70
|
+
<div class="card-icon">💚</div>
|
|
71
|
+
<div class="card-title">System Health <a class="doc-link" href="/api/docs/overview" target="_blank" title="Panel documentation">?</a></div>
|
|
72
|
+
</div>
|
|
73
|
+
<div id="system-health" class="loading metrics-list">Loading system health...</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div class="admin-card">
|
|
77
|
+
<div class="card-header">
|
|
78
|
+
<div class="card-icon">⚡</div>
|
|
79
|
+
<div class="card-title">Performance <a class="doc-link" href="/api/docs/overview" target="_blank" title="Panel documentation">?</a></div>
|
|
80
|
+
</div>
|
|
81
|
+
<div id="performance-stats" class="loading metrics-list">Loading performance data...</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<!-- index Status merged into System Statistics card -->
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<!-- Individual Tool Metrics -->
|
|
88
|
+
<div class="admin-card mt-30">
|
|
89
|
+
<div class="card-header">
|
|
90
|
+
<div class="card-icon">🔧</div>
|
|
91
|
+
<div class="card-title">Individual Tool Call Metrics <a class="doc-link" href="/api/docs/overview" target="_blank" title="Panel documentation">?</a></div>
|
|
92
|
+
</div>
|
|
93
|
+
<div id="tool-metrics" class="loading">Loading tool metrics...</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<!-- Usage Signals Summary -->
|
|
97
|
+
<div class="admin-card mt-30">
|
|
98
|
+
<div class="card-header">
|
|
99
|
+
<div class="card-icon">📡</div>
|
|
100
|
+
<div class="card-title">Usage Signals <a class="doc-link" href="/api/docs/overview" target="_blank" title="Panel documentation">?</a></div>
|
|
101
|
+
</div>
|
|
102
|
+
<div id="usage-signals-panel" class="loading">Loading usage signals...</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
<!-- Graph Section -->
|
|
106
|
+
<div id="graph-section" class="admin-section hidden">
|
|
107
|
+
<div class="admin-card">
|
|
108
|
+
<div class="card-header">
|
|
109
|
+
<div class="card-icon">🗺️</div>
|
|
110
|
+
<div class="card-title">Instruction Relationship Graph <a class="doc-link" href="/api/docs/graph" target="_blank" title="Panel documentation">?</a></div>
|
|
111
|
+
</div>
|
|
112
|
+
<div id="graph-toolbar" class="graph-toolbar toolbar mb-lg">
|
|
113
|
+
<label class="block order-0">Sel Categories
|
|
114
|
+
<select id="drill-categories" multiple size="5" onchange="coordinatedFilterChanged()"></select>
|
|
115
|
+
</label>
|
|
116
|
+
<label class="block order-1">Sel Instructions
|
|
117
|
+
<select id="drill-instructions" multiple size="5" onchange="coordinatedFilterChanged()"></select>
|
|
118
|
+
</label>
|
|
119
|
+
<div class="stack order-2">
|
|
120
|
+
<button class="action-btn sm" title="Reload categories list" onclick="refreshDrillCategories()">🔄 Load Categories</button>
|
|
121
|
+
<button class="action-btn sm btn-info" title="Load instructions (all or filtered by selected categories)" onclick="loadDrillInstructions()">📥 Load Instructions</button>
|
|
122
|
+
<button class="action-btn sm btn-success-gradient" title="Clear selections" onclick="clearSelections()">🧹 Clear Selections</button>
|
|
123
|
+
</div>
|
|
124
|
+
<label class="block layout-label" title="Graph layout engine (elk = hierarchical; default = Mermaid native)"><span class="lbl-icon">🧩</span><span>Layout</span>
|
|
125
|
+
<select id="graph-layout" class="form-input pill">
|
|
126
|
+
<option value="elk" selected>elk</option>
|
|
127
|
+
<option value="default">default</option>
|
|
128
|
+
</select>
|
|
129
|
+
</label>
|
|
130
|
+
<div class="actions order-6">
|
|
131
|
+
<button id="graph-refresh-btn" class="action-btn btn-success-gradient" title="Refresh Mermaid diagram from current filters">🔄 Refresh Graph</button>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div id="graph-meta" class="graph-meta"></div>
|
|
136
|
+
<div id="graph-meta2" class="graph-meta2"></div>
|
|
137
|
+
|
|
138
|
+
<!-- Advanced Options (collapsed) -->
|
|
139
|
+
<details id="graph-advanced-options" class="graph-details">
|
|
140
|
+
<summary>⚙️ Advanced Options</summary>
|
|
141
|
+
<div class="graph-advanced-content">
|
|
142
|
+
<div class="flags">
|
|
143
|
+
<label class="chk simple"><input id="graph-enrich" type="checkbox" checked> Enrich</label>
|
|
144
|
+
<label class="chk simple"><input id="graph-categories" type="checkbox" checked> Categories</label>
|
|
145
|
+
<label class="chk simple"><input id="graph-usage" type="checkbox"> Usage</label>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="flags edge-layout-group">
|
|
148
|
+
<label class="block etypes-label" title="Filter specific edge types (comma separated). Leave blank for all."><span class="lbl-icon">🔗</span><span>Edge Types</span>
|
|
149
|
+
<input id="graph-edgeTypes" class="form-input pill" placeholder="all (comma list)" autocomplete="off">
|
|
150
|
+
</label>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="stack">
|
|
153
|
+
<label class="chk" title="Toggle high Mermaid maxEdges secure limit (forces re-init)"><input id="mermaid-high-edges" type="checkbox"> High edge cap</label>
|
|
154
|
+
<label class="chk" title="Enable Large Graph Mode (raises maxEdges & maxTextSize caps)"><input id="mermaid-large-graph" type="checkbox"> Large graph mode</label>
|
|
155
|
+
<label class="chk simple" title="Enable debug logging"><input id="graph-debug" type="checkbox"> Debug</label>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</details>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<!-- DIAGRAM FIRST -->
|
|
162
|
+
<div class="admin-card mt-xl" id="graph-render-card">
|
|
163
|
+
<div class="graph-zoom-controls">
|
|
164
|
+
<button id="graph-zoom-in" class="action-btn sm" title="Zoom in">➕</button>
|
|
165
|
+
<button id="graph-zoom-out" class="action-btn sm" title="Zoom out">➖</button>
|
|
166
|
+
<button id="graph-zoom-reset" class="action-btn sm" title="Reset zoom">🔍</button>
|
|
167
|
+
<button id="graph-fullscreen-btn" class="action-btn sm" title="Toggle fullscreen">⛶</button>
|
|
168
|
+
</div>
|
|
169
|
+
<div class="card-header">
|
|
170
|
+
<span class="card-icon">🧪</span>
|
|
171
|
+
<span class="card-title">Rendered Diagram <a class="doc-link" href="/api/docs/graph" target="_blank" title="Panel documentation">?</a></span>
|
|
172
|
+
</div>
|
|
173
|
+
<div id="graph-mermaid-rendered" class="graph-rendered">
|
|
174
|
+
<div class="graph-loading-skeleton">Loading diagram...</div>
|
|
175
|
+
<div id="graph-mermaid-svg" class="graph-svg-min">(diagram not loaded)</div>
|
|
176
|
+
</div>
|
|
177
|
+
<div class="graph-render-note">Copy source into <a href="https://mermaid.live" target="_blank" rel="noopener" class="mermaid-link">Mermaid Live Editor</a> for advanced tweaks. Layout 'elk' leverages experimental ELK engine; fallback auto-applies legacy init directive if frontmatter unsupported.</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<!-- SOURCE SECOND (collapsed) -->
|
|
181
|
+
<details id="graph-source-details" class="graph-details mt-xl">
|
|
182
|
+
<summary>📝 Mermaid Source</summary>
|
|
183
|
+
<div class="admin-card">
|
|
184
|
+
<div class="source-actions">
|
|
185
|
+
<button id="graph-copy-btn" class="action-btn btn-info" title="Copy Mermaid source to clipboard" onclick="window.copyMermaidSource && window.copyMermaidSource()">📋 Copy Source</button>
|
|
186
|
+
<button id="graph-edit-btn" class="action-btn btn-edit" title="Edit Mermaid source inline" onclick="window.toggleGraphEdit && window.toggleGraphEdit()">✏️ Edit Source</button>
|
|
187
|
+
<button id="graph-apply-btn" class="action-btn btn-success-gradient" style="display:none" title="Apply manual edits" onclick="window.applyGraphEdit && window.applyGraphEdit()">💾 Apply Edits</button>
|
|
188
|
+
<button id="graph-cancel-btn" class="action-btn danger" style="display:none;" title="Cancel manual edits" onclick="window.cancelGraphEdit && window.cancelGraphEdit()">↩️ Cancel Edit</button>
|
|
189
|
+
<label class="graph-auto-label"><input type="checkbox" id="graph-auto-render" checked> auto-render</label>
|
|
190
|
+
</div>
|
|
191
|
+
<div id="graph-mermaid-wrapper" class="graph-wrapper">
|
|
192
|
+
<pre id="graph-mermaid" class="graph-source-pre">(loading graph...)</pre>
|
|
193
|
+
</div>
|
|
194
|
+
<div class="graph-source-note">Mermaid source shown above. Rendered diagram above uses same code.</div>
|
|
195
|
+
</div>
|
|
196
|
+
</details>
|
|
197
|
+
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<!-- Configuration Section -->
|
|
201
|
+
<div id="config-section" class="admin-section hidden">
|
|
202
|
+
<div class="admin-card">
|
|
203
|
+
<div class="card-header">
|
|
204
|
+
<div class="card-icon">⚙️</div>
|
|
205
|
+
<div class="card-title">Server Configuration <a class="doc-link" href="/api/docs/config" target="_blank" title="Panel documentation">?</a></div>
|
|
206
|
+
</div>
|
|
207
|
+
<div id="config-form" class="loading config-loading">Loading configuration...</div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<!-- Sessions Section -->
|
|
212
|
+
<div id="sessions-section" class="admin-section hidden">
|
|
213
|
+
<div class="admin-grid grid-single-col">
|
|
214
|
+
<div class="admin-card">
|
|
215
|
+
<div class="card-header">
|
|
216
|
+
<div class="card-icon">👥</div>
|
|
217
|
+
<div class="card-title">Active Admin Sessions <a class="doc-link" href="/api/docs/sessions" target="_blank" title="Panel documentation">?</a></div>
|
|
218
|
+
</div>
|
|
219
|
+
<div id="sessions-list" class="loading index-list">Loading sessions...</div>
|
|
220
|
+
<div id="sessions-pagination" class="index-pagination sessions-pagination" style="display:none">
|
|
221
|
+
<button data-role="prev-page" class="action-btn page-btn" onclick="setSessionsPage(window.__sessionsPage-1)">Prev</button>
|
|
222
|
+
<div data-role="page-info" class="page-info">Page 1 / 1</div>
|
|
223
|
+
<button data-role="next-page" class="action-btn page-btn" onclick="setSessionsPage(window.__sessionsPage+1)">Next</button>
|
|
224
|
+
<label class="page-size-label">
|
|
225
|
+
Size
|
|
226
|
+
<select data-role="page-size" class="form-input page-size-select" onchange="changeSessionsPageSize(parseInt(this.value,10))">
|
|
227
|
+
<option value="10">10</option>
|
|
228
|
+
<option value="25" selected>25</option>
|
|
229
|
+
<option value="50">50</option>
|
|
230
|
+
<option value="100">100</option>
|
|
231
|
+
</select>
|
|
232
|
+
</label>
|
|
233
|
+
</div>
|
|
234
|
+
<div class="session-actions">
|
|
235
|
+
<button class="action-btn">Create Test Session</button>
|
|
236
|
+
<button class="action-btn">🔄 Refresh</button>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
<div class="admin-card mt-xxl">
|
|
240
|
+
<div class="card-header">
|
|
241
|
+
<div class="card-icon">🔌</div>
|
|
242
|
+
<div class="card-title">Active WebSocket Connections <a class="doc-link" href="/api/docs/sessions" target="_blank" title="Panel documentation">?</a></div>
|
|
243
|
+
</div>
|
|
244
|
+
<div id="connections-list" class="loading">Loading connections...</div>
|
|
245
|
+
</div>
|
|
246
|
+
<div class="admin-card mt-xxl">
|
|
247
|
+
<div class="card-header">
|
|
248
|
+
<div class="card-icon">🗂️</div>
|
|
249
|
+
<div class="card-title">Session History <a class="doc-link" href="/api/docs/sessions" target="_blank" title="Panel documentation">?</a></div>
|
|
250
|
+
</div>
|
|
251
|
+
<div class="flex-row mb-md">
|
|
252
|
+
<label class="form-label-sm">Limit
|
|
253
|
+
<select id="session-history-limit" class="form-input w-80" onchange="refreshSessionHistory()">
|
|
254
|
+
<option value="25">25</option>
|
|
255
|
+
<option value="50" selected>50</option>
|
|
256
|
+
<option value="100">100</option>
|
|
257
|
+
<option value="250">250</option>
|
|
258
|
+
</select>
|
|
259
|
+
</label>
|
|
260
|
+
<button class="action-btn">🔄 Refresh History</button>
|
|
261
|
+
</div>
|
|
262
|
+
<div id="session-history-list" class="loading index-list">History not loaded...</div>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
<!-- Maintenance Section -->
|
|
268
|
+
<div id="maintenance-section" class="admin-section hidden">
|
|
269
|
+
<div class="admin-grid">
|
|
270
|
+
<div class="admin-card">
|
|
271
|
+
<div class="card-header">
|
|
272
|
+
<div class="card-icon">🔧</div>
|
|
273
|
+
<div class="card-title">Maintenance Control <a class="doc-link" href="/api/docs/maintenance" target="_blank" title="Panel documentation">?</a></div>
|
|
274
|
+
</div>
|
|
275
|
+
<div id="maintenance-control" class="loading">Loading maintenance status...</div>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<div class="admin-card">
|
|
279
|
+
<div class="card-header">
|
|
280
|
+
<div class="card-icon">💾</div>
|
|
281
|
+
<div class="card-title">System Operations <a class="doc-link" href="/api/docs/maintenance" target="_blank" title="Panel documentation">?</a></div>
|
|
282
|
+
</div>
|
|
283
|
+
<div class="action-buttons sys-ops flex-row">
|
|
284
|
+
<button id="btn-create-backup" class="action-btn primary" data-op="create-backup" title="Create a new instruction + data backup">💾 Create Backup</button>
|
|
285
|
+
<button id="btn-clear-caches" class="action-btn warning" data-op="clear-caches" title="Clear in-memory caches (manifest, stats, etc.)">🗑️ Clear Caches</button>
|
|
286
|
+
<button id="btn-restart-server" class="action-btn danger" data-op="restart-server" title="Gracefully restart the server">🔄 Restart Server</button>
|
|
287
|
+
</div>
|
|
288
|
+
<div class="action-buttons sys-ops flex-row" style="margin-top:8px;">
|
|
289
|
+
<button class="action-btn success" onclick="exportBackupToFile()" title="Export the selected backup to a downloadable JSON file">📥 Backup to File</button>
|
|
290
|
+
<button class="action-btn primary" onclick="importBackupFromFile()" title="Import a backup from a JSON file">📤 Restore from File</button>
|
|
291
|
+
<input type="file" id="backup-file-input" accept=".json" style="display:none" onchange="handleBackupFileSelected(event)" />
|
|
292
|
+
</div>
|
|
293
|
+
<div class="mt-lg">
|
|
294
|
+
<div class="restore-header">Restore Backup <button class="action-btn page-btn font-sm">Refresh</button></div>
|
|
295
|
+
<div id="backup-restore-area" class="restore-area">
|
|
296
|
+
<select id="backup-select" class="form-input backup-select">
|
|
297
|
+
<option value="">(no backups)</option>
|
|
298
|
+
</select>
|
|
299
|
+
<button id="btn-restore-backup" class="action-btn success" data-op="restore-backup" title="Restore selected backup">♻️ Restore</button>
|
|
300
|
+
<span id="backup-restore-status" class="backup-status"></span>
|
|
301
|
+
</div>
|
|
302
|
+
<div id="backup-list-meta" class="backup-meta"></div>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
<!-- Signal Groom Card -->
|
|
307
|
+
<div class="admin-card">
|
|
308
|
+
<div class="card-header">
|
|
309
|
+
<div class="card-icon">📡</div>
|
|
310
|
+
<div class="card-title">Signal Feedback Groom</div>
|
|
311
|
+
</div>
|
|
312
|
+
<div style="padding:8px 0;font-size:12px;opacity:.7;">
|
|
313
|
+
Apply usage signals (helpful/applied/not-relevant/outdated) to instruction priority and requirement fields.
|
|
314
|
+
</div>
|
|
315
|
+
<div class="action-buttons sys-ops flex-row">
|
|
316
|
+
<button class="action-btn primary" onclick="runSignalGroom(true)">🔍 Dry Run</button>
|
|
317
|
+
<button class="action-btn warning" onclick="if(confirm('Apply signal feedback to instructions?')) runSignalGroom(false)">⚡ Apply Signals</button>
|
|
318
|
+
</div>
|
|
319
|
+
<div id="signal-groom-status" class="mt-sm" style="font-size:12px;"></div>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<!-- Monitoring Section -->
|
|
325
|
+
<div id="monitoring-section" class="admin-section hidden">
|
|
326
|
+
<div class="admin-card">
|
|
327
|
+
<div class="card-header">
|
|
328
|
+
<div class="card-icon"><svg viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 14 10 14 12 10 16 20 18 14 22 14"/><circle cx="6" cy="6" r="2"/></svg></div>
|
|
329
|
+
<div class="card-title">Real-time Monitoring <a class="doc-link" href="/api/docs/monitoring" target="_blank" title="Panel documentation">?</a></div>
|
|
330
|
+
</div>
|
|
331
|
+
<div id="monitoring-data" class="loading monitoring-loading">Loading monitoring data...</div>
|
|
332
|
+
</div>
|
|
333
|
+
<div class="admin-card mt-xxl">
|
|
334
|
+
<div class="card-header">
|
|
335
|
+
<div class="card-icon">🧪</div>
|
|
336
|
+
<div class="card-title">Synthetic Activity <a class="doc-link" href="/api/docs/monitoring" target="_blank" title="Panel documentation">?</a></div>
|
|
337
|
+
</div>
|
|
338
|
+
<div class="synthetic-form">
|
|
339
|
+
<div>
|
|
340
|
+
<label class="form-label form-label-sm">Iterations</label>
|
|
341
|
+
<input id="synthetic-iterations" class="form-input form-input-sm" type="number" value="25" />
|
|
342
|
+
</div>
|
|
343
|
+
<div>
|
|
344
|
+
<label class="form-label form-label-sm">Concurrency</label>
|
|
345
|
+
<input id="synthetic-concurrency" class="form-input form-input-sm" type="number" value="3" />
|
|
346
|
+
</div>
|
|
347
|
+
<button id="synthetic-run-btn" class="action-btn">Run Synthetic Activity</button>
|
|
348
|
+
<div id="synthetic-output" class="synthetic-output"></div>
|
|
349
|
+
</div>
|
|
350
|
+
<div class="synthetic-note">Executes random safe tools to exercise metrics & health. <span id="synthetic-last-meta" class="text-italic"></span></div>
|
|
351
|
+
<div id="synthetic-traces-wrapper" class="mt-lg" style="display:none">
|
|
352
|
+
<div class="trace-header">
|
|
353
|
+
Per‑Call Trace
|
|
354
|
+
<label class="trace-toggle-label">
|
|
355
|
+
<input id="synthetic-trace-toggle" type="checkbox" checked onchange="toggleSyntheticTraceVisibility()" /> show
|
|
356
|
+
</label>
|
|
357
|
+
</div>
|
|
358
|
+
<div class="trace-scroll">
|
|
359
|
+
<table class="trace-table">
|
|
360
|
+
<thead class="trace-thead">
|
|
361
|
+
<tr>
|
|
362
|
+
<th class="trace-th">#</th>
|
|
363
|
+
<th class="trace-th">Tool</th>
|
|
364
|
+
<th class="trace-th">Success</th>
|
|
365
|
+
<th class="trace-th">Duration</th>
|
|
366
|
+
<th class="trace-th">Error</th>
|
|
367
|
+
</tr>
|
|
368
|
+
</thead>
|
|
369
|
+
<tbody id="synthetic-traces-body"></tbody>
|
|
370
|
+
</table>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
<!-- Log Viewer Section -->
|
|
376
|
+
<div class="admin-card mt-xxl">
|
|
377
|
+
<div class="card-header">
|
|
378
|
+
<div class="card-icon">📋</div>
|
|
379
|
+
<div class="card-title">Server Logs <a class="doc-link" href="/api/docs/monitoring" target="_blank" title="Panel documentation">?</a></div>
|
|
380
|
+
</div>
|
|
381
|
+
<div class="log-controls">
|
|
382
|
+
<div>
|
|
383
|
+
<label class="form-label text-xs">Lines</label>
|
|
384
|
+
<input id="log-lines" class="form-input w-80" type="number" value="100" />
|
|
385
|
+
</div>
|
|
386
|
+
<button id="log-refresh-btn" class="action-btn" onclick="window.loadLogs && window.loadLogs()">🔄 Refresh</button>
|
|
387
|
+
<button id="log-tail-btn" class="action-btn" onclick="window.toggleLogTail && window.toggleLogTail()">▶️ Start Tail</button>
|
|
388
|
+
<button id="log-clear-btn" class="action-btn" onclick="window.clearLogViewer && window.clearLogViewer()">🗑️ Clear</button>
|
|
389
|
+
<div id="log-status" class="log-status-text"></div>
|
|
390
|
+
</div>
|
|
391
|
+
<!-- Log content container id expected by scripts (was log-viewer) -->
|
|
392
|
+
<div id="log-content" class="log-content-box">
|
|
393
|
+
<div class="log-placeholder">Click "Refresh" to load server logs...</div>
|
|
394
|
+
</div>
|
|
395
|
+
<div class="log-note">
|
|
396
|
+
Real-time server log viewer. Set INDEX_SERVER_LOG_FILE to a path OR simply '1' (auto => logs/mcp-server.log).
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
|
|
401
|
+
<!-- Embeddings Visualization Section -->
|
|
402
|
+
<div id="embeddings-section" class="admin-section hidden">
|
|
403
|
+
<div class="admin-card" style="padding:0;overflow:hidden">
|
|
404
|
+
<div class="emb-layout">
|
|
405
|
+
<!-- Sidebar -->
|
|
406
|
+
<div class="emb-sidebar">
|
|
407
|
+
<div class="emb-section">
|
|
408
|
+
<input id="emb-search" class="emb-search-box" type="text" placeholder="Search instruction IDs…" />
|
|
409
|
+
<div class="emb-controls">
|
|
410
|
+
<button class="emb-btn" data-emb-action="compute">Compute</button>
|
|
411
|
+
<button class="emb-btn" data-emb-action="load">Load</button>
|
|
412
|
+
<button class="emb-btn" data-emb-action="reset">Reset</button>
|
|
413
|
+
<button class="emb-btn" id="emb-norm-btn" data-emb-action="norm">Norm</button>
|
|
414
|
+
</div>
|
|
415
|
+
<span id="emb-status" class="emb-status-text"></span>
|
|
416
|
+
</div>
|
|
417
|
+
<div class="emb-section" id="emb-stats-section">
|
|
418
|
+
<div class="emb-section-title">Statistics</div>
|
|
419
|
+
<div id="emb-stats"></div>
|
|
420
|
+
</div>
|
|
421
|
+
<div class="emb-section">
|
|
422
|
+
<div class="emb-section-title">Categories</div>
|
|
423
|
+
<div id="emb-legend"></div>
|
|
424
|
+
</div>
|
|
425
|
+
<div class="emb-section">
|
|
426
|
+
<div class="emb-section-title">Most Similar Pairs</div>
|
|
427
|
+
<div id="emb-similar"></div>
|
|
428
|
+
</div>
|
|
429
|
+
<div class="emb-section">
|
|
430
|
+
<div class="emb-section-title">Selected Point</div>
|
|
431
|
+
<div id="emb-detail"><em style="color:var(--admin-text-dim)">Click a point to inspect</em></div>
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
<!-- Canvas area -->
|
|
435
|
+
<div class="emb-canvas-wrap">
|
|
436
|
+
<canvas id="embeddings-canvas"></canvas>
|
|
437
|
+
<div id="emb-tooltip" class="emb-tooltip"></div>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
|
|
443
|
+
<!-- Instruction Management Section -->
|
|
444
|
+
<div id="instructions-section" class="admin-section hidden">
|
|
445
|
+
<div class="admin-card">
|
|
446
|
+
<div class="card-header">
|
|
447
|
+
<div class="card-icon">📚</div>
|
|
448
|
+
<div class="card-title">Instruction index <a class="doc-link" href="/api/docs/instructions" target="_blank" title="Panel documentation">?</a></div>
|
|
449
|
+
</div>
|
|
450
|
+
<div class="instr-toolbar">
|
|
451
|
+
<button class="action-btn" onclick="loadInstructions()">🔄 Refresh</button>
|
|
452
|
+
<button class="action-btn" onclick="showCreateInstruction()">➕ Create</button>
|
|
453
|
+
<input id="instruction-filter" placeholder="Filter by name (supports regex)..." class="form-input instr-filter" oninput="filterInstructions()" />
|
|
454
|
+
<label class="regex-toggle" title="Enable regex matching"><input type="checkbox" id="instruction-regex-toggle" onchange="filterInstructions()" /><span class="regex-label">.*</span></label>
|
|
455
|
+
<select id="instruction-category-filter" class="form-input w-130" onchange="filterInstructions()">
|
|
456
|
+
<option value="">All Categories</option>
|
|
457
|
+
<!-- Categories will be populated dynamically -->
|
|
458
|
+
</select>
|
|
459
|
+
<select id="instruction-size-filter" class="form-input w-120" onchange="filterInstructions()">
|
|
460
|
+
<option value="">All Sizes</option>
|
|
461
|
+
<option value="small">small</option>
|
|
462
|
+
<option value="medium">medium</option>
|
|
463
|
+
<option value="large">large</option>
|
|
464
|
+
</select>
|
|
465
|
+
<select id="instruction-sort" class="form-input w-150" onchange="filterInstructions()">
|
|
466
|
+
<option value="name-asc">Name A-Z</option>
|
|
467
|
+
<option value="name-desc">Name Z-A</option>
|
|
468
|
+
<option value="size-asc">Size ↑</option>
|
|
469
|
+
<option value="size-desc">Size ↓</option>
|
|
470
|
+
<option value="mtime-desc">Modified ↓</option>
|
|
471
|
+
<option value="mtime-asc">Modified ↑</option>
|
|
472
|
+
<option value="category">Category</option>
|
|
473
|
+
<option value="usage-desc">Usage ↓</option>
|
|
474
|
+
<option value="signal">Signal</option>
|
|
475
|
+
</select>
|
|
476
|
+
</div>
|
|
477
|
+
<!-- Global Search Row -->
|
|
478
|
+
<div class="instr-search-row">
|
|
479
|
+
<input id="instruction-global-search" placeholder="Global search (id, title, body, categories — supports regex)..." class="form-input instr-global" />
|
|
480
|
+
<label class="regex-toggle" title="Enable regex matching for global search"><input type="checkbox" id="instruction-global-regex-toggle" /><span class="regex-label">.*</span></label>
|
|
481
|
+
<button id="instruction-global-search-btn" class="action-btn btn-purple" onclick="(function(){ try { if(window.performGlobalInstructionSearch){ const v = document.getElementById('instruction-global-search')?.value||''; window.performGlobalInstructionSearch(v); } } catch(e){ console.warn('inline global search fallback failed', e); } })();">🌍 Search All</button>
|
|
482
|
+
<span class="text-dim">Fallback substring search across all instruction files.</span>
|
|
483
|
+
</div>
|
|
484
|
+
<div id="instruction-global-results" class="instr-results"></div>
|
|
485
|
+
<div id="instructions-list" class="loading">Loading instructions...</div>
|
|
486
|
+
<div id="instruction-pagination" class="mt-md"></div>
|
|
487
|
+
<!-- Editor will be dynamically repositioned above the list when activated -->
|
|
488
|
+
<div id="instruction-editor" class="hidden mt-lg">
|
|
489
|
+
<h3 id="instruction-editor-title" class="mb-sm">New Instruction</h3>
|
|
490
|
+
<div class="form-group">
|
|
491
|
+
<label class="form-label">File Name (no extension)</label>
|
|
492
|
+
<input id="instruction-filename" class="form-input" placeholder="example-instruction" />
|
|
493
|
+
</div>
|
|
494
|
+
<div class="form-group mt-sm">
|
|
495
|
+
<label class="form-label">JSON Content</label>
|
|
496
|
+
<textarea id="instruction-content" class="form-input instr-textarea" oninput="updateInstructionEditorDiagnostics()" placeholder="{\n \"id\": \"example\",\n \"title\": \"Example Instruction\"\n}"></textarea>
|
|
497
|
+
<div id="instruction-diagnostics" class="instr-diagnostics">
|
|
498
|
+
<em>Editor idle.</em>
|
|
499
|
+
</div>
|
|
500
|
+
<div id="instruction-preview-container" class="hidden instr-preview-wrap">
|
|
501
|
+
<div class="instr-preview-header">
|
|
502
|
+
<span class="instr-preview-label">Body Preview (Markdown)</span>
|
|
503
|
+
</div>
|
|
504
|
+
<div id="instruction-preview" class="instr-preview-content"></div>
|
|
505
|
+
</div>
|
|
506
|
+
<div id="instruction-diff-container" class="hidden instr-diff-wrap">
|
|
507
|
+
<div class="instr-diff-label">Diff (original vs current)</div>
|
|
508
|
+
<pre id="instruction-diff" class="instr-diff-pre"></pre>
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
<div class="instr-actions">
|
|
512
|
+
<button id="instruction-save-btn" class="action-btn" onclick="saveInstruction()">💾 Save</button>
|
|
513
|
+
<button id="instruction-preview-btn" class="action-btn btn-green" onclick="toggleInstructionPreview()">📖 Preview</button>
|
|
514
|
+
<button id="instruction-format-btn" class="action-btn btn-info" onclick="formatInstructionJson()">🧹 Format</button>
|
|
515
|
+
<button id="instruction-diff-btn" class="action-btn btn-purple" onclick="toggleInstructionDiff()">🔍 Diff</button>
|
|
516
|
+
<button id="instruction-template-btn" class="action-btn btn-teal" onclick="applyInstructionTemplate()">📐 Template</button>
|
|
517
|
+
<button id="instruction-cancel-btn" class="action-btn warning" onclick="cancelEditInstruction()">✖ Cancel</button>
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
|
|
523
|
+
<!-- Messaging Section -->
|
|
524
|
+
<div id="messaging-section" class="admin-section hidden">
|
|
525
|
+
<div class="admin-card">
|
|
526
|
+
<div class="card-header">
|
|
527
|
+
<div class="card-icon">💬</div>
|
|
528
|
+
<div class="card-title">Inter-Agent Messaging</div>
|
|
529
|
+
</div>
|
|
530
|
+
<div class="msg-layout">
|
|
531
|
+
<!-- Sidebar -->
|
|
532
|
+
<div class="msg-sidebar">
|
|
533
|
+
<div style="padding:0 12px 8px;font-size:14px;font-weight:600;color:var(--admin-text)">Messages <span id="msg-summary" style="font-weight:400;font-size:12px;color:var(--admin-text-dim)"></span></div>
|
|
534
|
+
<div class="msg-sidebar-heading">CHANNELS</div>
|
|
535
|
+
<div id="messaging-channel-list"></div>
|
|
536
|
+
<div class="msg-sidebar-heading" style="margin-top:12px">SENDERS</div>
|
|
537
|
+
<div id="messaging-sender-list"></div>
|
|
538
|
+
</div>
|
|
539
|
+
<!-- Main content -->
|
|
540
|
+
<div class="msg-main">
|
|
541
|
+
<!-- Toolbar -->
|
|
542
|
+
<div class="msg-toolbar">
|
|
543
|
+
<input id="msg-search" type="text" placeholder="Search messages…" class="form-input"
|
|
544
|
+
data-action="filter-input" />
|
|
545
|
+
<select id="msg-priority-filter" data-action="filter-priority">
|
|
546
|
+
<option value="">Priority</option>
|
|
547
|
+
<option value="critical">Critical</option>
|
|
548
|
+
<option value="high">High</option>
|
|
549
|
+
<option value="normal">Normal</option>
|
|
550
|
+
<option value="low">Low</option>
|
|
551
|
+
</select>
|
|
552
|
+
<select id="msg-sort" data-action="sort">
|
|
553
|
+
<option value="newest">Newest</option>
|
|
554
|
+
<option value="oldest">Oldest</option>
|
|
555
|
+
<option value="priority">Priority</option>
|
|
556
|
+
</select>
|
|
557
|
+
<div class="msg-toolbar-group">
|
|
558
|
+
<button class="msg-toggle-btn active" id="msg-view-list" data-action="view-mode" data-mode="list">List</button>
|
|
559
|
+
<button class="msg-toggle-btn" id="msg-view-timeline" data-action="view-mode" data-mode="timeline">Timeline</button>
|
|
560
|
+
</div>
|
|
561
|
+
<label style="display:flex;align-items:center;gap:4px;font-size:12px;color:var(--admin-text-dim);cursor:pointer;white-space:nowrap">
|
|
562
|
+
<input type="checkbox" id="msg-select-all" data-action="select-all" style="accent-color:var(--admin-accent)"> Select All
|
|
563
|
+
</label>
|
|
564
|
+
<span class="msg-toolbar-count" id="msg-count"></span>
|
|
565
|
+
<button class="action-btn" data-action="refresh">🔄 Refresh</button>
|
|
566
|
+
<button class="action-btn" data-action="download">⬇ Download</button>
|
|
567
|
+
</div>
|
|
568
|
+
<!-- Message list -->
|
|
569
|
+
<div id="messaging-message-list"></div>
|
|
570
|
+
<div id="messaging-pagination" class="msg-pagination"></div>
|
|
571
|
+
</div>
|
|
572
|
+
</div>
|
|
573
|
+
<!-- Compose -->
|
|
574
|
+
<div class="msg-compose">
|
|
575
|
+
<h4>Compose Message</h4>
|
|
576
|
+
<div class="msg-compose-row">
|
|
577
|
+
<input id="msg-compose-channel" type="text" placeholder="Channel" class="form-input" />
|
|
578
|
+
<input id="msg-compose-sender" type="text" placeholder="Sender (default: dashboard)" class="form-input" />
|
|
579
|
+
<input id="msg-compose-recipients" type="text" placeholder="Recipients (* = broadcast)" class="form-input" value="*" />
|
|
580
|
+
</div>
|
|
581
|
+
<div class="msg-compose-row">
|
|
582
|
+
<input id="msg-compose-tags" type="text" placeholder="Tags (comma-separated)" class="form-input" />
|
|
583
|
+
<select id="msg-compose-priority" class="form-input" style="max-width:140px">
|
|
584
|
+
<option value="normal">Normal</option>
|
|
585
|
+
<option value="low">Low</option>
|
|
586
|
+
<option value="high">High</option>
|
|
587
|
+
<option value="critical">Critical</option>
|
|
588
|
+
</select>
|
|
589
|
+
</div>
|
|
590
|
+
<textarea id="msg-compose-body" placeholder="Message body…" class="form-input" rows="3" style="width:100%;resize:vertical"></textarea>
|
|
591
|
+
<button class="action-btn" style="margin-top:8px" data-action="send">📤 Send</button>
|
|
592
|
+
</div>
|
|
593
|
+
</div>
|
|
594
|
+
<div id="messaging-detail"></div>
|
|
595
|
+
</div>
|
|
596
|
+
|
|
597
|
+
<!-- SQLite Section -->
|
|
598
|
+
<div id="sqlite-section" class="admin-section hidden">
|
|
599
|
+
<div class="admin-grid">
|
|
600
|
+
<div class="admin-card">
|
|
601
|
+
<div class="card-header">
|
|
602
|
+
<div class="card-icon">🗄️</div>
|
|
603
|
+
<div class="card-title">Database Info</div>
|
|
604
|
+
</div>
|
|
605
|
+
<div id="sqlite-info" class="loading metrics-list">Loading database info…</div>
|
|
606
|
+
</div>
|
|
607
|
+
<div class="admin-card">
|
|
608
|
+
<div class="card-header">
|
|
609
|
+
<div class="card-icon">📊</div>
|
|
610
|
+
<div class="card-title">Table Statistics</div>
|
|
611
|
+
</div>
|
|
612
|
+
<div id="sqlite-tables" class="metrics-list">—</div>
|
|
613
|
+
</div>
|
|
614
|
+
<div class="admin-card">
|
|
615
|
+
<div class="card-header">
|
|
616
|
+
<div class="card-icon">🔧</div>
|
|
617
|
+
<div class="card-title">Maintenance</div>
|
|
618
|
+
</div>
|
|
619
|
+
<div class="sqlite-maintenance-btns" style="display:flex;gap:8px;flex-wrap:wrap;padding:8px 0">
|
|
620
|
+
<button class="action-btn" data-sqlite-action="vacuum">🧹 VACUUM</button>
|
|
621
|
+
<button class="action-btn" data-sqlite-action="optimize">⚡ Optimize FTS5</button>
|
|
622
|
+
<button class="action-btn" data-sqlite-action="integrity">✅ Integrity Check</button>
|
|
623
|
+
<button class="action-btn" data-sqlite-action="analyze">📈 ANALYZE</button>
|
|
624
|
+
<button class="action-btn" data-sqlite-action="reindex">🔄 REINDEX</button>
|
|
625
|
+
<button class="action-btn" data-sqlite-action="groom">🧽 Groom</button>
|
|
626
|
+
</div>
|
|
627
|
+
<div id="sqlite-maintenance-result" style="margin-top:8px;font-size:13px"></div>
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
630
|
+
<div class="admin-grid" style="margin-top:16px">
|
|
631
|
+
<div class="admin-card">
|
|
632
|
+
<div class="card-header">
|
|
633
|
+
<div class="card-icon">💾</div>
|
|
634
|
+
<div class="card-title">Backup & Restore</div>
|
|
635
|
+
</div>
|
|
636
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap;padding:8px 0">
|
|
637
|
+
<button class="action-btn" data-sqlite-action="backup">📦 Create Backup</button>
|
|
638
|
+
<button class="action-btn" data-sqlite-action="wal-checkpoint">📝 WAL Checkpoint</button>
|
|
639
|
+
</div>
|
|
640
|
+
<div id="sqlite-backup-result" style="margin-top:8px;font-size:13px"></div>
|
|
641
|
+
<div style="margin-top:12px">
|
|
642
|
+
<div style="font-weight:600;font-size:13px;color:var(--admin-text,#e2e8f0);margin-bottom:6px">Available Backups</div>
|
|
643
|
+
<div id="sqlite-backups-list" style="font-size:13px;color:var(--admin-text-dim,#94a3b8)">Loading…</div>
|
|
644
|
+
</div>
|
|
645
|
+
</div>
|
|
646
|
+
<div class="admin-card">
|
|
647
|
+
<div class="card-header">
|
|
648
|
+
<div class="card-icon">🔄</div>
|
|
649
|
+
<div class="card-title">Migration & Recovery</div>
|
|
650
|
+
</div>
|
|
651
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap;padding:8px 0">
|
|
652
|
+
<button class="action-btn" data-sqlite-action="migrate">📥 Import from JSON</button>
|
|
653
|
+
<button class="action-btn" data-sqlite-action="export">📤 Export to JSON</button>
|
|
654
|
+
<button class="action-btn" data-sqlite-action="reset" style="background:#7f1d1d;border-color:#ef4444">🗑️ Reset Database</button>
|
|
655
|
+
</div>
|
|
656
|
+
<div id="sqlite-migration-result" style="margin-top:8px;font-size:13px"></div>
|
|
657
|
+
</div>
|
|
658
|
+
</div>
|
|
659
|
+
<div class="admin-card" style="margin-top:16px">
|
|
660
|
+
<div class="card-header">
|
|
661
|
+
<div class="card-icon">🔍</div>
|
|
662
|
+
<div class="card-title">Query Console</div>
|
|
663
|
+
</div>
|
|
664
|
+
<div style="padding:8px 0">
|
|
665
|
+
<textarea id="sqlite-query-input" class="form-input" rows="4" style="width:100%;font-family:monospace;font-size:13px;resize:vertical" placeholder="SELECT * FROM instructions LIMIT 10"></textarea>
|
|
666
|
+
<div style="display:flex;gap:8px;margin-top:8px;align-items:center">
|
|
667
|
+
<button class="action-btn" data-sqlite-action="run-query">▶ Run Query</button>
|
|
668
|
+
<span id="sqlite-query-status" style="font-size:12px;color:var(--admin-text-dim)"></span>
|
|
669
|
+
</div>
|
|
670
|
+
</div>
|
|
671
|
+
<div id="sqlite-query-result" style="margin-top:8px;overflow-x:auto"></div>
|
|
672
|
+
</div>
|
|
673
|
+
</div>
|
|
674
|
+
</div>
|
|
675
|
+
|
|
676
|
+
<script>
|
|
677
|
+
// Admin Panel JavaScript
|
|
678
|
+
let currentSection = 'overview';
|
|
679
|
+
let refreshInterval;
|
|
680
|
+
// Track if /api/admin/stats responded successfully on most recent loadOverviewData()
|
|
681
|
+
// Used to downgrade health display (memory/errors) when stats are missing.
|
|
682
|
+
// NOTE: use window-scoped flag so extracted overview script and inline health renderer share state
|
|
683
|
+
// Previously a local 'let statsAvailable' caused displaySystemHealth() to always treat statistics as unavailable
|
|
684
|
+
// which produced a persistent 'Statistics unavailable' issue despite stats loading successfully.
|
|
685
|
+
window.statsAvailable = false;
|
|
686
|
+
// WebSocket for live events (metrics + synthetic trace streaming)
|
|
687
|
+
let dashboardSocket = null;
|
|
688
|
+
let lastSyntheticRunId = null;
|
|
689
|
+
function initDashboardSocket(){
|
|
690
|
+
try {
|
|
691
|
+
if (dashboardSocket && dashboardSocket.readyState === WebSocket.OPEN) return;
|
|
692
|
+
const proto = (location.protocol === 'https:') ? 'wss' : 'ws';
|
|
693
|
+
dashboardSocket = new WebSocket(`${proto}://${location.host}/ws`);
|
|
694
|
+
dashboardSocket.onopen = ()=>{/* noop */};
|
|
695
|
+
dashboardSocket.onmessage = ev => {
|
|
696
|
+
try {
|
|
697
|
+
const msg = JSON.parse(ev.data);
|
|
698
|
+
if (msg.type === 'synthetic_trace' && msg.data) {
|
|
699
|
+
handleSyntheticTrace(msg.data);
|
|
700
|
+
}
|
|
701
|
+
} catch {/* ignore */}
|
|
702
|
+
};
|
|
703
|
+
dashboardSocket.onclose = ()=>{ setTimeout(initDashboardSocket, 4000); };
|
|
704
|
+
} catch(e){ console.warn('ws init failed', e); }
|
|
705
|
+
}
|
|
706
|
+
function handleSyntheticTrace(data){
|
|
707
|
+
if (!data || !data.runId) return;
|
|
708
|
+
// If new run starts while traces visible, auto-clear
|
|
709
|
+
if (lastSyntheticRunId && data.runId !== lastSyntheticRunId) {
|
|
710
|
+
const body = document.getElementById('synthetic-traces-body');
|
|
711
|
+
if (body) body.innerHTML='';
|
|
712
|
+
}
|
|
713
|
+
lastSyntheticRunId = data.runId;
|
|
714
|
+
const body = document.getElementById('synthetic-traces-body');
|
|
715
|
+
if (!body) return;
|
|
716
|
+
const clr = data.success ? '#0a0' : '#a00';
|
|
717
|
+
const err = data.error ? String(data.error).slice(0,80) : '';
|
|
718
|
+
const skipped = data.skipped ? ' (skipped)' : '';
|
|
719
|
+
const tr = document.createElement('tr');
|
|
720
|
+
tr.innerHTML = `<td class="trace-cell">${data.seq}</td>
|
|
721
|
+
<td class="trace-cell trace-mono">${data.method}${skipped}</td>
|
|
722
|
+
<td class="trace-cell ${data.success ? 'text-ok' : 'text-fail'}">${data.success?'✓':'✗'}</td>
|
|
723
|
+
<td class="trace-cell">${data.durationMs}ms</td>
|
|
724
|
+
<td class="trace-cell ${data.error ? 'text-err' : 'text-muted'}">${err}</td>`;
|
|
725
|
+
body.appendChild(tr);
|
|
726
|
+
const wrap = document.getElementById('synthetic-traces-wrapper');
|
|
727
|
+
if (wrap && wrap.style.display === 'none') wrap.style.display='block';
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Initialize admin panel
|
|
731
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
732
|
+
showSection('overview');
|
|
733
|
+
startAutoRefresh();
|
|
734
|
+
// Attempt to auto-create a dashboard admin session if none exists for this browser tab
|
|
735
|
+
try { maybeEnsureAdminSession(); } catch(e) { console.warn('auto session create failed', e); }
|
|
736
|
+
initDashboardSocket();
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
function showSection(section) {
|
|
740
|
+
// Hide all sections
|
|
741
|
+
document.querySelectorAll('.admin-section').forEach(s => s.classList.add('hidden'));
|
|
742
|
+
|
|
743
|
+
// Show selected section if present
|
|
744
|
+
const activeSection = document.getElementById(section + '-section');
|
|
745
|
+
if (activeSection) activeSection.classList.remove('hidden');
|
|
746
|
+
|
|
747
|
+
// Update nav buttons without relying on implicit event
|
|
748
|
+
document.querySelectorAll('.nav-btn').forEach(btn => {
|
|
749
|
+
const isTarget = btn.getAttribute('onclick')?.includes(`showSection('${section}')`);
|
|
750
|
+
if (isTarget) {
|
|
751
|
+
btn.classList.add('active');
|
|
752
|
+
} else {
|
|
753
|
+
btn.classList.remove('active');
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
currentSection = section;
|
|
758
|
+
loadSectionData(section);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function loadSectionData(section) {
|
|
762
|
+
switch(section) {
|
|
763
|
+
case 'overview':
|
|
764
|
+
loadOverviewData();
|
|
765
|
+
break;
|
|
766
|
+
case 'graph':
|
|
767
|
+
initGraphScopeDefaults();
|
|
768
|
+
break;
|
|
769
|
+
case 'config':
|
|
770
|
+
loadConfiguration();
|
|
771
|
+
break;
|
|
772
|
+
case 'sessions':
|
|
773
|
+
loadSessions();
|
|
774
|
+
break;
|
|
775
|
+
case 'maintenance':
|
|
776
|
+
loadMaintenanceStatus();
|
|
777
|
+
loadBackups();
|
|
778
|
+
break;
|
|
779
|
+
case 'monitoring':
|
|
780
|
+
loadMonitoringData();
|
|
781
|
+
ensureMonitoringPoll();
|
|
782
|
+
break;
|
|
783
|
+
case 'instructions':
|
|
784
|
+
loadInstructions();
|
|
785
|
+
break;
|
|
786
|
+
case 'messaging':
|
|
787
|
+
if (window.initMessaging) window.initMessaging();
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Graph logic was extracted to js/admin.graph.js?v=1.19.1-e48e1b0d
|
|
793
|
+
// Functions available globally: reloadGraphMermaid, initGraphScopeDefaults, copyMermaidSource, toggleGraphEdit, applyGraphEdit, cancelGraphEdit, refreshDrillCategories, loadDrillInstructions, clearSelections
|
|
794
|
+
|
|
795
|
+
<!-- overview functions moved to js/admin.overview.js?v=1.19.1-e48e1b0d -->
|
|
796
|
+
|
|
797
|
+
// Lightweight overview-level maintenance display (optional)
|
|
798
|
+
// Intentionally minimal to avoid blocking overview rendering.
|
|
799
|
+
// If an element with id 'maintenance-overview' exists, populate it; otherwise no-op.
|
|
800
|
+
function displayMaintenanceInfo(maintenance) {
|
|
801
|
+
try {
|
|
802
|
+
const el = document.getElementById('maintenance-overview');
|
|
803
|
+
if (!el || !maintenance) return; // Safe no-op if overview element not present
|
|
804
|
+
const mode = maintenance.maintenanceMode ? 'ENABLED' : 'Disabled';
|
|
805
|
+
el.innerHTML = `
|
|
806
|
+
<div class="stat-row">
|
|
807
|
+
<span class="stat-label">Last Backup</span>
|
|
808
|
+
<span class="stat-value">${maintenance.lastBackup ? new Date(maintenance.lastBackup).toLocaleString() : 'Never'}</span>
|
|
809
|
+
</div>`;
|
|
810
|
+
} catch (err) {
|
|
811
|
+
console.warn('displayMaintenanceInfo error:', err);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function displaySystemHealth(health) {
|
|
816
|
+
// Defensive normalization: /api/health returns { status, checks, uptime, timestamp } (no issues/recommendations)
|
|
817
|
+
// while richer maintenance/system health objects may include arrays. Avoid assuming presence.
|
|
818
|
+
if(!health || typeof health !== 'object') {
|
|
819
|
+
document.getElementById('system-health').innerHTML = '<div class="error-message">Health data unavailable</div>';
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const normalized = {
|
|
823
|
+
status: (health.status || 'unknown').toString(),
|
|
824
|
+
issues: Array.isArray(health.issues) ? health.issues : [],
|
|
825
|
+
recommendations: Array.isArray(health.recommendations) ? health.recommendations : [],
|
|
826
|
+
uptime: typeof health.uptime === 'number' ? health.uptime : (typeof health.server?.uptime === 'number' ? health.server.uptime : undefined),
|
|
827
|
+
checks: health.checks || {}
|
|
828
|
+
};
|
|
829
|
+
// Inject CPU check if not present using last resource cache / stats
|
|
830
|
+
try {
|
|
831
|
+
if(!normalized.checks.cpu){
|
|
832
|
+
let cpuVal = undefined;
|
|
833
|
+
if(window.__resourceTrendCache && typeof window.__resourceTrendCache.latestCpu === 'number') cpuVal = window.__resourceTrendCache.latestCpu;
|
|
834
|
+
else if(window.lastSystemStats && window.lastSystemStats.cpuUsage && typeof window.lastSystemStats.cpuUsage.percent==='number') cpuVal = window.lastSystemStats.cpuUsage.percent;
|
|
835
|
+
if(typeof cpuVal === 'number'){
|
|
836
|
+
// simple thresholds: <50 ok, 50-80 warn (ok), >80 fail
|
|
837
|
+
normalized.checks.cpu = cpuVal < 85; // treat >85 as fail
|
|
838
|
+
if(cpuVal >=85 && !normalized.issues.some(i=>/high cpu/i.test(i))) normalized.issues.push('High CPU usage');
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
// Inject Memory check if not present (heap usage based) using resource trend cache or lastSystemStats
|
|
842
|
+
if(!normalized.checks.memory){
|
|
843
|
+
let memPercent = undefined;
|
|
844
|
+
// Use heapLimit (V8 heap_size_limit) when available for accurate memory health.
|
|
845
|
+
// heapTotal tracks current V8 heap reservation which stays close to heapUsed, causing false alarms.
|
|
846
|
+
const mu = window.lastSystemStats && window.lastSystemStats.memoryUsage;
|
|
847
|
+
if(mu && typeof mu.heapUsed === 'number' && typeof mu.heapLimit === 'number' && mu.heapLimit > 0){
|
|
848
|
+
memPercent = (mu.heapUsed / mu.heapLimit) * 100;
|
|
849
|
+
} else if(mu && typeof mu.heapUsed === 'number' && typeof mu.heapTotal === 'number' && mu.heapTotal > 0){
|
|
850
|
+
memPercent = (mu.heapUsed / mu.heapTotal) * 100;
|
|
851
|
+
}
|
|
852
|
+
if(typeof memPercent === 'number'){
|
|
853
|
+
// Thresholds: <70 ok, 70-90 warn (still ok), >90 fail
|
|
854
|
+
normalized.checks.memory = memPercent < 90;
|
|
855
|
+
if(memPercent >= 90 && !normalized.issues.some(i=>/high memory/i.test(i))) normalized.issues.push('High memory usage');
|
|
856
|
+
// Provide a leak / growth recommendation if increasing trend and above moderate threshold
|
|
857
|
+
if(health.memoryTrend === 'increasing' && memPercent >= 75 && !normalized.recommendations.some(r=>/investigate memory growth/i.test(r))){
|
|
858
|
+
normalized.recommendations.push('Investigate memory growth – potential leak risk');
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
} catch{/*ignore*/}
|
|
863
|
+
// Uptime regression styling escalation: if an issue contains 'Uptime regression' force critical indicator
|
|
864
|
+
let statusOverride = normalized.status;
|
|
865
|
+
if (normalized.issues.some(i => /uptime regression/i.test(i))) {
|
|
866
|
+
statusOverride = 'critical';
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// If stats are unavailable, degrade overall status (but don't alarm) – rely on shared window.statsAvailable
|
|
870
|
+
if (!window.statsAvailable) {
|
|
871
|
+
// degrade to 'unknown' instead of forced failure to avoid alarming UI when stats temporarily missing
|
|
872
|
+
if (!normalized.issues.some(i => /statistics unavailable/i.test(i))) normalized.issues.push('Statistics unavailable');
|
|
873
|
+
if (statusOverride === 'healthy') statusOverride = 'unknown';
|
|
874
|
+
}
|
|
875
|
+
const statusClass = `status-${statusOverride}`;
|
|
876
|
+
let html = `
|
|
877
|
+
<div class="stat-row">
|
|
878
|
+
<span class="stat-label">Overall Status</span>
|
|
879
|
+
<span class="stat-value">
|
|
880
|
+
${statusOverride.toUpperCase()}
|
|
881
|
+
<span class="${statusClass} status-indicator"></span>
|
|
882
|
+
</span>
|
|
883
|
+
</div>
|
|
884
|
+
`;
|
|
885
|
+
|
|
886
|
+
// Show basic check breakdown if present
|
|
887
|
+
try {
|
|
888
|
+
const checkKeys = Object.keys(normalized.checks);
|
|
889
|
+
if(checkKeys.length){
|
|
890
|
+
html += '<div class="health-mt"><strong>Checks:</strong><ul class="health-list">' +
|
|
891
|
+
checkKeys.map(k => `<li class="${normalized.checks[k]?'text-ok':'text-fail'}">${k}: ${normalized.checks[k]?'ok':'fail'}</li>`).join('') + '</ul></div>';
|
|
892
|
+
}
|
|
893
|
+
} catch { /* ignore */ }
|
|
894
|
+
|
|
895
|
+
// Add CPU trend information if available
|
|
896
|
+
if (health.cpuTrend) {
|
|
897
|
+
html += `
|
|
898
|
+
<div class="health-mt">
|
|
899
|
+
<strong>CPU Trend:</strong>
|
|
900
|
+
<span class="${health.cpuTrend === 'stable' ? 'text-ok' : health.cpuTrend === 'increasing' ? 'text-warn' : 'text-fail'}">
|
|
901
|
+
${health.cpuTrend}
|
|
902
|
+
</span>
|
|
903
|
+
</div>
|
|
904
|
+
`;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Add memory trend information if available
|
|
908
|
+
if (health.memoryTrend) {
|
|
909
|
+
const formatGrowthRate = (rate) => {
|
|
910
|
+
if (Math.abs(rate) < 1024) return `${rate.toFixed(0)} B/min`;
|
|
911
|
+
if (Math.abs(rate) < 1024 * 1024) return `${(rate / 1024).toFixed(1)} KB/min`;
|
|
912
|
+
return `${(rate / (1024 * 1024)).toFixed(1)} MB/min`;
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
html += `
|
|
916
|
+
<div class="health-mt">
|
|
917
|
+
<strong>Memory Trend:</strong>
|
|
918
|
+
<span class="${health.memoryTrend === 'stable' ? 'text-ok' : health.memoryTrend === 'increasing' ? 'text-warn' : 'text-fail'}">
|
|
919
|
+
${health.memoryTrend}
|
|
920
|
+
</span>
|
|
921
|
+
${health.memoryGrowthRate ? ` (${formatGrowthRate(health.memoryGrowthRate)})` : ''}
|
|
922
|
+
</div>
|
|
923
|
+
`;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (normalized.issues.length > 0) {
|
|
927
|
+
html += `
|
|
928
|
+
<div class="health-mt-lg">
|
|
929
|
+
<strong>Issues:</strong>
|
|
930
|
+
<ul class="health-list">
|
|
931
|
+
${normalized.issues.map(issue => `<li class="text-fail">${issue}</li>`).join('')}
|
|
932
|
+
</ul>
|
|
933
|
+
</div>
|
|
934
|
+
`;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (normalized.recommendations.length > 0) {
|
|
938
|
+
html += `
|
|
939
|
+
<div class="health-mt-lg">
|
|
940
|
+
<strong>Recommendations:</strong>
|
|
941
|
+
<ul class="health-list">
|
|
942
|
+
${normalized.recommendations.map(rec => `<li class="text-warn">${rec}</li>`).join('')}
|
|
943
|
+
</ul>
|
|
944
|
+
</div>
|
|
945
|
+
`;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Append CPU / Memory spark lines (moved from performance card)
|
|
949
|
+
try {
|
|
950
|
+
if (window.__resourceTrendCache) {
|
|
951
|
+
const t = window.__resourceTrendCache;
|
|
952
|
+
const sparkHtml = `
|
|
953
|
+
<div class="mt-lg">
|
|
954
|
+
<div class="resource-trend-label">Resource Trend</div>
|
|
955
|
+
<div class="resource-trend-col">
|
|
956
|
+
<div class="spark-col">
|
|
957
|
+
<span class="spark-label">CPU Spark (last ${Math.min(40, t.sampleCount || 0)} samples)</span>
|
|
958
|
+
<span class="spark-value">${t.spark || ''}</span>
|
|
959
|
+
<span class="spark-stats">Latest ${t.latestCpu?.toFixed ? t.latestCpu.toFixed(1) : '0'}% • Min ${(t.minCpu??0).toFixed(1)}% • Max ${(t.maxCpu??0).toFixed(1)}%</span>
|
|
960
|
+
</div>
|
|
961
|
+
<div class="spark-col">
|
|
962
|
+
<span class="spark-label">Mem Spark (heap)</span>
|
|
963
|
+
<span class="spark-value">${t.memSpark || ''}</span>
|
|
964
|
+
<span class="spark-stats">Latest ${(t.latestHeap/1024/1024).toFixed(2)} MB • Min ${(t.minHeap/1024/1024).toFixed(2)} MB • Max ${(t.maxHeap/1024/1024).toFixed(2)} MB</span>
|
|
965
|
+
</div>
|
|
966
|
+
</div>
|
|
967
|
+
</div>`;
|
|
968
|
+
html += sparkHtml;
|
|
969
|
+
}
|
|
970
|
+
} catch {/* ignore */}
|
|
971
|
+
|
|
972
|
+
document.getElementById('system-health').innerHTML = html;
|
|
973
|
+
try { window.lastSystemHealth = health; } catch {/* ignore */}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// --- Backup / Restore ---
|
|
977
|
+
// Extracted to js/admin.maintenance.js?v=1.19.1-e48e1b0d
|
|
978
|
+
|
|
979
|
+
async function performBackup() {
|
|
980
|
+
try {
|
|
981
|
+
showSuccess('Backup started...');
|
|
982
|
+
const response = await fetch('/api/admin/maintenance/backup', {
|
|
983
|
+
method: 'POST'
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
const data = await response.json();
|
|
987
|
+
if (data.success) {
|
|
988
|
+
showSuccess(`Backup completed: ${data.backupId}`);
|
|
989
|
+
loadMaintenanceStatus();
|
|
990
|
+
} else {
|
|
991
|
+
showError('Backup failed');
|
|
992
|
+
}
|
|
993
|
+
} catch (error) {
|
|
994
|
+
console.error('Error performing backup:', error);
|
|
995
|
+
showError('Backup failed');
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
async function clearCaches() {
|
|
1000
|
+
try {
|
|
1001
|
+
const response = await fetch('/api/admin/cache/clear', {
|
|
1002
|
+
method: 'POST'
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
const data = await response.json();
|
|
1006
|
+
if (data.success) {
|
|
1007
|
+
showSuccess(`Caches cleared: ${data.cleared.join(', ')}`);
|
|
1008
|
+
} else {
|
|
1009
|
+
showError('Failed to clear caches');
|
|
1010
|
+
}
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
console.error('Error clearing caches:', error);
|
|
1013
|
+
showError('Failed to clear caches');
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
async function restartServer() {
|
|
1018
|
+
if (!confirm('Are you sure you want to restart the server? This may temporarily interrupt service.')) {
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
try {
|
|
1023
|
+
showSuccess('Server restart initiated...');
|
|
1024
|
+
const response = await fetch('/api/admin/restart', {
|
|
1025
|
+
method: 'POST',
|
|
1026
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1027
|
+
body: JSON.stringify({ component: 'all' })
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
const data = await response.json();
|
|
1031
|
+
if (data.success) {
|
|
1032
|
+
showSuccess(data.message);
|
|
1033
|
+
} else {
|
|
1034
|
+
showError('Server restart failed');
|
|
1035
|
+
}
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
console.error('Error restarting server:', error);
|
|
1038
|
+
showError('Server restart failed');
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
async function loadConfiguration() {
|
|
1043
|
+
// Primary implementation in js/admin.config.js?v=1.19.1-e48e1b0d (loaded via defer).
|
|
1044
|
+
// This inline fallback only fires if the external script failed to load.
|
|
1045
|
+
if (window.__configExternalLoaded) return;
|
|
1046
|
+
try {
|
|
1047
|
+
var res = await fetch('/api/admin/config');
|
|
1048
|
+
var data = await res.json();
|
|
1049
|
+
if (!data.success) throw new Error('Failed to load config');
|
|
1050
|
+
var cfg = data.config;
|
|
1051
|
+
var html = '<form onsubmit="return updateConfiguration(event)">'
|
|
1052
|
+
+ '<div class="form-group"><label class="form-label">Max Connections</label>'
|
|
1053
|
+
+ '<input class="form-input" type="number" id="cfg-maxConnections" value="' + cfg.serverSettings.maxConnections + '" /></div>'
|
|
1054
|
+
+ '<div class="form-group"><label class="form-label">Request Timeout (ms)</label>'
|
|
1055
|
+
+ '<input class="form-input" type="number" id="cfg-requestTimeout" value="' + cfg.serverSettings.requestTimeout + '" /></div>'
|
|
1056
|
+
+ '<div class="form-group"><label class="form-label">Verbose Logging</label>'
|
|
1057
|
+
+ '<select class="form-input" id="cfg-verbose"><option value="1"' + (cfg.serverSettings.enableVerboseLogging ? ' selected' : '') + '>Enabled</option>'
|
|
1058
|
+
+ '<option value="0"' + (!cfg.serverSettings.enableVerboseLogging ? ' selected' : '') + '>Disabled</option></select></div>'
|
|
1059
|
+
+ '<div class="form-group"><label class="form-label">Enable Mutation</label>'
|
|
1060
|
+
+ '<select class="form-input" id="cfg-mutation"><option value="1"' + (cfg.serverSettings.enableMutation ? ' selected' : '') + '>Enabled</option>'
|
|
1061
|
+
+ '<option value="0"' + (!cfg.serverSettings.enableMutation ? ' selected' : '') + '>Disabled</option></select></div>'
|
|
1062
|
+
+ '<div style="margin-top:10px;"><button class="action-btn" type="submit">💾 Save Config</button></div>'
|
|
1063
|
+
+ '</form>'
|
|
1064
|
+
+ '<div style="opacity:0.5; margin-top:12px; font-size:11px;">Inline fallback — external config JS not loaded. Flags panel unavailable.</div>';
|
|
1065
|
+
document.getElementById('config-form').innerHTML = html;
|
|
1066
|
+
} catch (e) {
|
|
1067
|
+
document.getElementById('config-form').innerHTML = '<div class="error">Failed to load configuration</div>';
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
async function updateConfiguration(ev) {
|
|
1072
|
+
ev.preventDefault();
|
|
1073
|
+
var flagSelects = document.querySelectorAll('[data-flag]');
|
|
1074
|
+
var featureFlags = {};
|
|
1075
|
+
flagSelects.forEach(function(sel){
|
|
1076
|
+
var name = sel.getAttribute('data-flag');
|
|
1077
|
+
if(!name) return;
|
|
1078
|
+
featureFlags[name] = sel.value === '1';
|
|
1079
|
+
});
|
|
1080
|
+
var updates = {
|
|
1081
|
+
serverSettings: {
|
|
1082
|
+
maxConnections: parseInt(document.getElementById('cfg-maxConnections').value),
|
|
1083
|
+
requestTimeout: parseInt(document.getElementById('cfg-requestTimeout').value),
|
|
1084
|
+
enableVerboseLogging: document.getElementById('cfg-verbose').value === '1',
|
|
1085
|
+
enableMutation: document.getElementById('cfg-mutation').value === '1',
|
|
1086
|
+
rateLimit: {
|
|
1087
|
+
windowMs: parseInt((document.getElementById('cfg-windowMs') || {}).value || '60000'),
|
|
1088
|
+
maxRequests: parseInt((document.getElementById('cfg-maxRequests') || {}).value || '100')
|
|
1089
|
+
}
|
|
1090
|
+
},
|
|
1091
|
+
featureFlags: featureFlags
|
|
1092
|
+
};
|
|
1093
|
+
try {
|
|
1094
|
+
var res = await fetch('/api/admin/config', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(updates)});
|
|
1095
|
+
var data = await res.json();
|
|
1096
|
+
if (data.success) { showSuccess('Configuration updated'); loadConfiguration(); } else { showError(data.error || 'Update failed'); }
|
|
1097
|
+
} catch (e) { showError('Update failed'); }
|
|
1098
|
+
return false;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// Monitoring functions moved to js/admin.monitor.js?v=1.19.1-e48e1b0d
|
|
1102
|
+
|
|
1103
|
+
// ===== Log Viewer =====
|
|
1104
|
+
// Extracted to js/admin.logs.js?v=1.19.1-e48e1b0d
|
|
1105
|
+
|
|
1106
|
+
// ===== Instruction Management =====
|
|
1107
|
+
let instructionEditing = null;
|
|
1108
|
+
// Pagination state
|
|
1109
|
+
let allInstructions = [];
|
|
1110
|
+
let instructionPage = 1;
|
|
1111
|
+
let instructionPageSize = 25; // default page size
|
|
1112
|
+
|
|
1113
|
+
function getInstructionPageSizes() { return [10,25,50,100, 'All']; }
|
|
1114
|
+
|
|
1115
|
+
function buildInstructionPaginationControls(totalFiltered) {
|
|
1116
|
+
const container = document.getElementById('instruction-pagination');
|
|
1117
|
+
if (!container) return;
|
|
1118
|
+
const total = totalFiltered;
|
|
1119
|
+
const pageSize = instructionPageSize === 'All' ? total : instructionPageSize;
|
|
1120
|
+
const totalPages = pageSize === 0 ? 1 : Math.max(1, Math.ceil(total / pageSize));
|
|
1121
|
+
if (instructionPage > totalPages) instructionPage = totalPages; // clamp
|
|
1122
|
+
|
|
1123
|
+
const disablePrev = instructionPage <= 1;
|
|
1124
|
+
const disableNext = instructionPage >= totalPages;
|
|
1125
|
+
|
|
1126
|
+
const sizeOptions = getInstructionPageSizes().map(s => `<option value="${s}" ${s===instructionPageSize? 'selected':''}>${s}</option>`).join('');
|
|
1127
|
+
container.innerHTML = `
|
|
1128
|
+
<div class="ipag-row">
|
|
1129
|
+
<label class="ipag-label">Page Size:
|
|
1130
|
+
<select id="instruction-page-size" class="form-input ipag-select">${sizeOptions}</select>
|
|
1131
|
+
</label>
|
|
1132
|
+
<div class="ipag-nav">
|
|
1133
|
+
<button class="action-btn" ${disablePrev?'disabled':''}>⏮ First</button>
|
|
1134
|
+
<button class="action-btn" ${disablePrev?'disabled':''}>◀ Prev</button>
|
|
1135
|
+
<span class="ipag-page">Page ${instructionPage} / ${totalPages}</span>
|
|
1136
|
+
<button class="action-btn" ${disableNext?'disabled':''}>Next ▶</button>
|
|
1137
|
+
<button class="action-btn" ${disableNext?'disabled':''}>Last ⏭</button>
|
|
1138
|
+
</div>
|
|
1139
|
+
<span class="ipag-total">Filtered: ${total} total</span>
|
|
1140
|
+
</div>`;
|
|
1141
|
+
const sizeSelect = document.getElementById('instruction-page-size');
|
|
1142
|
+
sizeSelect.onchange = () => {
|
|
1143
|
+
instructionPageSize = sizeSelect.value === 'All' ? 'All' : parseInt(sizeSelect.value,10);
|
|
1144
|
+
instructionPage = 1; // reset to first page when size changes
|
|
1145
|
+
renderInstructionList(allInstructions);
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function changeInstructionPage(dir) {
|
|
1150
|
+
const totalFiltered = getFilteredInstructions(allInstructions).length;
|
|
1151
|
+
const pageSizeVal = instructionPageSize === 'All' ? totalFiltered : instructionPageSize;
|
|
1152
|
+
const totalPages = pageSizeVal === 0 ? 1 : Math.max(1, Math.ceil(totalFiltered / pageSizeVal));
|
|
1153
|
+
if (dir === 'first') instructionPage = 1;
|
|
1154
|
+
else if (dir === 'prev' && instructionPage > 1) instructionPage--;
|
|
1155
|
+
else if (dir === 'next' && instructionPage < totalPages) instructionPage++;
|
|
1156
|
+
else if (dir === 'last') instructionPage = totalPages;
|
|
1157
|
+
renderInstructionList(allInstructions);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// Instruction filtering & sorting enhancements
|
|
1161
|
+
function getFilteredInstructions(list) {
|
|
1162
|
+
const nameFilter = (document.getElementById('instruction-filter').value || '').toLowerCase();
|
|
1163
|
+
const categoryFilter = (document.getElementById('instruction-category-filter')?.value || '');
|
|
1164
|
+
const sizeFilter = (document.getElementById('instruction-size-filter')?.value || '');
|
|
1165
|
+
let filtered = list.filter(i => i.name.toLowerCase().includes(nameFilter));
|
|
1166
|
+
if (categoryFilter) {
|
|
1167
|
+
filtered = filtered.filter(i => {
|
|
1168
|
+
if (i.category === categoryFilter) return true;
|
|
1169
|
+
if (Array.isArray(i.categories) && i.categories.includes(categoryFilter)) return true;
|
|
1170
|
+
return false;
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
if (sizeFilter) filtered = filtered.filter(i => i.sizeCategory === sizeFilter);
|
|
1174
|
+
const sortSelect = document.getElementById('instruction-sort');
|
|
1175
|
+
const sortVal = sortSelect ? sortSelect.value : 'name-asc';
|
|
1176
|
+
const cmp = (a,b, key, dir='asc') => {
|
|
1177
|
+
if (a[key] === b[key]) return 0;
|
|
1178
|
+
return (a[key] < b[key] ? -1 : 1) * (dir === 'asc' ? 1 : -1);
|
|
1179
|
+
};
|
|
1180
|
+
switch(sortVal) {
|
|
1181
|
+
case 'name-desc': filtered.sort((a,b)=>cmp(a,b,'name','desc')); break;
|
|
1182
|
+
case 'size-asc': filtered.sort((a,b)=>cmp(a,b,'size','asc')); break;
|
|
1183
|
+
case 'size-desc': filtered.sort((a,b)=>cmp(a,b,'size','desc')); break;
|
|
1184
|
+
case 'mtime-asc': filtered.sort((a,b)=>cmp(a,b,'mtime','asc')); break;
|
|
1185
|
+
case 'mtime-desc': filtered.sort((a,b)=>cmp(a,b,'mtime','desc')); break;
|
|
1186
|
+
case 'category': filtered.sort((a,b)=>cmp(a,b,'category','asc') || cmp(a,b,'name','asc')); break;
|
|
1187
|
+
default: // name-asc
|
|
1188
|
+
filtered.sort((a,b)=>cmp(a,b,'name','asc'));
|
|
1189
|
+
}
|
|
1190
|
+
return filtered;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/* Removed legacy duplicate loadInstructions; unified enhanced version defined later */
|
|
1194
|
+
|
|
1195
|
+
async function loadInstructionCategories() {
|
|
1196
|
+
try {
|
|
1197
|
+
const res = await fetch('/api/instructions/categories');
|
|
1198
|
+
if(!res.ok) throw new Error('http '+res.status);
|
|
1199
|
+
const data = await res.json();
|
|
1200
|
+
// Accept shapes: {success:true,categories:[{name,count}]}, {success:true,data:{categories:[...]}}
|
|
1201
|
+
let cats = data.categories || data.data?.categories || [];
|
|
1202
|
+
if(Array.isArray(cats) && cats.length && typeof cats[0] === 'string') {
|
|
1203
|
+
cats = cats.map(n=>({ name:n, count: undefined }));
|
|
1204
|
+
}
|
|
1205
|
+
if(!Array.isArray(cats)) cats = [];
|
|
1206
|
+
const select = document.getElementById('instruction-category-filter');
|
|
1207
|
+
if(select){
|
|
1208
|
+
select.innerHTML = '<option value="">All Categories</option>';
|
|
1209
|
+
cats.forEach(cat => {
|
|
1210
|
+
if(!cat || !cat.name) return;
|
|
1211
|
+
const option = document.createElement('option');
|
|
1212
|
+
option.value = cat.name;
|
|
1213
|
+
option.textContent = cat.count != null ? `${cat.name} (${cat.count})` : cat.name;
|
|
1214
|
+
select.appendChild(option);
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
return cats.map(c=>c.name);
|
|
1218
|
+
} catch (e) {
|
|
1219
|
+
console.warn('Failed to load instruction categories:', e);
|
|
1220
|
+
return [];
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function renderInstructionList(instructions) {
|
|
1225
|
+
const filtered = getFilteredInstructions(instructions);
|
|
1226
|
+
if (filtered.length === 0) {
|
|
1227
|
+
document.getElementById('instructions-list').innerHTML = '<p>No instructions found</p>';
|
|
1228
|
+
buildInstructionPaginationControls(0);
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
const totalFiltered = filtered.length;
|
|
1232
|
+
let pageItems = filtered;
|
|
1233
|
+
if (instructionPageSize !== 'All') {
|
|
1234
|
+
const start = (instructionPage - 1) * instructionPageSize;
|
|
1235
|
+
const end = start + instructionPageSize;
|
|
1236
|
+
pageItems = filtered.slice(start, end);
|
|
1237
|
+
}
|
|
1238
|
+
const rows = pageItems.map(instr => {
|
|
1239
|
+
const rawSummary = (instr.semanticSummary || '').trim();
|
|
1240
|
+
let short = rawSummary.slice(0, 160);
|
|
1241
|
+
if (rawSummary.length > 160) short += '…';
|
|
1242
|
+
const safeSummary = escapeHtml(short);
|
|
1243
|
+
return `
|
|
1244
|
+
<div class="session-item instr-item-bg">
|
|
1245
|
+
<div class="session-header">
|
|
1246
|
+
<span class="session-id instr-id-bg">${instr.name}</span>
|
|
1247
|
+
<div>
|
|
1248
|
+
<button class="action-btn" onclick="editInstruction('${instr.name}')">✏ Edit</button>
|
|
1249
|
+
<button class="action-btn danger" onclick="deleteInstruction('${instr.name}')">🗑 Delete</button>
|
|
1250
|
+
</div>
|
|
1251
|
+
</div>
|
|
1252
|
+
<div class="stat-row"><span class="stat-label">Category</span><span class="stat-value">${instr.category || '—'}</span></div>
|
|
1253
|
+
<div class="stat-row"><span class="stat-label">Size</span><span class="stat-value">${instr.size} bytes (${instr.sizeCategory})</span></div>
|
|
1254
|
+
<div class="stat-row"><span class="stat-label">Modified</span><span class="stat-value">${new Date(instr.mtime).toLocaleString()}</span></div>
|
|
1255
|
+
<div class="stat-row stat-row-top">
|
|
1256
|
+
<span class="stat-label stat-label-pt">Summary</span>
|
|
1257
|
+
<span class="stat-value stat-value-wrap">
|
|
1258
|
+
${safeSummary || '<span style=\"opacity:.5;\">—</span>'}
|
|
1259
|
+
</span>
|
|
1260
|
+
</div>
|
|
1261
|
+
</div>`;
|
|
1262
|
+
}).join('');
|
|
1263
|
+
document.getElementById('instructions-list').innerHTML = rows;
|
|
1264
|
+
buildInstructionPaginationControls(totalFiltered);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function filterInstructions() {
|
|
1268
|
+
instructionPage = 1; // reset to first page on filter change
|
|
1269
|
+
renderInstructionList(allInstructions);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function showCreateInstruction() {
|
|
1273
|
+
instructionEditing = null;
|
|
1274
|
+
document.getElementById('instruction-editor-title').textContent = 'New Instruction';
|
|
1275
|
+
document.getElementById('instruction-filename').value = '';
|
|
1276
|
+
document.getElementById('instruction-filename').disabled = false;
|
|
1277
|
+
document.getElementById('instruction-content').value = '{\n "description": "New instruction"\n}';
|
|
1278
|
+
ensureInstructionEditorAtTop();
|
|
1279
|
+
const ed = document.getElementById('instruction-editor');
|
|
1280
|
+
ed.classList.remove('hidden');
|
|
1281
|
+
// Smooth scroll to make editor visible at top
|
|
1282
|
+
try { ed.scrollIntoView({ behavior:'smooth', block:'start' }); } catch {}
|
|
1283
|
+
document.getElementById('instruction-filename').focus();
|
|
1284
|
+
instructionOriginalContent = document.getElementById('instruction-content').value;
|
|
1285
|
+
updateInstructionEditorDiagnostics();
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
async function editInstruction(name) {
|
|
1289
|
+
const editor = document.getElementById('instruction-editor');
|
|
1290
|
+
const filenameEl = document.getElementById('instruction-filename');
|
|
1291
|
+
const contentEl = document.getElementById('instruction-content');
|
|
1292
|
+
let attempts = 0;
|
|
1293
|
+
const maxAttempts = 2; // one retry after initial attempt
|
|
1294
|
+
let lastError;
|
|
1295
|
+
while (attempts < maxAttempts) {
|
|
1296
|
+
try {
|
|
1297
|
+
attempts++;
|
|
1298
|
+
// lightweight loading indicator
|
|
1299
|
+
if (contentEl && attempts === 1) {
|
|
1300
|
+
contentEl.value = '// Loading ' + name + '...';
|
|
1301
|
+
}
|
|
1302
|
+
const res = await fetch('/api/instructions/' + encodeURIComponent(name));
|
|
1303
|
+
if (!res.ok) throw new Error('http ' + res.status);
|
|
1304
|
+
const data = await res.json();
|
|
1305
|
+
if (data.success === false && !data.content && !data.data?.content) throw new Error('server reported failure');
|
|
1306
|
+
if (!data.content && data.data?.content) data.content = data.data.content;
|
|
1307
|
+
if (!data.content) throw new Error('missing content');
|
|
1308
|
+
instructionEditing = name;
|
|
1309
|
+
document.getElementById('instruction-editor-title').textContent = 'Edit Instruction: ' + name;
|
|
1310
|
+
filenameEl.value = name;
|
|
1311
|
+
filenameEl.disabled = true;
|
|
1312
|
+
const pretty = JSON.stringify(data.content, null, 2);
|
|
1313
|
+
contentEl.value = pretty;
|
|
1314
|
+
ensureInstructionEditorAtTop();
|
|
1315
|
+
editor.classList.remove('hidden');
|
|
1316
|
+
try { editor.scrollIntoView({ behavior:'smooth', block:'start' }); } catch {}
|
|
1317
|
+
instructionOriginalContent = pretty;
|
|
1318
|
+
updateInstructionEditorDiagnostics();
|
|
1319
|
+
return; // success
|
|
1320
|
+
} catch (e) {
|
|
1321
|
+
lastError = e;
|
|
1322
|
+
// brief delay before retry to allow file system settle or server warm path
|
|
1323
|
+
if (attempts < maxAttempts) {
|
|
1324
|
+
await new Promise(r => setTimeout(r, 120));
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
console.warn('editInstruction failed after retries', lastError);
|
|
1330
|
+
showError('Failed to load instruction');
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function cancelEditInstruction() {
|
|
1334
|
+
document.getElementById('instruction-editor').classList.add('hidden');
|
|
1335
|
+
const diff = document.getElementById('instruction-diff-container');
|
|
1336
|
+
if(diff) diff.classList.add('hidden');
|
|
1337
|
+
instructionOriginalContent='';
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// Reposition editor just below the toolbar (before the instruction list) when activated
|
|
1341
|
+
function ensureInstructionEditorAtTop(){
|
|
1342
|
+
try {
|
|
1343
|
+
const editor = document.getElementById('instruction-editor');
|
|
1344
|
+
const list = document.getElementById('instructions-list');
|
|
1345
|
+
if(!editor || !list) return;
|
|
1346
|
+
const parent = list.parentElement; // card body
|
|
1347
|
+
// The toolbar is the element immediately before instructions-list (with buttons + filters)
|
|
1348
|
+
if(parent && parent.contains(list)){
|
|
1349
|
+
// Insert editor right BEFORE instructions-list so it appears at top
|
|
1350
|
+
if(editor.nextElementSibling !== list){
|
|
1351
|
+
parent.insertBefore(editor, list);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
} catch {/* ignore */}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// --- Instruction Editor Helpers (added patch) ---
|
|
1358
|
+
let instructionOriginalContent = '';
|
|
1359
|
+
let instructionDiffVisible = false;
|
|
1360
|
+
|
|
1361
|
+
function safeParseInstruction(raw){
|
|
1362
|
+
try { return JSON.parse(raw); } catch { return null; }
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function updateInstructionEditorDiagnostics(){
|
|
1366
|
+
const ta = document.getElementById('instruction-content');
|
|
1367
|
+
const diag = document.getElementById('instruction-diagnostics');
|
|
1368
|
+
if(!ta||!diag) return;
|
|
1369
|
+
const raw = ta.value;
|
|
1370
|
+
if(!raw.trim()){ diag.innerHTML = '<em>Empty.</em>'; return; }
|
|
1371
|
+
const parsed = safeParseInstruction(raw);
|
|
1372
|
+
if(!parsed){
|
|
1373
|
+
diag.innerHTML = '<span class="text-danger">Invalid JSON</span>';
|
|
1374
|
+
} else {
|
|
1375
|
+
const size = raw.length;
|
|
1376
|
+
const cats = Array.isArray(parsed.categories)? parsed.categories.length : 0;
|
|
1377
|
+
const schemaVer = parsed.schemaVersion || parsed.schema || '?';
|
|
1378
|
+
const changed = instructionOriginalContent && raw !== instructionOriginalContent;
|
|
1379
|
+
diag.innerHTML = `Size: ${size} chars • Categories: ${cats} • Schema: ${schemaVer} ${changed?'<span class="text-warn">(modified)</span>':''}`;
|
|
1380
|
+
}
|
|
1381
|
+
if(instructionDiffVisible) refreshInstructionDiff();
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function refreshInstructionDiff(){
|
|
1385
|
+
const diffWrap = document.getElementById('instruction-diff-container');
|
|
1386
|
+
const diffPre = document.getElementById('instruction-diff');
|
|
1387
|
+
const ta = document.getElementById('instruction-content');
|
|
1388
|
+
if(!diffWrap||!diffPre||!ta) return;
|
|
1389
|
+
if(!instructionOriginalContent){ diffPre.textContent='(no baseline)'; return; }
|
|
1390
|
+
if(ta.value === instructionOriginalContent){ diffPre.textContent='(no changes)'; return; }
|
|
1391
|
+
// Simple line diff (not optimal but lightweight)
|
|
1392
|
+
const before = instructionOriginalContent.split(/\r?\n/);
|
|
1393
|
+
const after = ta.value.split(/\r?\n/);
|
|
1394
|
+
const max = Math.max(before.length, after.length);
|
|
1395
|
+
const out = [];
|
|
1396
|
+
for(let i=0;i<max;i++){
|
|
1397
|
+
const a = before[i];
|
|
1398
|
+
const b = after[i];
|
|
1399
|
+
if(a === b){
|
|
1400
|
+
if(a !== undefined) out.push(' ' + a);
|
|
1401
|
+
} else {
|
|
1402
|
+
if(a !== undefined) out.push('- ' + a);
|
|
1403
|
+
if(b !== undefined) out.push('+ ' + b);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
diffPre.textContent = out.join('\n');
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function toggleInstructionDiff(){
|
|
1410
|
+
instructionDiffVisible = !instructionDiffVisible;
|
|
1411
|
+
const wrap = document.getElementById('instruction-diff-container');
|
|
1412
|
+
if(!wrap) return;
|
|
1413
|
+
if(instructionDiffVisible){
|
|
1414
|
+
wrap.classList.remove('hidden');
|
|
1415
|
+
refreshInstructionDiff();
|
|
1416
|
+
} else {
|
|
1417
|
+
wrap.classList.add('hidden');
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
async function saveInstruction(){
|
|
1422
|
+
const nameEl = document.getElementById('instruction-filename');
|
|
1423
|
+
const ta = document.getElementById('instruction-content');
|
|
1424
|
+
if(!nameEl||!ta) return;
|
|
1425
|
+
const raw = ta.value;
|
|
1426
|
+
const parsed = safeParseInstruction(raw);
|
|
1427
|
+
if(!parsed){ showError('Cannot save: invalid JSON'); return; }
|
|
1428
|
+
// Auto-upgrade template schemaVersion if legacy 1.x
|
|
1429
|
+
if(parsed && parsed.schemaVersion && /^1(\.|$)/.test(String(parsed.schemaVersion))){
|
|
1430
|
+
parsed.schemaVersion = '2';
|
|
1431
|
+
}
|
|
1432
|
+
const body = { content: parsed };
|
|
1433
|
+
let url = '/api/instructions';
|
|
1434
|
+
let method = 'POST';
|
|
1435
|
+
if(instructionEditing){
|
|
1436
|
+
url += '/' + encodeURIComponent(instructionEditing);
|
|
1437
|
+
method = 'PUT';
|
|
1438
|
+
} else {
|
|
1439
|
+
body.name = nameEl.value.trim();
|
|
1440
|
+
if(!body.name){ showError('Provide file name'); return; }
|
|
1441
|
+
}
|
|
1442
|
+
try {
|
|
1443
|
+
const res = await fetch(url, { method, headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)});
|
|
1444
|
+
const data = await res.json();
|
|
1445
|
+
if(!res.ok || !data.success){ throw new Error(data.error || data.message || 'Save failed'); }
|
|
1446
|
+
showSuccess(instructionEditing? 'Instruction updated':'Instruction created');
|
|
1447
|
+
instructionOriginalContent = JSON.stringify(parsed, null, 2);
|
|
1448
|
+
ta.value = instructionOriginalContent; // normalized pretty
|
|
1449
|
+
if(!instructionEditing) instructionEditing = body.name;
|
|
1450
|
+
updateInstructionEditorDiagnostics();
|
|
1451
|
+
loadInstructions();
|
|
1452
|
+
} catch(e){ showError(e.message || 'Save failed'); }
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
async function loadInstructions() {
|
|
1456
|
+
const listEl = document.getElementById('instructions-list');
|
|
1457
|
+
listEl.innerHTML = 'Loading...';
|
|
1458
|
+
try {
|
|
1459
|
+
// Load categories first to populate the dropdown (ignore errors; we'll derive later if needed)
|
|
1460
|
+
const catNames = await loadInstructionCategories();
|
|
1461
|
+
const res = await fetch('/api/instructions');
|
|
1462
|
+
if(!res.ok) throw new Error('http '+res.status);
|
|
1463
|
+
const data = await res.json();
|
|
1464
|
+
// Accept shapes: {success:true,instructions:[...]}, {data:{instructions:[...]}}, or legacy {instructions:[...]}
|
|
1465
|
+
if (!('success' in data) && !('data' in data) && !('instructions' in data)) throw new Error('unrecognized instructions payload');
|
|
1466
|
+
const rawList = data.instructions || data.data?.instructions || [];
|
|
1467
|
+
allInstructions = Array.isArray(rawList) ? rawList : [];
|
|
1468
|
+
try { console.log('[dashboard] fetched instructions:', allInstructions.length); } catch {}
|
|
1469
|
+
// If categories endpoint failed / returned empty, derive from instruction list
|
|
1470
|
+
if(!catNames.length) {
|
|
1471
|
+
try {
|
|
1472
|
+
const select = document.getElementById('instruction-category-filter');
|
|
1473
|
+
if(select){
|
|
1474
|
+
const derived = Array.from(new Set(allInstructions.flatMap(i=> [i.category, ...(Array.isArray(i.categories)? i.categories: [])]).filter(Boolean))).sort();
|
|
1475
|
+
derived.forEach(n=>{
|
|
1476
|
+
const opt = document.createElement('option');
|
|
1477
|
+
opt.value = n; opt.textContent = n; select.appendChild(opt);
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
} catch(_) { /* ignore */ }
|
|
1481
|
+
}
|
|
1482
|
+
instructionPage = 1; // reset page whenever we reload
|
|
1483
|
+
renderInstructionList(allInstructions);
|
|
1484
|
+
} catch (e) {
|
|
1485
|
+
console.warn('loadInstructions error', e);
|
|
1486
|
+
listEl.innerHTML = '<div class="error">Failed to load instructions</div>';
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// Re-introduced after patch: formatInstructionJson utility (was accidentally truncated)
|
|
1491
|
+
function formatInstructionJson(){
|
|
1492
|
+
const ta = document.getElementById('instruction-content');
|
|
1493
|
+
if(!ta) return;
|
|
1494
|
+
try {
|
|
1495
|
+
const parsed = JSON.parse(ta.value);
|
|
1496
|
+
ta.value = JSON.stringify(parsed, null, 2);
|
|
1497
|
+
updateInstructionEditorDiagnostics();
|
|
1498
|
+
} catch { showError('Cannot format: invalid JSON'); }
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function applyInstructionTemplate(){
|
|
1502
|
+
const ta = document.getElementById('instruction-content');
|
|
1503
|
+
if(!ta) return;
|
|
1504
|
+
if(ta.value.trim() && !confirm('Replace current content with template?')) return;
|
|
1505
|
+
const now = new Date().toISOString();
|
|
1506
|
+
const template = {
|
|
1507
|
+
id: 'sample-instruction',
|
|
1508
|
+
title: 'Sample Instruction',
|
|
1509
|
+
body: 'Detailed instruction content here.\nAdd multi-line guidance and steps.',
|
|
1510
|
+
priority: 50,
|
|
1511
|
+
audience: 'all',
|
|
1512
|
+
requirement: 'optional',
|
|
1513
|
+
categories: ['general'],
|
|
1514
|
+
primaryCategory: 'general',
|
|
1515
|
+
reviewIntervalDays: 180,
|
|
1516
|
+
schemaVersion: '4',
|
|
1517
|
+
description: 'Describe purpose and scope.',
|
|
1518
|
+
createdAt: now,
|
|
1519
|
+
updatedAt: now
|
|
1520
|
+
};
|
|
1521
|
+
ta.value = JSON.stringify(template, null, 2);
|
|
1522
|
+
updateInstructionEditorDiagnostics();
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// ===== Resource Trend (CPU/Mem) Long-Term History (merged into Performance card) =====
|
|
1526
|
+
(function initResourceTrendMerge(){
|
|
1527
|
+
async function fetchResourceTrends(){
|
|
1528
|
+
try {
|
|
1529
|
+
const res = await fetch('/api/system/resources?limit=300');
|
|
1530
|
+
if(!res.ok) throw new Error('http '+res.status);
|
|
1531
|
+
const json = await res.json();
|
|
1532
|
+
const samples = json?.data?.samples || [];
|
|
1533
|
+
const trend = json?.data?.trend || { cpuSlope:0, memSlope:0 };
|
|
1534
|
+
if(samples.length === 0){
|
|
1535
|
+
window.__resourceTrendCache = { windowSec:0, sampleCount:0, latestCpu:0, latestHeap:0, cpuSlope:0, memSlope:0, spark:'' };
|
|
1536
|
+
// trigger re-render next stats update
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
const latest = samples[samples.length-1];
|
|
1540
|
+
const first = samples[0];
|
|
1541
|
+
const durationSec = ((latest.timestamp - first.timestamp)/1000).toFixed(0);
|
|
1542
|
+
const tail = samples.slice(-40);
|
|
1543
|
+
const spark = tail.map(s=>{
|
|
1544
|
+
const v = Math.min(100, Math.max(0, s.cpuPercent));
|
|
1545
|
+
const idx = Math.round(v/12.5);
|
|
1546
|
+
const blocks = ['▁','▂','▃','▄','▅','▆','▇','█'];
|
|
1547
|
+
return blocks[Math.min(blocks.length-1, idx)];
|
|
1548
|
+
}).join('');
|
|
1549
|
+
const minCpu = tail.reduce((m,s)=> s.cpuPercent<m? s.cpuPercent:m, tail[0].cpuPercent);
|
|
1550
|
+
const maxCpu = tail.reduce((m,s)=> s.cpuPercent>m? s.cpuPercent:m, tail[0].cpuPercent);
|
|
1551
|
+
// Memory spark (scale heap delta relative to max in window)
|
|
1552
|
+
const maxHeap = tail.reduce((m,s)=> s.heapUsed>m?s.heapUsed:m,0) || 1;
|
|
1553
|
+
const minHeap = tail.reduce((m,s)=> s.heapUsed<m?s.heapUsed:m, tail[0].heapUsed);
|
|
1554
|
+
const memSpark = tail.map(s=>{
|
|
1555
|
+
const ratio = Math.min(1, Math.max(0, s.heapUsed / maxHeap));
|
|
1556
|
+
const idx = Math.round(ratio*7);
|
|
1557
|
+
const blocks = ['▁','▂','▃','▄','▅','▆','▇','█'];
|
|
1558
|
+
return blocks[Math.min(blocks.length-1, idx)];
|
|
1559
|
+
}).join('');
|
|
1560
|
+
window.__resourceTrendCache = {
|
|
1561
|
+
windowSec: durationSec,
|
|
1562
|
+
sampleCount: samples.length,
|
|
1563
|
+
latestCpu: latest.cpuPercent,
|
|
1564
|
+
latestHeap: latest.heapUsed,
|
|
1565
|
+
minCpu,
|
|
1566
|
+
maxCpu,
|
|
1567
|
+
minHeap,
|
|
1568
|
+
maxHeap,
|
|
1569
|
+
cpuSlope: trend.cpuSlope || 0,
|
|
1570
|
+
memSlope: trend.memSlope || 0,
|
|
1571
|
+
spark,
|
|
1572
|
+
memSpark
|
|
1573
|
+
};
|
|
1574
|
+
// Re-render performance card if stats already loaded
|
|
1575
|
+
try {
|
|
1576
|
+
if(typeof window.lastSystemStats === 'object') displaySystemStats(window.lastSystemStats);
|
|
1577
|
+
if(typeof window.lastSystemHealth === 'object') displaySystemHealth(window.lastSystemHealth);
|
|
1578
|
+
} catch(e){/*ignore*/}
|
|
1579
|
+
} catch(e){
|
|
1580
|
+
// ignore failures; card will just show base stats
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
// periodic fetch
|
|
1584
|
+
fetchResourceTrends();
|
|
1585
|
+
setInterval(fetchResourceTrends, 10000);
|
|
1586
|
+
})();
|
|
1587
|
+
|
|
1588
|
+
// Instruction management logic extracted to js/admin.instructions.js?v=1.19.1-e48e1b0d
|
|
1589
|
+
// Functions exposed globally: loadInstructions, renderInstructionList, editInstruction, saveInstruction, deleteInstruction, etc.
|
|
1590
|
+
|
|
1591
|
+
function startAutoRefresh() {
|
|
1592
|
+
refreshInterval = setInterval(() => {
|
|
1593
|
+
if (currentSection === 'overview') {
|
|
1594
|
+
loadOverviewData();
|
|
1595
|
+
} else if (currentSection === 'sessions') {
|
|
1596
|
+
loadSessions();
|
|
1597
|
+
} else if (currentSection === 'maintenance') {
|
|
1598
|
+
loadMaintenanceStatus();
|
|
1599
|
+
}
|
|
1600
|
+
}, 30000); // Refresh every 30 seconds
|
|
1601
|
+
}
|
|
1602
|
+
// Build metadata loader
|
|
1603
|
+
(async function fetchBuildMeta(){
|
|
1604
|
+
try {
|
|
1605
|
+
// Cache bust query param to avoid any intermediary caching of status response
|
|
1606
|
+
const r = await fetch('/api/status?t=' + Date.now());
|
|
1607
|
+
const j = await r.json();
|
|
1608
|
+
const el = document.getElementById('buildMeta');
|
|
1609
|
+
const ver = j.version || '?.?.?';
|
|
1610
|
+
const commit = j.build ? `<span class=\"build-badge\">${j.build}</span>` : '';
|
|
1611
|
+
const bt = j.buildTime ? new Date(j.buildTime).toLocaleString() : 'unknown';
|
|
1612
|
+
el.innerHTML = `Version <strong>${ver}</strong> ${commit} • Built ${bt}`;
|
|
1613
|
+
} catch { const el = document.getElementById('buildMeta'); if(el) el.textContent='Build metadata unavailable'; }
|
|
1614
|
+
})();
|
|
1615
|
+
|
|
1616
|
+
function showError(message) {
|
|
1617
|
+
// Remove existing notifications (only toast notifications, not styled buttons)
|
|
1618
|
+
document.querySelectorAll('.toast-notification').forEach(el => el.remove());
|
|
1619
|
+
|
|
1620
|
+
const errorDiv = document.createElement('div');
|
|
1621
|
+
errorDiv.className = 'error toast-notification';
|
|
1622
|
+
errorDiv.textContent = message;
|
|
1623
|
+
document.querySelector('.admin-container').insertBefore(errorDiv, document.querySelector('.admin-container').firstChild.nextSibling);
|
|
1624
|
+
|
|
1625
|
+
setTimeout(() => errorDiv.remove(), 5000);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
function showSuccess(message) {
|
|
1629
|
+
// Remove existing notifications (only toast notifications, not styled buttons)
|
|
1630
|
+
document.querySelectorAll('.toast-notification').forEach(el => el.remove());
|
|
1631
|
+
|
|
1632
|
+
const successDiv = document.createElement('div');
|
|
1633
|
+
successDiv.className = 'success toast-notification';
|
|
1634
|
+
successDiv.textContent = message;
|
|
1635
|
+
document.querySelector('.admin-container').insertBefore(successDiv, document.querySelector('.admin-container').firstChild.nextSibling);
|
|
1636
|
+
|
|
1637
|
+
setTimeout(() => successDiv.remove(), 5000);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// Utility functions
|
|
1641
|
+
function formatUptime(seconds) {
|
|
1642
|
+
const days = Math.floor(seconds / 86400);
|
|
1643
|
+
const hours = Math.floor((seconds % 86400) / 3600);
|
|
1644
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
1645
|
+
|
|
1646
|
+
if (days > 0) return `${days}d ${hours}h ${minutes}m`;
|
|
1647
|
+
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
1648
|
+
return `${minutes}m`;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
function formatBytes(bytes) {
|
|
1652
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
1653
|
+
if (bytes === 0) return '0 B';
|
|
1654
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
1655
|
+
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
|
1656
|
+
}
|
|
1657
|
+
function escapeHtml(str) {
|
|
1658
|
+
if (str == null) return '';
|
|
1659
|
+
return String(str)
|
|
1660
|
+
.replace(/&/g, '&')
|
|
1661
|
+
.replace(/</g, '<')
|
|
1662
|
+
.replace(/>/g, '>')
|
|
1663
|
+
.replace(/"/g, '"')
|
|
1664
|
+
.replace(/'/g, ''');
|
|
1665
|
+
}
|
|
1666
|
+
// ---------------- Drilldown Layered SVG (Experimental) -----------------
|
|
1667
|
+
// ELK auto-layout integration (Option A): dynamically load elkjs for layered layout.
|
|
1668
|
+
// Falls back to prior manual lane layout if ELK load fails or toggle disabled.
|
|
1669
|
+
const ELK_LOCAL_URL = '/js/elk.bundled.js';
|
|
1670
|
+
let __elkLoading = null; let __elkInstance = null;
|
|
1671
|
+
function ensureElk(){
|
|
1672
|
+
if(__elkInstance) return Promise.resolve(__elkInstance);
|
|
1673
|
+
if(__elkLoading) return __elkLoading;
|
|
1674
|
+
__elkLoading = new Promise((resolve,reject)=>{
|
|
1675
|
+
if(window.ELK){ try { __elkInstance = new window.ELK(); return resolve(__elkInstance); } catch(e){ return reject(e);} }
|
|
1676
|
+
const s = document.createElement('script');
|
|
1677
|
+
s.src = ELK_LOCAL_URL; s.async = true; s.onload = ()=>{ try { __elkInstance = new window.ELK(); resolve(__elkInstance);} catch(e){ reject(e);} };
|
|
1678
|
+
s.onerror = (e)=> reject(new Error('elk load failed'));
|
|
1679
|
+
document.head.appendChild(s);
|
|
1680
|
+
});
|
|
1681
|
+
return __elkLoading;
|
|
1682
|
+
}
|
|
1683
|
+
function drilldownUseElk(){
|
|
1684
|
+
const cb = document.getElementById('drill-use-elk');
|
|
1685
|
+
return !!(cb && cb.checked);
|
|
1686
|
+
}
|
|
1687
|
+
async function refreshDrillCategories(){
|
|
1688
|
+
const status = document.getElementById('drill-status');
|
|
1689
|
+
const sel = document.getElementById('drill-categories');
|
|
1690
|
+
if(status) status.textContent='loading categories...';
|
|
1691
|
+
try {
|
|
1692
|
+
const r = await fetch('/api/graph/categories');
|
|
1693
|
+
const j = await r.json();
|
|
1694
|
+
if(!j.success) throw new Error(j.error||'failed');
|
|
1695
|
+
if(sel){
|
|
1696
|
+
sel.innerHTML='';
|
|
1697
|
+
j.categories.forEach(c=>{
|
|
1698
|
+
const o = document.createElement('option');
|
|
1699
|
+
o.value = c.id; o.textContent = `${c.id} (${c.count})`;
|
|
1700
|
+
sel.appendChild(o);
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
if(status) status.textContent=`loaded ${j.total} categories`;
|
|
1704
|
+
} catch(e){ if(status) status.textContent='category load error: '+ (e?.message||e); }
|
|
1705
|
+
}
|
|
1706
|
+
async function loadDrillInstructions(){
|
|
1707
|
+
const catSel = document.getElementById('drill-categories');
|
|
1708
|
+
const instSel = document.getElementById('drill-instructions');
|
|
1709
|
+
const status = document.getElementById('drill-status');
|
|
1710
|
+
if(!catSel || !instSel) return;
|
|
1711
|
+
const cats = [...catSel.selectedOptions].map(o=> o.value).join(',');
|
|
1712
|
+
if(status) status.textContent='loading instructions...';
|
|
1713
|
+
try {
|
|
1714
|
+
const r = await fetch('/api/graph/instructions?categories=' + encodeURIComponent(cats));
|
|
1715
|
+
const j = await r.json();
|
|
1716
|
+
if(!j.success) throw new Error(j.error||'failed');
|
|
1717
|
+
instSel.innerHTML='';
|
|
1718
|
+
j.instructions.forEach(i=>{
|
|
1719
|
+
const o = document.createElement('option');
|
|
1720
|
+
o.value = i.id; o.textContent = i.id; instSel.appendChild(o);
|
|
1721
|
+
});
|
|
1722
|
+
if(status) status.textContent=`loaded ${j.count} instructions`;
|
|
1723
|
+
} catch(e){ if(status) status.textContent='instruction load error: '+ (e?.message||e); }
|
|
1724
|
+
}
|
|
1725
|
+
function clearDrillSvg(){
|
|
1726
|
+
const svg = document.getElementById('drill-svg');
|
|
1727
|
+
const status = document.getElementById('drill-status');
|
|
1728
|
+
const catSel = document.getElementById('drill-categories');
|
|
1729
|
+
const instSel = document.getElementById('drill-instructions');
|
|
1730
|
+
|
|
1731
|
+
if(svg) svg.innerHTML='';
|
|
1732
|
+
if(status) status.textContent='selections cleared';
|
|
1733
|
+
|
|
1734
|
+
// Clear selections in both dropdowns
|
|
1735
|
+
if(catSel) {
|
|
1736
|
+
for(let i = 0; i < catSel.options.length; i++) {
|
|
1737
|
+
catSel.options[i].selected = false;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
if(instSel) {
|
|
1741
|
+
for(let i = 0; i < instSel.options.length; i++) {
|
|
1742
|
+
instSel.options[i].selected = false;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
async function renderDrillSvg(){
|
|
1747
|
+
const instSel = document.getElementById('drill-instructions');
|
|
1748
|
+
const status = document.getElementById('drill-status');
|
|
1749
|
+
const svg = document.getElementById('drill-svg');
|
|
1750
|
+
if(!instSel || !svg) return;
|
|
1751
|
+
// Clear detail panel each render
|
|
1752
|
+
const detail = document.getElementById('drill-detail');
|
|
1753
|
+
if(detail){ detail.style.display='none'; detail.textContent=''; }
|
|
1754
|
+
// Persistent for click handlers
|
|
1755
|
+
window.__lastDrillData = null;
|
|
1756
|
+
const ids = [...instSel.selectedOptions].map(o=> o.value);
|
|
1757
|
+
if(!ids.length){ if(status) status.textContent='select instructions first'; return; }
|
|
1758
|
+
const expandEnabled = !!document.getElementById('drill-expand') && document.getElementById('drill-expand').checked;
|
|
1759
|
+
const membershipEnabled = !!document.getElementById('drill-show-membership') && document.getElementById('drill-show-membership').checked;
|
|
1760
|
+
if(status) status.textContent='fetching relations...';
|
|
1761
|
+
try {
|
|
1762
|
+
const r = await fetch('/api/graph/relations?instructions=' + encodeURIComponent(ids.join(',')) + (expandEnabled? '&expand=1':'') );
|
|
1763
|
+
const j = await r.json();
|
|
1764
|
+
if(!j.success) throw new Error(j.error||'failed');
|
|
1765
|
+
const instructions = j.nodes.filter(n=> n.nodeType==='instruction');
|
|
1766
|
+
// Keep full structure for interactions
|
|
1767
|
+
window.__lastDrillData = { raw:j, instructions };
|
|
1768
|
+
const nodeWidth = 140; const nodeHeight = 32;
|
|
1769
|
+
// Decide layout strategy
|
|
1770
|
+
let usedElk = false;
|
|
1771
|
+
let positions = new Map();
|
|
1772
|
+
let categories = [];
|
|
1773
|
+
let instrEdgeCount = 0; // unified edge count for legend
|
|
1774
|
+
if(drilldownUseElk()){
|
|
1775
|
+
if(status) status.textContent='loading ELK layout...';
|
|
1776
|
+
try {
|
|
1777
|
+
const elk = await ensureElk();
|
|
1778
|
+
if(!elk) throw new Error('ELK unavailable');
|
|
1779
|
+
const elkGraph = {
|
|
1780
|
+
id:'root',
|
|
1781
|
+
layoutOptions: {
|
|
1782
|
+
'elk.algorithm':'layered',
|
|
1783
|
+
'elk.direction':'RIGHT',
|
|
1784
|
+
'elk.layered.spacing.nodeNodeBetweenLayers':'55',
|
|
1785
|
+
'elk.spacing.nodeNode':'30'
|
|
1786
|
+
},
|
|
1787
|
+
children: instructions.map(inst=> ({ id: inst.id, width: nodeWidth, height: nodeHeight })),
|
|
1788
|
+
edges: j.edges.filter(e=> e.type!=='category' && instructions.find(n=> n.id===e.from) && instructions.find(n=> n.id===e.to)).map((e,i)=> ({ id:'e'+i, sources:[e.from], targets:[e.to] }))
|
|
1789
|
+
};
|
|
1790
|
+
const laidOut = await elk.layout(elkGraph);
|
|
1791
|
+
// Initial raw positions from ELK
|
|
1792
|
+
laidOut.children.forEach(c=>{ positions.set(c.id,{ x:c.x||0, y:c.y||0 }); });
|
|
1793
|
+
usedElk = true;
|
|
1794
|
+
// Derive categories (primary only for now)
|
|
1795
|
+
categories = [...new Set(instructions.map(i=> (i.categories&&i.categories[0])||'uncategorized'))];
|
|
1796
|
+
// Order categories by average original y so visual order is stable / data-driven
|
|
1797
|
+
categories.sort((a,b)=>{
|
|
1798
|
+
const ay = avg(instructions.filter(i=> (i.categories&&i.categories[0])===a).map(i=> positions.get(i.id)?.y||0));
|
|
1799
|
+
const by = avg(instructions.filter(i=> (i.categories&&i.categories[0])===b).map(i=> positions.get(i.id)?.y||0));
|
|
1800
|
+
return ay - by;
|
|
1801
|
+
});
|
|
1802
|
+
// Snap categories into uniform horizontal lanes for readability
|
|
1803
|
+
const laneHeight = 70; // vertical space per category lane
|
|
1804
|
+
const gutterWidth = 140; // reserved space on left for labels + connectors
|
|
1805
|
+
const padX = 40; const padY = 40; // outer vertical padding + x padding after gutter
|
|
1806
|
+
const categoryLaneY = new Map();
|
|
1807
|
+
categories.forEach((c,i)=> categoryLaneY.set(c, padY + i*laneHeight));
|
|
1808
|
+
// Reposition nodes: keep original x, quantize y to lane + small intra-lane spread based on original y ordering
|
|
1809
|
+
const laneOffsets = new Map();
|
|
1810
|
+
instructions.forEach(inst=>{
|
|
1811
|
+
const cat = (inst.categories&&inst.categories[0])||'uncategorized';
|
|
1812
|
+
const baseY = categoryLaneY.get(cat) || padY;
|
|
1813
|
+
const p = positions.get(inst.id); if(!p) return;
|
|
1814
|
+
p.y = baseY; // snap exactly
|
|
1815
|
+
// shift x by gutter + padX so nodes never overlap labels
|
|
1816
|
+
p.x = p.x + gutterWidth + padX;
|
|
1817
|
+
});
|
|
1818
|
+
// Compute new bounding box after snapping
|
|
1819
|
+
let maxX=0,maxY=0,minX=Infinity,minY=Infinity;
|
|
1820
|
+
positions.forEach(p=>{ maxX=Math.max(maxX,p.x+nodeWidth); maxY=Math.max(maxY,p.y+nodeHeight); minX=Math.min(minX,p.x); minY=Math.min(minY,p.y); });
|
|
1821
|
+
const totalWidth = Math.max(900, maxX - minX + padX*2 + gutterWidth);
|
|
1822
|
+
const totalHeight = Math.max(160, (categories.length? (padY + categories.length*laneHeight) : (maxY-minY+padY*2)));
|
|
1823
|
+
svg.setAttribute('width', String(totalWidth));
|
|
1824
|
+
svg.setAttribute('height', String(totalHeight));
|
|
1825
|
+
// Prepare drawing surface
|
|
1826
|
+
svg.innerHTML='';
|
|
1827
|
+
// Lane backgrounds (subtle)
|
|
1828
|
+
categories.forEach((cat,i)=>{
|
|
1829
|
+
const y = categoryLaneY.get(cat);
|
|
1830
|
+
if(y==null) return;
|
|
1831
|
+
const laneRect = document.createElementNS('http://www.w3.org/2000/svg','rect');
|
|
1832
|
+
laneRect.setAttribute('x', String(gutterWidth - 8)); laneRect.setAttribute('y', String(y - 10));
|
|
1833
|
+
laneRect.setAttribute('width', String(totalWidth - (gutterWidth - 8))); laneRect.setAttribute('height', String(laneHeight));
|
|
1834
|
+
laneRect.setAttribute('fill', i % 2 === 0 ? '#1b2734' : '#1d2c3b'); laneRect.setAttribute('opacity','0.55');
|
|
1835
|
+
svg.appendChild(laneRect);
|
|
1836
|
+
});
|
|
1837
|
+
// Gutter separator line
|
|
1838
|
+
const gutterSep = document.createElementNS('http://www.w3.org/2000/svg','line');
|
|
1839
|
+
gutterSep.setAttribute('x1', String(gutterWidth - 12)); gutterSep.setAttribute('x2', String(gutterWidth - 12));
|
|
1840
|
+
gutterSep.setAttribute('y1','0'); gutterSep.setAttribute('y2', String(totalHeight));
|
|
1841
|
+
gutterSep.setAttribute('stroke','#253548'); gutterSep.setAttribute('stroke-width','1'); gutterSep.setAttribute('opacity','0.9');
|
|
1842
|
+
svg.appendChild(gutterSep);
|
|
1843
|
+
// Vertical guideline (optional future: grid) -- skipped for now
|
|
1844
|
+
// Continue with edges & nodes below
|
|
1845
|
+
// NOTE: status text updated later includes '(ELK snapped)'
|
|
1846
|
+
function avg(arr){ return arr.length? arr.reduce((s,v)=> s+v,0)/arr.length : 0; }
|
|
1847
|
+
// Draw edges from elkGraph edges using node centers (after snapping)
|
|
1848
|
+
const edgeGroup = document.createElementNS('http://www.w3.org/2000/svg','g');
|
|
1849
|
+
edgeGroup.setAttribute('stroke','#5479ff'); edgeGroup.setAttribute('stroke-width','1.1'); edgeGroup.setAttribute('fill','none'); edgeGroup.setAttribute('opacity','0.9');
|
|
1850
|
+
elkGraph.edges.forEach(e=>{
|
|
1851
|
+
const a = positions.get(e.sources[0]); const b = positions.get(e.targets[0]); if(!a||!b) return;
|
|
1852
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg','path');
|
|
1853
|
+
const hdx = Math.max(40, (b.x - a.x)/3);
|
|
1854
|
+
const d = `M ${a.x + nodeWidth} ${a.y + nodeHeight/2} C ${a.x + nodeWidth + hdx} ${a.y + nodeHeight/2}, ${b.x - hdx} ${b.y + nodeHeight/2}, ${b.x} ${b.y + nodeHeight/2}`;
|
|
1855
|
+
path.setAttribute('d', d);
|
|
1856
|
+
edgeGroup.appendChild(path);
|
|
1857
|
+
instrEdgeCount++;
|
|
1858
|
+
});
|
|
1859
|
+
svg.appendChild(edgeGroup);
|
|
1860
|
+
// Arrowhead marker definition (once)
|
|
1861
|
+
const defs = document.createElementNS('http://www.w3.org/2000/svg','defs');
|
|
1862
|
+
const marker = document.createElementNS('http://www.w3.org/2000/svg','marker');
|
|
1863
|
+
marker.setAttribute('id','drill-arrow'); marker.setAttribute('markerWidth','10'); marker.setAttribute('markerHeight','6'); marker.setAttribute('refX','10'); marker.setAttribute('refY','3'); marker.setAttribute('orient','auto');
|
|
1864
|
+
const mpath = document.createElementNS('http://www.w3.org/2000/svg','path'); mpath.setAttribute('d','M0,0 L10,3 L0,6 Z'); mpath.setAttribute('fill','#5479ff'); marker.appendChild(mpath); defs.appendChild(marker); svg.appendChild(defs);
|
|
1865
|
+
edgeGroup.setAttribute('marker-end','url(#drill-arrow)');
|
|
1866
|
+
// Category membership connectors (pseudo category anchors in gutter)
|
|
1867
|
+
if(membershipEnabled){
|
|
1868
|
+
const membershipGroup = document.createElementNS('http://www.w3.org/2000/svg','g');
|
|
1869
|
+
membershipGroup.setAttribute('stroke','#44566d'); membershipGroup.setAttribute('stroke-width','1'); membershipGroup.setAttribute('opacity','0.8');
|
|
1870
|
+
membershipGroup.setAttribute('data-role','membership-group');
|
|
1871
|
+
instructions.forEach(inst=>{
|
|
1872
|
+
const p = positions.get(inst.id); if(!p) return;
|
|
1873
|
+
const yMid = p.y + nodeHeight/2;
|
|
1874
|
+
const startX = gutterWidth - 15; const endX = p.x; // horizontal connector
|
|
1875
|
+
const conn = document.createElementNS('http://www.w3.org/2000/svg','path');
|
|
1876
|
+
conn.setAttribute('d', `M ${startX} ${yMid} L ${endX} ${yMid}`);
|
|
1877
|
+
membershipGroup.appendChild(conn);
|
|
1878
|
+
});
|
|
1879
|
+
svg.appendChild(membershipGroup);
|
|
1880
|
+
}
|
|
1881
|
+
// Nodes (interactive)
|
|
1882
|
+
instructions.forEach(inst=>{
|
|
1883
|
+
const pos = positions.get(inst.id); if(!pos) return;
|
|
1884
|
+
const g = document.createElementNS('http://www.w3.org/2000/svg','g');
|
|
1885
|
+
g.setAttribute('transform', `translate(${pos.x},${pos.y})`);
|
|
1886
|
+
g.setAttribute('data-id', inst.id);
|
|
1887
|
+
g.setAttribute('data-cat', (inst.categories&&inst.categories[0])||'uncategorized');
|
|
1888
|
+
g.style.cursor='pointer';
|
|
1889
|
+
const rect = document.createElementNS('http://www.w3.org/2000/svg','rect');
|
|
1890
|
+
rect.setAttribute('width', String(nodeWidth)); rect.setAttribute('height', String(nodeHeight)); rect.setAttribute('rx','6');
|
|
1891
|
+
rect.setAttribute('fill','#3a4554'); rect.setAttribute('stroke','#6b8cff');
|
|
1892
|
+
rect.classList.add('drill-node-rect');
|
|
1893
|
+
const label = document.createElementNS('http://www.w3.org/2000/svg','text');
|
|
1894
|
+
label.setAttribute('x','8'); label.setAttribute('y','20'); label.setAttribute('fill','#e3ebf5'); label.textContent = inst.id;
|
|
1895
|
+
label.setAttribute('pointer-events','none');
|
|
1896
|
+
// Tooltip
|
|
1897
|
+
const title = document.createElementNS('http://www.w3.org/2000/svg','title'); title.textContent = inst.id; g.appendChild(title);
|
|
1898
|
+
g.appendChild(rect); g.appendChild(label); svg.appendChild(g);
|
|
1899
|
+
g.addEventListener('mouseenter', ()=> rect.setAttribute('stroke','#8fb2ff'));
|
|
1900
|
+
g.addEventListener('mouseleave', ()=>{ if(!g.classList.contains('selected')) rect.setAttribute('stroke','#6b8cff'); });
|
|
1901
|
+
g.addEventListener('click', ()=> selectDrillNode(inst.id));
|
|
1902
|
+
});
|
|
1903
|
+
// Category labels (lane centered)
|
|
1904
|
+
categories.forEach((cat,i)=>{
|
|
1905
|
+
const yCenter = (categoryLaneY.get(cat)||0) + (laneHeight/2) - 10; // adjust for padding applied earlier
|
|
1906
|
+
const t = document.createElementNS('http://www.w3.org/2000/svg','text');
|
|
1907
|
+
t.setAttribute('x','10'); t.setAttribute('y', String(yCenter)); t.setAttribute('fill','#9fb5cc'); t.setAttribute('font-weight','600'); t.textContent = cat; svg.appendChild(t);
|
|
1908
|
+
// Anchor circle for filtering
|
|
1909
|
+
const anchor = document.createElementNS('http://www.w3.org/2000/svg','circle');
|
|
1910
|
+
anchor.setAttribute('cx', String(110)); // inside gutter
|
|
1911
|
+
anchor.setAttribute('cy', String(yCenter-2));
|
|
1912
|
+
anchor.setAttribute('r','6');
|
|
1913
|
+
anchor.setAttribute('fill','#2e3d50');
|
|
1914
|
+
anchor.setAttribute('stroke','#6b8cff');
|
|
1915
|
+
anchor.setAttribute('data-cat-anchor', cat);
|
|
1916
|
+
anchor.style.cursor='pointer';
|
|
1917
|
+
anchor.addEventListener('click', ()=> toggleLaneFilter(cat));
|
|
1918
|
+
const anchorTitle = document.createElementNS('http://www.w3.org/2000/svg','title'); anchorTitle.textContent = 'Filter lane: '+cat; anchor.appendChild(anchorTitle);
|
|
1919
|
+
svg.appendChild(anchor);
|
|
1920
|
+
});
|
|
1921
|
+
if(status) status.textContent = `rendered ${instructions.length} nodes (ELK snapped) - categories ${categories.length}`;
|
|
1922
|
+
} catch(elkErr){ if(status) status.textContent = 'ELK layout failed; using manual lanes (' + (elkErr?.message||elkErr) + ')'; }
|
|
1923
|
+
}
|
|
1924
|
+
if(!usedElk){
|
|
1925
|
+
// Manual lane fallback (original implementation)
|
|
1926
|
+
const categoriesManual = [...new Set(instructions.map(i=> (i.categories&&i.categories[0])||'uncategorized'))].sort();
|
|
1927
|
+
const catIndex = new Map(categoriesManual.map((c,i)=> [c,i]));
|
|
1928
|
+
const laneHeight = 70; const marginLeft = 180; const hGap = 40; const vPad = 24;
|
|
1929
|
+
const laneCounters = new Map(categoriesManual.map(c=> [c,0]));
|
|
1930
|
+
positions = new Map();
|
|
1931
|
+
instructions.forEach(inst=>{
|
|
1932
|
+
const lane = (inst.categories&&inst.categories[0])||'uncategorized';
|
|
1933
|
+
const idx = laneCounters.get(lane) || 0;
|
|
1934
|
+
laneCounters.set(lane, idx+1);
|
|
1935
|
+
const x = marginLeft + idx*(nodeWidth + hGap);
|
|
1936
|
+
const y = vPad + (catIndex.get(lane)||0)*laneHeight;
|
|
1937
|
+
positions.set(inst.id, { x, y });
|
|
1938
|
+
});
|
|
1939
|
+
const totalWidth = Math.max(800, marginLeft + Math.max(1, ...[...laneCounters.values()])* (nodeWidth + hGap));
|
|
1940
|
+
const totalHeight = vPad + categoriesManual.length * laneHeight;
|
|
1941
|
+
svg.setAttribute('width', String(totalWidth));
|
|
1942
|
+
svg.setAttribute('height', String(totalHeight));
|
|
1943
|
+
svg.innerHTML='';
|
|
1944
|
+
// Lane separators + labels
|
|
1945
|
+
categoriesManual.forEach(cat=>{
|
|
1946
|
+
const laneY = vPad + (catIndex.get(cat)||0)*laneHeight;
|
|
1947
|
+
const yMid = laneY + nodeHeight/2;
|
|
1948
|
+
const sep = document.createElementNS('http://www.w3.org/2000/svg','line');
|
|
1949
|
+
sep.setAttribute('x1','0'); sep.setAttribute('x2', String(totalWidth)); sep.setAttribute('y1', String(laneY + laneHeight - 8)); sep.setAttribute('y2', String(laneY + laneHeight - 8)); sep.setAttribute('stroke','#1f2e40'); sep.setAttribute('stroke-width','1'); sep.setAttribute('opacity','0.35'); svg.appendChild(sep);
|
|
1950
|
+
const text = document.createElementNS('http://www.w3.org/2000/svg','text');
|
|
1951
|
+
text.setAttribute('x','10'); text.setAttribute('y', String(yMid)); text.setAttribute('fill','#9fb5cc'); text.setAttribute('font-weight','600'); text.textContent = cat; svg.appendChild(text);
|
|
1952
|
+
});
|
|
1953
|
+
const edgeGroup = document.createElementNS('http://www.w3.org/2000/svg','g');
|
|
1954
|
+
edgeGroup.setAttribute('stroke','#5479ff'); edgeGroup.setAttribute('stroke-width','1.2'); edgeGroup.setAttribute('fill','none'); edgeGroup.setAttribute('opacity','0.9');
|
|
1955
|
+
const defs = document.createElementNS('http://www.w3.org/2000/svg','defs');
|
|
1956
|
+
const marker = document.createElementNS('http://www.w3.org/2000/svg','marker');
|
|
1957
|
+
marker.setAttribute('id','drill-arrow'); marker.setAttribute('markerWidth','10'); marker.setAttribute('markerHeight','6'); marker.setAttribute('refX','10'); marker.setAttribute('refY','3'); marker.setAttribute('orient','auto');
|
|
1958
|
+
const mpath = document.createElementNS('http://www.w3.org/2000/svg','path'); mpath.setAttribute('d','M0,0 L10,3 L0,6 Z'); mpath.setAttribute('fill','#5479ff'); marker.appendChild(mpath); defs.appendChild(marker); svg.appendChild(defs);
|
|
1959
|
+
edgeGroup.setAttribute('marker-end','url(#drill-arrow)');
|
|
1960
|
+
j.edges.filter(e=> positions.has(e.from) && positions.has(e.to)).forEach(e=>{
|
|
1961
|
+
const a = positions.get(e.from); const b = positions.get(e.to); if(!a||!b) return;
|
|
1962
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg','path');
|
|
1963
|
+
const dx = (b.x - a.x)/2;
|
|
1964
|
+
const d = `M ${a.x + nodeWidth} ${a.y + nodeHeight/2} C ${a.x + nodeWidth + dx} ${a.y + nodeHeight/2}, ${b.x - dx} ${b.y + nodeHeight/2}, ${b.x} ${b.y + nodeHeight/2}`;
|
|
1965
|
+
path.setAttribute('d', d); path.setAttribute('stroke', e.type==='category'? '#8fb2ff':'#5479ff'); edgeGroup.appendChild(path);
|
|
1966
|
+
instrEdgeCount++;
|
|
1967
|
+
});
|
|
1968
|
+
svg.appendChild(edgeGroup);
|
|
1969
|
+
if(membershipEnabled){
|
|
1970
|
+
const membershipGroup = document.createElementNS('http://www.w3.org/2000/svg','g');
|
|
1971
|
+
membershipGroup.setAttribute('stroke','#44566d'); membershipGroup.setAttribute('stroke-width','1'); membershipGroup.setAttribute('opacity','0.8');
|
|
1972
|
+
membershipGroup.setAttribute('data-role','membership-group');
|
|
1973
|
+
instructions.forEach(inst=>{
|
|
1974
|
+
const pos = positions.get(inst.id); if(!pos) return;
|
|
1975
|
+
const yMid = pos.y + nodeHeight/2;
|
|
1976
|
+
const conn = document.createElementNS('http://www.w3.org/2000/svg','path');
|
|
1977
|
+
conn.setAttribute('d', `M 150 ${yMid} L ${pos.x} ${yMid}`);
|
|
1978
|
+
membershipGroup.appendChild(conn);
|
|
1979
|
+
});
|
|
1980
|
+
svg.appendChild(membershipGroup);
|
|
1981
|
+
}
|
|
1982
|
+
instructions.forEach(inst=>{
|
|
1983
|
+
const pos = positions.get(inst.id); if(!pos) return;
|
|
1984
|
+
const g = document.createElementNS('http://www.w3.org/2000/svg','g'); g.setAttribute('transform', `translate(${pos.x},${pos.y})`); g.setAttribute('data-id', inst.id); g.style.cursor='pointer';
|
|
1985
|
+
g.setAttribute('data-cat', (inst.categories&&inst.categories[0])||'uncategorized');
|
|
1986
|
+
const rect = document.createElementNS('http://www.w3.org/2000/svg','rect'); rect.setAttribute('width', String(nodeWidth)); rect.setAttribute('height', String(nodeHeight)); rect.setAttribute('rx','6'); rect.setAttribute('fill','#3a4554'); rect.setAttribute('stroke','#6b8cff'); rect.classList.add('drill-node-rect');
|
|
1987
|
+
const label = document.createElementNS('http://www.w3.org/2000/svg','text'); label.setAttribute('x','8'); label.setAttribute('y','20'); label.setAttribute('fill','#e3ebf5'); label.textContent = inst.id; label.setAttribute('pointer-events','none');
|
|
1988
|
+
const title = document.createElementNS('http://www.w3.org/2000/svg','title'); title.textContent = inst.id; g.appendChild(title);
|
|
1989
|
+
g.appendChild(rect); g.appendChild(label); svg.appendChild(g);
|
|
1990
|
+
g.addEventListener('mouseenter', ()=> rect.setAttribute('stroke','#8fb2ff'));
|
|
1991
|
+
g.addEventListener('mouseleave', ()=>{ if(!g.classList.contains('selected')) rect.setAttribute('stroke','#6b8cff'); });
|
|
1992
|
+
g.addEventListener('click', ()=> selectDrillNode(inst.id));
|
|
1993
|
+
});
|
|
1994
|
+
if(status) status.textContent = `rendered ${instructions.length} nodes across ${categoriesManual.length} lane(s)`;
|
|
1995
|
+
}
|
|
1996
|
+
// Legend update
|
|
1997
|
+
updateDrillLegend({
|
|
1998
|
+
selected: ids.length,
|
|
1999
|
+
expanded: j.expanded||0,
|
|
2000
|
+
total: instructions.length,
|
|
2001
|
+
edges: instrEdgeCount,
|
|
2002
|
+
membership: membershipEnabled? instructions.length:0,
|
|
2003
|
+
layout: usedElk? 'ELK':'Manual',
|
|
2004
|
+
expansionEnabled: expandEnabled
|
|
2005
|
+
});
|
|
2006
|
+
} catch(e){ if(status) status.textContent='render error: ' + (e?.message||e); }
|
|
2007
|
+
}
|
|
2008
|
+
function updateDrillLegend(data){
|
|
2009
|
+
const el = document.getElementById('drill-legend'); if(!el) return;
|
|
2010
|
+
el.innerHTML = `<div class="drill-legend-title">Legend / Stats</div>` +
|
|
2011
|
+
`<div class="drill-legend-body">`+
|
|
2012
|
+
`Selected: <b>${data.selected}</b>${data.expansionEnabled? ' (expand '+(data.expanded||0)+')':''}<br>`+
|
|
2013
|
+
`Rendered Instructions: <b>${data.total}</b><br>`+
|
|
2014
|
+
`Instruction Edges: <b>${data.edges}</b><br>`+
|
|
2015
|
+
`Membership Lines: <b>${data.membership}</b> ${data.membership? '(primary category connectors)':''}<br>`+
|
|
2016
|
+
`Layout: <b>${data.layout}</b>${data.expansionEnabled? ' + expand':''}`+
|
|
2017
|
+
`</div>`;
|
|
2018
|
+
}
|
|
2019
|
+
function exportDrillSvg(){
|
|
2020
|
+
const svg = document.getElementById('drill-svg'); if(!svg || !svg.firstChild) return;
|
|
2021
|
+
const clone = svg.cloneNode(true);
|
|
2022
|
+
if(clone instanceof SVGElement){
|
|
2023
|
+
clone.setAttribute('xmlns','http://www.w3.org/2000/svg');
|
|
2024
|
+
}
|
|
2025
|
+
const ser = new XMLSerializer();
|
|
2026
|
+
const data = ser.serializeToString(clone);
|
|
2027
|
+
const blob = new Blob([data], { type:'image/svg+xml;charset=utf-8' });
|
|
2028
|
+
const url = URL.createObjectURL(blob);
|
|
2029
|
+
const a = document.createElement('a');
|
|
2030
|
+
const ts = new Date().toISOString().replace(/[:T]/g,'-').slice(0,19);
|
|
2031
|
+
a.href = url; a.download = 'drilldown-graph-'+ts+'.svg';
|
|
2032
|
+
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
|
2033
|
+
setTimeout(()=> URL.revokeObjectURL(url), 2500);
|
|
2034
|
+
}
|
|
2035
|
+
function applyLaneFilter(){
|
|
2036
|
+
const active = window.__drillLaneFilter || null;
|
|
2037
|
+
const svg = document.getElementById('drill-svg'); if(!svg) return;
|
|
2038
|
+
const nodes = svg.querySelectorAll('g[data-id]');
|
|
2039
|
+
nodes.forEach(n=>{
|
|
2040
|
+
const cat = n.getAttribute('data-cat');
|
|
2041
|
+
if(!active || cat===active){
|
|
2042
|
+
n.style.opacity='1';
|
|
2043
|
+
} else {
|
|
2044
|
+
n.style.opacity='0.15';
|
|
2045
|
+
}
|
|
2046
|
+
});
|
|
2047
|
+
// Dull membership lines when filter active
|
|
2048
|
+
const membershipGroup = svg.querySelector('[data-role="membership-group"]');
|
|
2049
|
+
if(membershipGroup){ membershipGroup.setAttribute('opacity', active? '0.25':'0.8'); }
|
|
2050
|
+
// Highlight anchors
|
|
2051
|
+
svg.querySelectorAll('[data-cat-anchor]').forEach(a=>{
|
|
2052
|
+
if(a.getAttribute('data-cat-anchor')===active){ a.setAttribute('fill','#6b8cff'); a.setAttribute('stroke','#ffd36b'); }
|
|
2053
|
+
else { a.setAttribute('fill','#2e3d50'); a.setAttribute('stroke','#6b8cff'); }
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
function toggleLaneFilter(cat){
|
|
2057
|
+
if(window.__drillLaneFilter === cat){ window.__drillLaneFilter = null; }
|
|
2058
|
+
else { window.__drillLaneFilter = cat; }
|
|
2059
|
+
applyLaneFilter();
|
|
2060
|
+
}
|
|
2061
|
+
// Control wiring (idempotent safeguard)
|
|
2062
|
+
(function(){
|
|
2063
|
+
const exp = document.getElementById('drill-expand'); if(exp && !exp.__wired){ exp.addEventListener('change', ()=> renderDrillSvg()); exp.__wired=true; }
|
|
2064
|
+
const mem = document.getElementById('drill-show-membership'); if(mem && !mem.__wired){ mem.addEventListener('change', ()=> renderDrillSvg()); mem.__wired=true; }
|
|
2065
|
+
const exb = document.getElementById('drill-export'); if(exb && !exb.__wired){ exb.addEventListener('click', exportDrillSvg); exb.__wired=true; }
|
|
2066
|
+
const hed = document.getElementById('mermaid-high-edges');
|
|
2067
|
+
if(hed && !hed.__wired){
|
|
2068
|
+
hed.addEventListener('change', async ()=>{
|
|
2069
|
+
// Set override variable then force reload mermaid core
|
|
2070
|
+
if(hed.checked){ window.__MERMAID_MAX_EDGES = 10000; } else { window.__MERMAID_MAX_EDGES = 3000; }
|
|
2071
|
+
await ensureMermaid(true);
|
|
2072
|
+
// Re-render existing mermaid graph if source present
|
|
2073
|
+
try {
|
|
2074
|
+
const srcEl = document.getElementById('graph-mermaid');
|
|
2075
|
+
const host = document.getElementById('graph-mermaid-svg');
|
|
2076
|
+
if(srcEl && host && window.mermaid){
|
|
2077
|
+
const codeRaw = srcEl.textContent || '';
|
|
2078
|
+
const code = ensureMermaidDirective(codeRaw);
|
|
2079
|
+
host.innerHTML = `<div class="mermaid">${code}</div>`;
|
|
2080
|
+
await window.mermaid.run({ querySelector:'#graph-mermaid-svg .mermaid' });
|
|
2081
|
+
if(window.__GRAPH_DEBUG || document.getElementById('graph-debug')?.checked){
|
|
2082
|
+
graphLog('re-render after high edge cap toggle', { firstLine: code.split(/\n/)[0] });
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
const meta2 = document.getElementById('graph-meta2');
|
|
2086
|
+
if(meta2){ meta2.textContent = (meta2.textContent||'') + ` | maxEdges=${window.__MERMAID_ACTIVE_MAX_EDGES}`; }
|
|
2087
|
+
} catch(e){ console.warn('Mermaid re-render after high-edge toggle failed', e); }
|
|
2088
|
+
});
|
|
2089
|
+
hed.__wired = true;
|
|
2090
|
+
}
|
|
2091
|
+
const lg = document.getElementById('mermaid-large-graph');
|
|
2092
|
+
if(lg && !lg.__wired){
|
|
2093
|
+
lg.addEventListener('change', async ()=>{
|
|
2094
|
+
window.__MERMAID_LARGE_GRAPH_FLAG = !!lg.checked;
|
|
2095
|
+
// Clear explicit override so flag logic chooses caps unless high-edge override also set.
|
|
2096
|
+
if(!window.__MERMAID_LARGE_GRAPH_FLAG && typeof window.__MERMAID_MAX_EDGES === 'number'){
|
|
2097
|
+
// keep existing manual override; do nothing
|
|
2098
|
+
}
|
|
2099
|
+
await ensureMermaid(true);
|
|
2100
|
+
try {
|
|
2101
|
+
const srcEl = document.getElementById('graph-mermaid');
|
|
2102
|
+
const host = document.getElementById('graph-mermaid-svg');
|
|
2103
|
+
if(srcEl && host && window.mermaid){
|
|
2104
|
+
const codeRaw = srcEl.textContent || '';
|
|
2105
|
+
const code = ensureMermaidDirective(codeRaw);
|
|
2106
|
+
host.innerHTML = `<div class=\"mermaid\">${code}</div>`;
|
|
2107
|
+
await window.mermaid.run({ querySelector:'#graph-mermaid-svg .mermaid' });
|
|
2108
|
+
if(window.__GRAPH_DEBUG || document.getElementById('graph-debug')?.checked){
|
|
2109
|
+
graphLog('re-render after large graph toggle', { firstLine: code.split(/\n/)[0], largeGraph: window.__MERMAID_LARGE_GRAPH_FLAG });
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
const meta2 = document.getElementById('graph-meta2');
|
|
2113
|
+
if(meta2){ meta2.textContent = (meta2.textContent||'') + ` | largeGraph=${window.__MERMAID_LARGE_GRAPH_FLAG} maxEdges=${window.__MERMAID_ACTIVE_MAX_EDGES} maxTextSize=${window.__MERMAID_ACTIVE_MAX_TEXT_SIZE}`; }
|
|
2114
|
+
} catch(e){ console.warn('Mermaid re-render after large-graph toggle failed', e); }
|
|
2115
|
+
});
|
|
2116
|
+
lg.__wired = true;
|
|
2117
|
+
}
|
|
2118
|
+
})();
|
|
2119
|
+
function selectDrillNode(id){
|
|
2120
|
+
const svg = document.getElementById('drill-svg'); if(!svg) return;
|
|
2121
|
+
// Clear previous selection
|
|
2122
|
+
[...svg.querySelectorAll('g.selected')].forEach(g=>{ g.classList.remove('selected'); const rect = g.querySelector('rect.drill-node-rect'); if(rect) rect.setAttribute('stroke','#6b8cff'); });
|
|
2123
|
+
const target = svg.querySelector(`g[data-id="${CSS.escape(id)}"]`);
|
|
2124
|
+
if(target){
|
|
2125
|
+
target.classList.add('selected'); const rect = target.querySelector('rect.drill-node-rect'); if(rect) rect.setAttribute('stroke','#ffd36b');
|
|
2126
|
+
}
|
|
2127
|
+
const data = window.__lastDrillData; const detail = document.getElementById('drill-detail');
|
|
2128
|
+
if(!data || !detail) return;
|
|
2129
|
+
const inst = data.instructions.find(i=> i.id===id); if(!inst){ detail.style.display='none'; return; }
|
|
2130
|
+
// Compute incident edges
|
|
2131
|
+
const edges = data.raw.edges.filter(e=> e.from===id || e.to===id);
|
|
2132
|
+
const incoming = edges.filter(e=> e.to===id).map(e=> e.from);
|
|
2133
|
+
const outgoing = edges.filter(e=> e.from===id).map(e=> e.to);
|
|
2134
|
+
const cats = inst.categories || [];
|
|
2135
|
+
detail.style.display='block';
|
|
2136
|
+
detail.innerHTML = `<div class="drill-detail-heading">📄 ${id}</div>` +
|
|
2137
|
+
`<div class="drill-detail-sub">Primary: ${(inst.primaryCategory||cats[0]||'–')}` +
|
|
2138
|
+
(cats.length? ` | Categories: ${cats.join(', ')}`:'') + `</div>` +
|
|
2139
|
+
`<div class="drill-detail-section">Incoming (${incoming.length}): ${incoming.length? incoming.join(', '):'none'}</div>` +
|
|
2140
|
+
`<div class="drill-detail-section">Outgoing (${outgoing.length}): ${outgoing.length? outgoing.join(', '):'none'}</div>` +
|
|
2141
|
+
`<div class="drill-detail-footer">Click another node to update. (Future: open file / copy / filter.)</div>`;
|
|
2142
|
+
}
|
|
2143
|
+
// Auto-preload categories when graph section first opened (best-effort)
|
|
2144
|
+
document.addEventListener('visibilitychange', ()=>{
|
|
2145
|
+
if(!document.hidden){ const catSel = document.getElementById('drill-categories'); if(catSel && !catSel.options.length) refreshDrillCategories(); }
|
|
2146
|
+
});
|
|
2147
|
+
// ----------------------------------------------------------------------
|
|
2148
|
+
</script>
|
|
2149
|
+
</body>
|
|
2150
|
+
</html>
|