instar 0.23.17 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.claude/hooks/auto-approve-claude-edits.py +72 -0
  2. package/.claude/settings.json +65 -1
  3. package/dashboard/index.html +515 -0
  4. package/dist/cli.js +299 -0
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/init.d.ts.map +1 -1
  7. package/dist/commands/init.js +770 -311
  8. package/dist/commands/init.js.map +1 -1
  9. package/dist/commands/server.d.ts.map +1 -1
  10. package/dist/commands/server.js +171 -32
  11. package/dist/commands/server.js.map +1 -1
  12. package/dist/core/AutonomyProfileManager.d.ts +2 -0
  13. package/dist/core/AutonomyProfileManager.d.ts.map +1 -1
  14. package/dist/core/AutonomyProfileManager.js +13 -0
  15. package/dist/core/AutonomyProfileManager.js.map +1 -1
  16. package/dist/core/Config.d.ts.map +1 -1
  17. package/dist/core/Config.js +5 -3
  18. package/dist/core/Config.js.map +1 -1
  19. package/dist/core/DiscoveryEvaluator.d.ts +131 -0
  20. package/dist/core/DiscoveryEvaluator.d.ts.map +1 -0
  21. package/dist/core/DiscoveryEvaluator.js +377 -0
  22. package/dist/core/DiscoveryEvaluator.js.map +1 -0
  23. package/dist/core/FeatureDefinitions.d.ts +14 -0
  24. package/dist/core/FeatureDefinitions.d.ts.map +1 -0
  25. package/dist/core/FeatureDefinitions.js +374 -0
  26. package/dist/core/FeatureDefinitions.js.map +1 -0
  27. package/dist/core/FeatureRegistry.d.ts +337 -0
  28. package/dist/core/FeatureRegistry.d.ts.map +1 -0
  29. package/dist/core/FeatureRegistry.js +863 -0
  30. package/dist/core/FeatureRegistry.js.map +1 -0
  31. package/dist/core/ResponseReviewGate.d.ts +182 -0
  32. package/dist/core/ResponseReviewGate.d.ts.map +1 -0
  33. package/dist/core/ResponseReviewGate.js +956 -0
  34. package/dist/core/ResponseReviewGate.js.map +1 -0
  35. package/dist/core/SessionManager.d.ts +16 -0
  36. package/dist/core/SessionManager.d.ts.map +1 -1
  37. package/dist/core/SessionManager.js +65 -0
  38. package/dist/core/SessionManager.js.map +1 -1
  39. package/dist/core/SurfacingTemplates.d.ts +63 -0
  40. package/dist/core/SurfacingTemplates.d.ts.map +1 -0
  41. package/dist/core/SurfacingTemplates.js +138 -0
  42. package/dist/core/SurfacingTemplates.js.map +1 -0
  43. package/dist/core/TopicClassifier.d.ts +54 -0
  44. package/dist/core/TopicClassifier.d.ts.map +1 -0
  45. package/dist/core/TopicClassifier.js +144 -0
  46. package/dist/core/TopicClassifier.js.map +1 -0
  47. package/dist/core/TopicResumeMap.d.ts +16 -12
  48. package/dist/core/TopicResumeMap.d.ts.map +1 -1
  49. package/dist/core/TopicResumeMap.js +42 -43
  50. package/dist/core/TopicResumeMap.js.map +1 -1
  51. package/dist/core/types.d.ts +93 -0
  52. package/dist/core/types.d.ts.map +1 -1
  53. package/dist/core/types.js.map +1 -1
  54. package/dist/knowledge/TreeGenerator.d.ts.map +1 -1
  55. package/dist/knowledge/TreeGenerator.js +14 -0
  56. package/dist/knowledge/TreeGenerator.js.map +1 -1
  57. package/dist/memory/SemanticMemory.d.ts +44 -0
  58. package/dist/memory/SemanticMemory.d.ts.map +1 -1
  59. package/dist/memory/SemanticMemory.js +179 -2
  60. package/dist/memory/SemanticMemory.js.map +1 -1
  61. package/dist/monitoring/DegradationReporter.d.ts +6 -0
  62. package/dist/monitoring/DegradationReporter.d.ts.map +1 -1
  63. package/dist/monitoring/DegradationReporter.js +26 -3
  64. package/dist/monitoring/DegradationReporter.js.map +1 -1
  65. package/dist/monitoring/HomeostasisMonitor.d.ts +102 -0
  66. package/dist/monitoring/HomeostasisMonitor.d.ts.map +1 -0
  67. package/dist/monitoring/HomeostasisMonitor.js +185 -0
  68. package/dist/monitoring/HomeostasisMonitor.js.map +1 -0
  69. package/dist/monitoring/QuotaManager.d.ts +5 -0
  70. package/dist/monitoring/QuotaManager.d.ts.map +1 -1
  71. package/dist/monitoring/QuotaManager.js +23 -4
  72. package/dist/monitoring/QuotaManager.js.map +1 -1
  73. package/dist/monitoring/QuotaNotifier.d.ts.map +1 -1
  74. package/dist/monitoring/QuotaNotifier.js +12 -5
  75. package/dist/monitoring/QuotaNotifier.js.map +1 -1
  76. package/dist/monitoring/QuotaTracker.d.ts +3 -3
  77. package/dist/monitoring/QuotaTracker.js +3 -3
  78. package/dist/monitoring/SessionWatchdog.d.ts +41 -0
  79. package/dist/monitoring/SessionWatchdog.d.ts.map +1 -1
  80. package/dist/monitoring/SessionWatchdog.js +137 -0
  81. package/dist/monitoring/SessionWatchdog.js.map +1 -1
  82. package/dist/monitoring/SystemReviewer.js +11 -11
  83. package/dist/monitoring/SystemReviewer.js.map +1 -1
  84. package/dist/monitoring/TelemetryAuth.d.ts +64 -0
  85. package/dist/monitoring/TelemetryAuth.d.ts.map +1 -0
  86. package/dist/monitoring/TelemetryAuth.js +141 -0
  87. package/dist/monitoring/TelemetryAuth.js.map +1 -0
  88. package/dist/monitoring/TelemetryCollector.d.ts +62 -0
  89. package/dist/monitoring/TelemetryCollector.d.ts.map +1 -0
  90. package/dist/monitoring/TelemetryCollector.js +291 -0
  91. package/dist/monitoring/TelemetryCollector.js.map +1 -0
  92. package/dist/monitoring/TelemetryHeartbeat.d.ts +74 -13
  93. package/dist/monitoring/TelemetryHeartbeat.d.ts.map +1 -1
  94. package/dist/monitoring/TelemetryHeartbeat.js +249 -15
  95. package/dist/monitoring/TelemetryHeartbeat.js.map +1 -1
  96. package/dist/scaffold/templates.d.ts.map +1 -1
  97. package/dist/scaffold/templates.js +47 -0
  98. package/dist/scaffold/templates.js.map +1 -1
  99. package/dist/scheduler/JobScheduler.d.ts.map +1 -1
  100. package/dist/scheduler/JobScheduler.js +2 -1
  101. package/dist/scheduler/JobScheduler.js.map +1 -1
  102. package/dist/server/AgentServer.d.ts +2 -0
  103. package/dist/server/AgentServer.d.ts.map +1 -1
  104. package/dist/server/AgentServer.js +2 -0
  105. package/dist/server/AgentServer.js.map +1 -1
  106. package/dist/server/routes.d.ts +2 -0
  107. package/dist/server/routes.d.ts.map +1 -1
  108. package/dist/server/routes.js +614 -7
  109. package/dist/server/routes.js.map +1 -1
  110. package/package.json +2 -2
  111. package/scripts/telemetry-worker/worker.js +593 -19
  112. package/src/data/builtin-manifest.json +175 -135
  113. package/src/templates/hooks/compaction-recovery.sh +43 -0
  114. package/src/templates/hooks/session-start.sh +60 -0
  115. package/upgrades/0.23.18.md +41 -0
  116. package/upgrades/0.24.0.md +41 -0
  117. package/upgrades/NEXT.md +35 -0
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PermissionRequest hook: Auto-approve operations that autonomous sessions need.
4
+
5
+ Claude Code v2.1.81+ prompts for confirmation on more operations than before,
6
+ blocking autonomous sessions. This hook auto-approves:
7
+
8
+ 1. Edit/Write to .claude/ files (except settings.json/settings.local.json)
9
+ 2. Read from known-safe directories outside the project (e.g. ~/.dawn-server/)
10
+
11
+ Safety:
12
+ - Does NOT approve settings.json or settings.local.json edits
13
+ - Read approvals are limited to an explicit allowlist of safe directories
14
+ """
15
+ import json
16
+ import sys
17
+ import os
18
+
19
+ # Directories outside the project that are safe to read from
20
+ SAFE_READ_DIRS = [
21
+ os.path.expanduser("~/.dawn-server"),
22
+ os.path.expanduser("~/.claude"),
23
+ "/tmp/dawn-telegram",
24
+ "/tmp/dawn-",
25
+ ]
26
+
27
+ def allow():
28
+ """Print the allow decision and exit."""
29
+ result = {
30
+ "hookSpecificOutput": {
31
+ "hookEventName": "PermissionRequest",
32
+ "decision": {
33
+ "behavior": "allow"
34
+ }
35
+ }
36
+ }
37
+ print(json.dumps(result))
38
+
39
+ def main():
40
+ try:
41
+ input_data = json.loads(sys.stdin.read())
42
+ except (json.JSONDecodeError, EOFError):
43
+ return
44
+
45
+ tool_name = input_data.get("tool_name", "")
46
+ tool_input = input_data.get("tool_input", {})
47
+
48
+ file_path = tool_input.get("file_path", "") if isinstance(tool_input, dict) else str(tool_input)
49
+
50
+ # Handle Edit/Write to .claude/ files
51
+ if tool_name in ("Edit", "Write"):
52
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", "")
53
+ if project_dir and file_path.startswith(project_dir):
54
+ rel_path = file_path[len(project_dir):].lstrip("/")
55
+ else:
56
+ rel_path = file_path
57
+
58
+ if rel_path.startswith(".claude/"):
59
+ if rel_path in (".claude/settings.json", ".claude/settings.local.json"):
60
+ return
61
+ allow()
62
+ return
63
+
64
+ # Handle Read from known-safe directories
65
+ if tool_name == "Read":
66
+ for safe_dir in SAFE_READ_DIRS:
67
+ if file_path.startswith(safe_dir):
68
+ allow()
69
+ return
70
+
71
+ if __name__ == "__main__":
72
+ main()
@@ -11,7 +11,71 @@
11
11
  "timeout": 5000
12
12
  }
13
13
  ]
14
+ },
15
+ {
16
+ "matcher": "mcp__.*",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "node .instar/hooks/instar/external-operation-gate.js",
21
+ "blocking": true,
22
+ "timeout": 5000
23
+ }
24
+ ]
25
+ }
26
+ ],
27
+ "SessionStart": [
28
+ {
29
+ "matcher": "startup",
30
+ "hooks": [
31
+ {
32
+ "type": "command",
33
+ "command": "bash .instar/hooks/instar/session-start.sh",
34
+ "timeout": 5
35
+ }
36
+ ]
37
+ },
38
+ {
39
+ "matcher": "resume",
40
+ "hooks": [
41
+ {
42
+ "type": "command",
43
+ "command": "bash .instar/hooks/instar/session-start.sh",
44
+ "timeout": 5
45
+ }
46
+ ]
47
+ },
48
+ {
49
+ "matcher": "compact",
50
+ "hooks": [
51
+ {
52
+ "type": "command",
53
+ "command": "bash .instar/hooks/instar/session-start.sh",
54
+ "timeout": 5
55
+ }
56
+ ]
57
+ }
58
+ ],
59
+ "UserPromptSubmit": [
60
+ {
61
+ "matcher": "",
62
+ "hooks": [
63
+ {
64
+ "type": "command",
65
+ "command": "bash .instar/hooks/instar/telegram-topic-context.sh",
66
+ "timeout": 5000
67
+ }
68
+ ]
14
69
  }
15
70
  ]
71
+ },
72
+ "mcpServers": {
73
+ "playwright": {
74
+ "command": "npx",
75
+ "args": [
76
+ "-y",
77
+ "@playwright/mcp@latest"
78
+ ]
79
+ }
16
80
  }
17
- }
81
+ }
@@ -1915,6 +1915,274 @@
1915
1915
  .spark.s-pending { background: var(--blue); }
1916
1916
  .spark.s-skipped { background: #333; }
1917
1917
 
1918
+ /* ── Discovery Tab ──────────────────────────────────────── */
1919
+ .discovery-container {
1920
+ grid-column: 1 / -1;
1921
+ overflow-y: auto;
1922
+ background: var(--bg);
1923
+ padding: 20px;
1924
+ }
1925
+
1926
+ .discovery-main {
1927
+ max-width: 900px;
1928
+ margin: 0 auto;
1929
+ }
1930
+
1931
+ .discovery-header {
1932
+ display: flex;
1933
+ align-items: center;
1934
+ justify-content: space-between;
1935
+ margin-bottom: 20px;
1936
+ }
1937
+
1938
+ .discovery-header h2 {
1939
+ font-size: 16px;
1940
+ font-weight: 600;
1941
+ color: var(--text-bright);
1942
+ }
1943
+
1944
+ .discovery-refresh {
1945
+ font-size: 11px;
1946
+ padding: 4px 12px;
1947
+ border-radius: 4px;
1948
+ border: 1px solid var(--border);
1949
+ background: transparent;
1950
+ color: var(--text-dim);
1951
+ cursor: pointer;
1952
+ }
1953
+
1954
+ .discovery-refresh:hover {
1955
+ border-color: var(--text-dim);
1956
+ color: var(--text);
1957
+ }
1958
+
1959
+ .discovery-section {
1960
+ margin-bottom: 24px;
1961
+ }
1962
+
1963
+ .discovery-section h3 {
1964
+ font-size: 13px;
1965
+ font-weight: 600;
1966
+ color: var(--text-bright);
1967
+ margin-bottom: 12px;
1968
+ }
1969
+
1970
+ .funnel-chart {
1971
+ display: flex;
1972
+ gap: 4px;
1973
+ align-items: flex-end;
1974
+ height: 120px;
1975
+ padding: 12px 16px;
1976
+ background: var(--bg-panel);
1977
+ border: 1px solid var(--border);
1978
+ border-radius: 8px;
1979
+ }
1980
+
1981
+ .funnel-bar {
1982
+ flex: 1;
1983
+ display: flex;
1984
+ flex-direction: column;
1985
+ align-items: center;
1986
+ gap: 4px;
1987
+ }
1988
+
1989
+ .funnel-bar-fill {
1990
+ width: 100%;
1991
+ border-radius: 3px 3px 0 0;
1992
+ min-height: 2px;
1993
+ transition: height 0.3s;
1994
+ }
1995
+
1996
+ .funnel-bar-label {
1997
+ font-size: 10px;
1998
+ color: var(--text-dim);
1999
+ text-align: center;
2000
+ white-space: nowrap;
2001
+ }
2002
+
2003
+ .funnel-bar-count {
2004
+ font-size: 12px;
2005
+ font-weight: 600;
2006
+ color: var(--text-bright);
2007
+ }
2008
+
2009
+ .discovery-filter-bar {
2010
+ display: flex;
2011
+ gap: 4px;
2012
+ margin-bottom: 12px;
2013
+ flex-wrap: wrap;
2014
+ }
2015
+
2016
+ .discovery-filter {
2017
+ font-size: 11px;
2018
+ padding: 2px 8px;
2019
+ border-radius: 10px;
2020
+ border: 1px solid var(--border);
2021
+ background: transparent;
2022
+ color: var(--text-dim);
2023
+ cursor: pointer;
2024
+ transition: all 0.15s;
2025
+ }
2026
+
2027
+ .discovery-filter:hover {
2028
+ border-color: var(--text-dim);
2029
+ color: var(--text);
2030
+ }
2031
+
2032
+ .discovery-filter.active {
2033
+ background: var(--accent-dim);
2034
+ border-color: var(--accent-dim);
2035
+ color: #000;
2036
+ }
2037
+
2038
+ .feature-grid {
2039
+ display: grid;
2040
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
2041
+ gap: 10px;
2042
+ }
2043
+
2044
+ .feature-card {
2045
+ background: var(--bg-panel);
2046
+ border: 1px solid var(--border);
2047
+ border-radius: 8px;
2048
+ padding: 12px 14px;
2049
+ font-size: 12px;
2050
+ }
2051
+
2052
+ .feature-card-top {
2053
+ display: flex;
2054
+ align-items: center;
2055
+ gap: 8px;
2056
+ margin-bottom: 6px;
2057
+ }
2058
+
2059
+ .feature-card-name {
2060
+ font-weight: 500;
2061
+ color: var(--text-bright);
2062
+ flex: 1;
2063
+ }
2064
+
2065
+ .feature-state-badge {
2066
+ font-size: 10px;
2067
+ padding: 1px 6px;
2068
+ border-radius: 3px;
2069
+ font-weight: 600;
2070
+ }
2071
+
2072
+ .feature-state-badge.enabled { background: #0f2f0f; color: var(--accent); }
2073
+ .feature-state-badge.undiscovered { background: #1a1a1a; color: #666; }
2074
+ .feature-state-badge.aware { background: #0f1f2f; color: var(--blue); }
2075
+ .feature-state-badge.interested { background: #1f1f0f; color: #eab308; }
2076
+ .feature-state-badge.deferred { background: #1a1a1a; color: #888; }
2077
+ .feature-state-badge.declined { background: #2f0f0f; color: var(--red); }
2078
+ .feature-state-badge.disabled { background: #1a1a1a; color: #555; }
2079
+
2080
+ .feature-card-desc {
2081
+ color: var(--text-dim);
2082
+ font-size: 11px;
2083
+ line-height: 1.4;
2084
+ margin-bottom: 4px;
2085
+ }
2086
+
2087
+ .feature-card-meta {
2088
+ display: flex;
2089
+ gap: 8px;
2090
+ color: var(--text-dim);
2091
+ font-size: 10px;
2092
+ }
2093
+
2094
+ .feature-card-meta .tier { text-transform: uppercase; letter-spacing: 0.5px; }
2095
+ .feature-card-meta .cooldown { color: var(--orange); }
2096
+
2097
+ .event-log {
2098
+ background: var(--bg-panel);
2099
+ border: 1px solid var(--border);
2100
+ border-radius: 8px;
2101
+ max-height: 300px;
2102
+ overflow-y: auto;
2103
+ }
2104
+
2105
+ .event-row {
2106
+ display: flex;
2107
+ gap: 10px;
2108
+ padding: 8px 14px;
2109
+ border-bottom: 1px solid var(--border);
2110
+ font-size: 11px;
2111
+ align-items: center;
2112
+ }
2113
+
2114
+ .event-row:last-child { border-bottom: none; }
2115
+
2116
+ .event-time {
2117
+ color: var(--text-dim);
2118
+ flex-shrink: 0;
2119
+ width: 70px;
2120
+ }
2121
+
2122
+ .event-feature {
2123
+ color: var(--text-bright);
2124
+ font-weight: 500;
2125
+ flex-shrink: 0;
2126
+ width: 140px;
2127
+ }
2128
+
2129
+ .event-transition {
2130
+ color: var(--text);
2131
+ flex: 1;
2132
+ }
2133
+
2134
+ .event-arrow { color: var(--text-dim); }
2135
+
2136
+ .digest-item {
2137
+ background: var(--bg-panel);
2138
+ border: 1px solid var(--border);
2139
+ border-radius: 6px;
2140
+ padding: 10px 14px;
2141
+ margin-bottom: 8px;
2142
+ font-size: 12px;
2143
+ }
2144
+
2145
+ .digest-item-title {
2146
+ color: var(--text-bright);
2147
+ font-weight: 500;
2148
+ margin-bottom: 2px;
2149
+ }
2150
+
2151
+ .digest-item-desc {
2152
+ color: var(--text-dim);
2153
+ font-size: 11px;
2154
+ }
2155
+
2156
+ /* Discovery metrics summary */
2157
+ .discovery-metrics {
2158
+ display: flex;
2159
+ gap: 12px;
2160
+ margin-bottom: 16px;
2161
+ }
2162
+
2163
+ .metric-card {
2164
+ flex: 1;
2165
+ background: var(--bg-panel);
2166
+ border: 1px solid var(--border);
2167
+ border-radius: 8px;
2168
+ padding: 12px 14px;
2169
+ text-align: center;
2170
+ }
2171
+
2172
+ .metric-value {
2173
+ font-size: 22px;
2174
+ font-weight: 600;
2175
+ color: var(--text-bright);
2176
+ }
2177
+
2178
+ .metric-label {
2179
+ font-size: 10px;
2180
+ color: var(--text-dim);
2181
+ text-transform: uppercase;
2182
+ letter-spacing: 0.5px;
2183
+ margin-top: 2px;
2184
+ }
2185
+
1918
2186
  /* ── Mobile responsive ─────────────────────────────────── */
1919
2187
  @media (max-width: 768px) {
1920
2188
  .app {
@@ -2164,6 +2432,7 @@
2164
2432
  <button class="tab" data-tab="files" onclick="switchTab('files')">Files</button>
2165
2433
  <button class="tab" data-tab="dropzone" onclick="switchTab('dropzone')">Drop Zone</button>
2166
2434
  <button class="tab" data-tab="jobs" onclick="switchTab('jobs')">Jobs <span class="tab-count" id="tabJobCount">0</span></button>
2435
+ <button class="tab" data-tab="discovery" onclick="switchTab('discovery')">Discovery</button>
2167
2436
  </nav>
2168
2437
  </div>
2169
2438
  <div class="vital-signs" id="vitalSigns">
@@ -2383,6 +2652,52 @@
2383
2652
  <div class="jobs-detail-content" id="jobsDetailContent" style="display:none"></div>
2384
2653
  </div>
2385
2654
  </div>
2655
+
2656
+ <!-- Discovery Tab -->
2657
+ <div class="discovery-container" id="discoveryTab" style="display:none">
2658
+ <div class="discovery-main">
2659
+ <div class="discovery-header">
2660
+ <h2>Feature Discovery</h2>
2661
+ <button class="discovery-refresh" onclick="loadDiscovery()">Refresh</button>
2662
+ </div>
2663
+
2664
+ <!-- Funnel Visualization -->
2665
+ <div class="discovery-section">
2666
+ <h3>Discovery Funnel</h3>
2667
+ <div class="funnel-chart" id="funnelChart">
2668
+ <div style="padding:20px;color:var(--text-dim);text-align:center">Loading...</div>
2669
+ </div>
2670
+ </div>
2671
+
2672
+ <!-- Feature States Grid -->
2673
+ <div class="discovery-section">
2674
+ <h3>Feature States</h3>
2675
+ <div class="discovery-filter-bar">
2676
+ <button class="discovery-filter active" data-filter="all" onclick="setDiscoveryFilter('all')">All</button>
2677
+ <button class="discovery-filter" data-filter="enabled" onclick="setDiscoveryFilter('enabled')">Enabled</button>
2678
+ <button class="discovery-filter" data-filter="undiscovered" onclick="setDiscoveryFilter('undiscovered')">Undiscovered</button>
2679
+ <button class="discovery-filter" data-filter="aware" onclick="setDiscoveryFilter('aware')">Aware</button>
2680
+ <button class="discovery-filter" data-filter="cooldown" onclick="setDiscoveryFilter('cooldown')">In Cooldown</button>
2681
+ </div>
2682
+ <div class="feature-grid" id="featureGrid"></div>
2683
+ </div>
2684
+
2685
+ <!-- Digest Section -->
2686
+ <div class="discovery-section" id="digestSection" style="display:none">
2687
+ <h3>Digest</h3>
2688
+ <div id="digestContent"></div>
2689
+ </div>
2690
+
2691
+ <!-- Event Log -->
2692
+ <div class="discovery-section">
2693
+ <h3>Recent Events</h3>
2694
+ <div class="event-log" id="eventLog">
2695
+ <div style="padding:12px;color:var(--text-dim);text-align:center">No events yet</div>
2696
+ </div>
2697
+ </div>
2698
+ </div>
2699
+ </div>
2700
+
2386
2701
  </div>
2387
2702
 
2388
2703
  <!-- WhatsApp QR panel (hidden by default) -->
@@ -3293,6 +3608,12 @@
3293
3608
  },
3294
3609
  onDeactivate: () => { disconnectJobsSSE(); },
3295
3610
  },
3611
+ {
3612
+ id: 'discovery',
3613
+ panels: ['discoveryTab'],
3614
+ display: ['flex'],
3615
+ onActivate: () => { if (!discoveryLoaded) loadDiscovery(); },
3616
+ },
3296
3617
  ];
3297
3618
 
3298
3619
  function switchTab(tabName) {
@@ -4720,6 +5041,200 @@
4720
5041
  }
4721
5042
  }
4722
5043
 
5044
+ // ── Discovery Tab ──────────────────────────────────────
5045
+ let discoveryLoaded = false;
5046
+ let discoveryData = null;
5047
+ let discoveryFilter = 'all';
5048
+
5049
+ const FUNNEL_COLORS = {
5050
+ undiscovered: '#444',
5051
+ aware: 'var(--blue)',
5052
+ interested: '#eab308',
5053
+ deferred: '#888',
5054
+ declined: 'var(--red)',
5055
+ enabled: 'var(--accent)',
5056
+ disabled: '#555',
5057
+ };
5058
+
5059
+ const FUNNEL_ORDER = ['undiscovered', 'aware', 'interested', 'deferred', 'declined', 'enabled', 'disabled'];
5060
+
5061
+ async function loadDiscovery() {
5062
+ discoveryLoaded = true;
5063
+ try {
5064
+ discoveryData = await apiFetch('/features/analytics');
5065
+ renderDiscoveryMetrics();
5066
+ renderFunnel();
5067
+ renderFeatureGrid();
5068
+ renderDigest();
5069
+ renderEventLog();
5070
+ } catch (e) {
5071
+ document.getElementById('funnelChart').innerHTML =
5072
+ '<div style="padding:20px;color:var(--red);text-align:center">Failed to load discovery data</div>';
5073
+ }
5074
+ }
5075
+
5076
+ function renderDiscoveryMetrics() {
5077
+ if (!discoveryData) return;
5078
+ const d = discoveryData;
5079
+ const metricsHtml = `
5080
+ <div class="discovery-metrics">
5081
+ <div class="metric-card">
5082
+ <div class="metric-value">${d.totalFeatures}</div>
5083
+ <div class="metric-label">Total Features</div>
5084
+ </div>
5085
+ <div class="metric-card">
5086
+ <div class="metric-value">${d.enabledCount}</div>
5087
+ <div class="metric-label">Enabled</div>
5088
+ </div>
5089
+ <div class="metric-card">
5090
+ <div class="metric-value">${Math.round(d.discoveryRate * 100)}%</div>
5091
+ <div class="metric-label">Discovery Rate</div>
5092
+ </div>
5093
+ <div class="metric-card">
5094
+ <div class="metric-value">${d.cooldowns.filter(c => c.cooldownExpiresAt).length}</div>
5095
+ <div class="metric-label">In Cooldown</div>
5096
+ </div>
5097
+ </div>`;
5098
+ // Insert before funnel
5099
+ const section = document.querySelector('.discovery-section');
5100
+ if (section && !document.getElementById('discoveryMetrics')) {
5101
+ const div = document.createElement('div');
5102
+ div.id = 'discoveryMetrics';
5103
+ div.innerHTML = metricsHtml;
5104
+ section.parentNode.insertBefore(div, section);
5105
+ }
5106
+ }
5107
+
5108
+ function renderFunnel() {
5109
+ if (!discoveryData) return;
5110
+ const funnel = discoveryData.funnel;
5111
+ const maxVal = Math.max(1, ...Object.values(funnel));
5112
+ const chart = document.getElementById('funnelChart');
5113
+
5114
+ chart.innerHTML = FUNNEL_ORDER.map(state => {
5115
+ const count = funnel[state] || 0;
5116
+ const height = Math.max(2, (count / maxVal) * 80);
5117
+ const color = FUNNEL_COLORS[state] || '#444';
5118
+ return `<div class="funnel-bar">
5119
+ <div class="funnel-bar-count">${count}</div>
5120
+ <div class="funnel-bar-fill" style="height:${height}px;background:${color}"></div>
5121
+ <div class="funnel-bar-label">${state}</div>
5122
+ </div>`;
5123
+ }).join('');
5124
+ }
5125
+
5126
+ function setDiscoveryFilter(filter) {
5127
+ discoveryFilter = filter;
5128
+ document.querySelectorAll('.discovery-filter').forEach(c => {
5129
+ c.classList.toggle('active', c.dataset.filter === filter);
5130
+ });
5131
+ renderFeatureGrid();
5132
+ }
5133
+
5134
+ function renderFeatureGrid() {
5135
+ if (!discoveryData) return;
5136
+ const grid = document.getElementById('featureGrid');
5137
+ const cooldowns = discoveryData.cooldowns || [];
5138
+
5139
+ let filtered = cooldowns;
5140
+ if (discoveryFilter === 'enabled') {
5141
+ filtered = cooldowns.filter(c => c.discoveryState === 'enabled');
5142
+ } else if (discoveryFilter === 'undiscovered') {
5143
+ filtered = cooldowns.filter(c => c.discoveryState === 'undiscovered');
5144
+ } else if (discoveryFilter === 'aware') {
5145
+ filtered = cooldowns.filter(c => ['aware', 'interested', 'deferred'].includes(c.discoveryState));
5146
+ } else if (discoveryFilter === 'cooldown') {
5147
+ filtered = cooldowns.filter(c => c.cooldownExpiresAt || c.quieted);
5148
+ }
5149
+
5150
+ if (filtered.length === 0) {
5151
+ grid.innerHTML = '<div style="padding:16px;color:var(--text-dim);text-align:center">No features match filter</div>';
5152
+ return;
5153
+ }
5154
+
5155
+ grid.innerHTML = filtered.map(c => {
5156
+ const cooldownNote = c.cooldownExpiresAt
5157
+ ? '<span class="cooldown">cooldown until ' + new Date(c.cooldownExpiresAt).toLocaleTimeString() + '</span>'
5158
+ : c.quieted ? '<span class="cooldown">quieted (' + c.surfaceCount + '/' + c.maxSurfaces + ')</span>' : '';
5159
+
5160
+ return `<div class="feature-card">
5161
+ <div class="feature-card-top">
5162
+ <span class="feature-card-name">${esc(c.featureName)}</span>
5163
+ <span class="feature-state-badge ${esc(c.discoveryState)}">${esc(c.discoveryState)}</span>
5164
+ </div>
5165
+ <div class="feature-card-meta">
5166
+ <span>surfaced ${c.surfaceCount}/${c.maxSurfaces}</span>
5167
+ ${cooldownNote}
5168
+ </div>
5169
+ </div>`;
5170
+ }).join('');
5171
+ }
5172
+
5173
+ function renderDigest() {
5174
+ if (!discoveryData) return;
5175
+ const section = document.getElementById('digestSection');
5176
+ const content = document.getElementById('digestContent');
5177
+ const changed = discoveryData.changedDisabled || [];
5178
+ const unused = discoveryData.unusedEnabled || [];
5179
+
5180
+ if (changed.length === 0 && unused.length === 0) {
5181
+ section.style.display = 'none';
5182
+ return;
5183
+ }
5184
+
5185
+ section.style.display = '';
5186
+ let html = '';
5187
+
5188
+ if (changed.length > 0) {
5189
+ html += '<h4 style="font-size:12px;color:var(--text-dim);margin-bottom:8px">Disabled features with updates</h4>';
5190
+ html += changed.map(f =>
5191
+ `<div class="digest-item">
5192
+ <div class="digest-item-title">${esc(f.featureName)}</div>
5193
+ <div class="digest-item-desc">Version ${esc(f.currentVersion)} available</div>
5194
+ </div>`
5195
+ ).join('');
5196
+ }
5197
+
5198
+ if (unused.length > 0) {
5199
+ html += '<h4 style="font-size:12px;color:var(--text-dim);margin:12px 0 8px">Enabled but unused (&gt;15 days)</h4>';
5200
+ html += unused.map(f =>
5201
+ `<div class="digest-item">
5202
+ <div class="digest-item-title">${esc(f.featureName)}</div>
5203
+ <div class="digest-item-desc">${f.daysSinceActivity} days since last activity</div>
5204
+ </div>`
5205
+ ).join('');
5206
+ }
5207
+
5208
+ content.innerHTML = html;
5209
+ }
5210
+
5211
+ function renderEventLog() {
5212
+ if (!discoveryData) return;
5213
+ const log = document.getElementById('eventLog');
5214
+ const events = discoveryData.recentEvents || [];
5215
+
5216
+ if (events.length === 0) {
5217
+ log.innerHTML = '<div style="padding:12px;color:var(--text-dim);text-align:center">No events yet</div>';
5218
+ return;
5219
+ }
5220
+
5221
+ log.innerHTML = events.slice(0, 30).map(e => {
5222
+ const time = new Date(e.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
5223
+ const date = new Date(e.timestamp).toLocaleDateString([], { month: 'short', day: 'numeric' });
5224
+ const surfaceLabel = e.surfacedAs ? ` (${e.surfacedAs})` : '';
5225
+ return `<div class="event-row">
5226
+ <span class="event-time">${esc(date)} ${esc(time)}</span>
5227
+ <span class="event-feature">${esc(e.featureId)}</span>
5228
+ <span class="event-transition">
5229
+ <span class="feature-state-badge ${esc(e.previousState)}">${esc(e.previousState)}</span>
5230
+ <span class="event-arrow">&rarr;</span>
5231
+ <span class="feature-state-badge ${esc(e.newState)}">${esc(e.newState)}</span>
5232
+ ${surfaceLabel}
5233
+ </span>
5234
+ </div>`;
5235
+ }).join('');
5236
+ }
5237
+
4723
5238
  // Handle tab visibility — close SSE when tab is backgrounded
4724
5239
  document.addEventListener('visibilitychange', () => {
4725
5240
  if (document.hidden) {