@thinkingsage/kanon 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +410 -0
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/bridge/mcp-server.cjs +14171 -0
- package/package.json +98 -0
- package/src/adapters/capabilities.ts +178 -0
- package/src/adapters/claude-code.ts +110 -0
- package/src/adapters/cline.ts +98 -0
- package/src/adapters/codex.ts +173 -0
- package/src/adapters/copilot.ts +106 -0
- package/src/adapters/cursor.ts +97 -0
- package/src/adapters/degradation.ts +95 -0
- package/src/adapters/index.ts +324 -0
- package/src/adapters/kiro-frontmatter.ts +139 -0
- package/src/adapters/kiro-inclusion.ts +86 -0
- package/src/adapters/kiro.ts +412 -0
- package/src/adapters/qdeveloper.ts +115 -0
- package/src/adapters/types.ts +81 -0
- package/src/adapters/windsurf.ts +96 -0
- package/src/admin.ts +283 -0
- package/src/asset-conventions.ts +118 -0
- package/src/attribution-backfill.ts +319 -0
- package/src/attribution-report.ts +95 -0
- package/src/attribution.ts +239 -0
- package/src/backends/github.ts +194 -0
- package/src/backends/http.ts +122 -0
- package/src/backends/index.ts +39 -0
- package/src/backends/local.ts +47 -0
- package/src/backends/s3.ts +157 -0
- package/src/backends/types.ts +59 -0
- package/src/base-cache.ts +270 -0
- package/src/browse-ui.ts +3754 -0
- package/src/browse.ts +1038 -0
- package/src/build.ts +1108 -0
- package/src/catalog.ts +204 -0
- package/src/cli-deprecated.ts +29 -0
- package/src/cli.ts +773 -0
- package/src/collection-admin.ts +287 -0
- package/src/collection-builder.ts +464 -0
- package/src/collections.ts +116 -0
- package/src/compatibility.ts +105 -0
- package/src/config.ts +743 -0
- package/src/eval/rubrics/kiro-progressive-steering.ts +841 -0
- package/src/eval.ts +1169 -0
- package/src/file-writer.ts +61 -0
- package/src/format-registry.ts +141 -0
- package/src/guild/auto-updater.ts +163 -0
- package/src/guild/backend-resolver.ts +49 -0
- package/src/guild/cli.ts +592 -0
- package/src/guild/collection-expander.ts +47 -0
- package/src/guild/global-cache.ts +247 -0
- package/src/guild/hook-generator.ts +100 -0
- package/src/guild/manifest.ts +154 -0
- package/src/guild/path-utils.ts +12 -0
- package/src/guild/sync.ts +622 -0
- package/src/guild/version-resolver.ts +42 -0
- package/src/help/metadata.ts +445 -0
- package/src/help/renderer.ts +265 -0
- package/src/help/typo-suggester.ts +25 -0
- package/src/hooks/expression.ts +493 -0
- package/src/hooks/pipeline.ts +141 -0
- package/src/import.ts +773 -0
- package/src/importers/claude-code.ts +134 -0
- package/src/importers/cline.ts +103 -0
- package/src/importers/codex.ts +140 -0
- package/src/importers/copilot.ts +103 -0
- package/src/importers/cursor.ts +105 -0
- package/src/importers/index.ts +390 -0
- package/src/importers/kiro.ts +110 -0
- package/src/importers/qdeveloper.ts +103 -0
- package/src/importers/types.ts +54 -0
- package/src/importers/windsurf.ts +104 -0
- package/src/install.ts +1005 -0
- package/src/manifest-admin.ts +306 -0
- package/src/mcp-bridge.ts +240 -0
- package/src/mutation/delta.ts +50 -0
- package/src/mutation/history.ts +66 -0
- package/src/mutation/operators.ts +524 -0
- package/src/mutation/runner.ts +332 -0
- package/src/new.ts +106 -0
- package/src/outcomes/collision.ts +127 -0
- package/src/outcomes/normalize.ts +208 -0
- package/src/outcomes/registry.ts +173 -0
- package/src/parser.ts +446 -0
- package/src/provenance-backfill-cli.ts +319 -0
- package/src/provenance-backfill.ts +520 -0
- package/src/publish.ts +354 -0
- package/src/reconcile-orchestrator.ts +502 -0
- package/src/reconcile-report-renderer.ts +176 -0
- package/src/resolve-body.ts +15 -0
- package/src/rosetta/builtins/compatibility-profiles.ts +297 -0
- package/src/rosetta/builtins/contracts.ts +1033 -0
- package/src/rosetta/builtins/pretty-printers/claude-code-native.ts +122 -0
- package/src/rosetta/builtins/pretty-printers/cline-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/codex-native.ts +127 -0
- package/src/rosetta/builtins/pretty-printers/copilot-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/cursor-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/index.ts +81 -0
- package/src/rosetta/builtins/pretty-printers/kiro-native.ts +166 -0
- package/src/rosetta/builtins/pretty-printers/kiro-power.ts +108 -0
- package/src/rosetta/builtins/pretty-printers/kiro-skill.ts +88 -0
- package/src/rosetta/builtins/pretty-printers/qdeveloper-native.ts +51 -0
- package/src/rosetta/builtins/pretty-printers/superpowers.ts +97 -0
- package/src/rosetta/builtins/pretty-printers/windsurf-native.ts +50 -0
- package/src/rosetta/builtins/sources/claude-code-native.ts +348 -0
- package/src/rosetta/builtins/sources/cline-native.ts +176 -0
- package/src/rosetta/builtins/sources/codex-native.ts +343 -0
- package/src/rosetta/builtins/sources/copilot-native.ts +178 -0
- package/src/rosetta/builtins/sources/cursor-native.ts +176 -0
- package/src/rosetta/builtins/sources/index.ts +95 -0
- package/src/rosetta/builtins/sources/kiro-native.ts +462 -0
- package/src/rosetta/builtins/sources/kiro-power.ts +285 -0
- package/src/rosetta/builtins/sources/kiro-skill.ts +230 -0
- package/src/rosetta/builtins/sources/qdeveloper-native.ts +181 -0
- package/src/rosetta/builtins/sources/superpowers.ts +240 -0
- package/src/rosetta/builtins/sources/windsurf-native.ts +176 -0
- package/src/rosetta/builtins/targets/claude-code.ts +181 -0
- package/src/rosetta/builtins/targets/cline.ts +87 -0
- package/src/rosetta/builtins/targets/codex.ts +226 -0
- package/src/rosetta/builtins/targets/copilot.ts +103 -0
- package/src/rosetta/builtins/targets/cursor.ts +87 -0
- package/src/rosetta/builtins/targets/index.ts +60 -0
- package/src/rosetta/builtins/targets/kiro.ts +278 -0
- package/src/rosetta/builtins/targets/qdeveloper.ts +103 -0
- package/src/rosetta/builtins/targets/windsurf.ts +87 -0
- package/src/rosetta/canonical.ts +729 -0
- package/src/rosetta/compatibility.ts +432 -0
- package/src/rosetta/contracts.ts +329 -0
- package/src/rosetta/detector.ts +724 -0
- package/src/rosetta/diagnostics.ts +630 -0
- package/src/rosetta/engine-bootstrap.ts +103 -0
- package/src/rosetta/engine.ts +744 -0
- package/src/rosetta/index.ts +381 -0
- package/src/rosetta/inspection.ts +530 -0
- package/src/rosetta/plan.ts +448 -0
- package/src/rosetta/provenance-digest.ts +369 -0
- package/src/rosetta/reconcile.ts +812 -0
- package/src/rosetta/redaction.ts +467 -0
- package/src/rosetta/registry.ts +712 -0
- package/src/rosetta/renderers.ts +571 -0
- package/src/rosetta/request-guard.ts +335 -0
- package/src/rosetta/resolution.ts +419 -0
- package/src/rosetta/source-accounting.ts +233 -0
- package/src/rosetta/templates.ts +129 -0
- package/src/rosetta-cli.ts +717 -0
- package/src/rosetta-docs-generator.ts +793 -0
- package/src/rosetta-profiles-cli.ts +367 -0
- package/src/schemas.ts +1712 -0
- package/src/spec-coordination.ts +1141 -0
- package/src/temper.ts +747 -0
- package/src/template-bundle-loader.ts +312 -0
- package/src/template-engine.ts +53 -0
- package/src/translation-application-policy.ts +496 -0
- package/src/translation-orchestrator.ts +1013 -0
- package/src/translation-plan-applier.ts +473 -0
- package/src/tutorial.ts +305 -0
- package/src/validate.ts +1093 -0
- package/src/versioning.ts +553 -0
- package/src/wizard.ts +660 -0
- package/src/workspace.ts +237 -0
- package/templates/eval-contexts/claude-code.md.njk +6 -0
- package/templates/eval-contexts/cline.md.njk +6 -0
- package/templates/eval-contexts/copilot.md.njk +6 -0
- package/templates/eval-contexts/cursor.md.njk +6 -0
- package/templates/eval-contexts/kiro.md.njk +10 -0
- package/templates/eval-contexts/qdeveloper.md.njk +6 -0
- package/templates/eval-contexts/windsurf.md.njk +6 -0
- package/templates/harness-adapters/_base/attribution-footer.md.njk +17 -0
- package/templates/harness-adapters/_base/base.md.njk +16 -0
- package/templates/harness-adapters/claude-code/claude.md.njk +1 -0
- package/templates/harness-adapters/claude-code/mcp.json.njk +1 -0
- package/templates/harness-adapters/claude-code/settings.json.njk +1 -0
- package/templates/harness-adapters/claude-code/skill-library-index.md.njk +13 -0
- package/templates/harness-adapters/claude-code/skill.md.njk +19 -0
- package/templates/harness-adapters/cline/hook.sh.njk +4 -0
- package/templates/harness-adapters/cline/mcp.json.njk +1 -0
- package/templates/harness-adapters/cline/rule.md.njk +1 -0
- package/templates/harness-adapters/codex/agents-md.md.njk +6 -0
- package/templates/harness-adapters/codex/agents-pointer.md.njk +16 -0
- package/templates/harness-adapters/codex/skill.md.njk +27 -0
- package/templates/harness-adapters/copilot/agents.md.njk +1 -0
- package/templates/harness-adapters/copilot/instructions.md.njk +1 -0
- package/templates/harness-adapters/copilot/scoped.md.njk +6 -0
- package/templates/harness-adapters/cursor/mcp.json.njk +1 -0
- package/templates/harness-adapters/cursor/rule.md.njk +6 -0
- package/templates/harness-adapters/kiro/hook.json.njk +1 -0
- package/templates/harness-adapters/kiro/mcp.json.njk +1 -0
- package/templates/harness-adapters/kiro/power-steering.md.njk +3 -0
- package/templates/harness-adapters/kiro/power.md.njk +12 -0
- package/templates/harness-adapters/kiro/steering.md.njk +16 -0
- package/templates/harness-adapters/qdeveloper/agent.md.njk +1 -0
- package/templates/harness-adapters/qdeveloper/mcp.json.njk +1 -0
- package/templates/harness-adapters/qdeveloper/rule.md.njk +1 -0
- package/templates/harness-adapters/windsurf/mcp.json.njk +1 -0
- package/templates/harness-adapters/windsurf/rule.md.njk +1 -0
- package/templates/harness-adapters/windsurf/workflow.md.njk +1 -0
- package/templates/knowledge/hooks.yaml.njk +4 -0
- package/templates/knowledge/knowledge.md.njk +53 -0
- package/templates/knowledge/mcp-servers.yaml.njk +2 -0
package/src/browse-ui.ts
ADDED
|
@@ -0,0 +1,3754 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browse UI — HTML template generation for the catalog browser SPA.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from browse.ts to keep the route handler and server bootstrap
|
|
5
|
+
* lean. All CSS and JS are inlined in the generated HTML string.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { CAPABILITY_MATRIX } from "./adapters/capabilities";
|
|
9
|
+
import type { CatalogEntry } from "./schemas";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Escapes HTML special characters to prevent XSS in embedded content.
|
|
13
|
+
*/
|
|
14
|
+
export function escapeHtml(str: string): string {
|
|
15
|
+
return str
|
|
16
|
+
.replace(/&/g, "&")
|
|
17
|
+
.replace(/</g, "<")
|
|
18
|
+
.replace(/>/g, ">")
|
|
19
|
+
.replace(/"/g, """)
|
|
20
|
+
.replace(/'/g, "'");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function generateStyles(): string {
|
|
24
|
+
return ` <style>
|
|
25
|
+
:root {
|
|
26
|
+
/* Typography */
|
|
27
|
+
--font-display: "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
28
|
+
--font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
29
|
+
--font-mono: "Berkeley Mono", "IBM Plex Mono", "JetBrains Mono", "Fira Code", Menlo, Monaco, Consolas, monospace;
|
|
30
|
+
/* Colors — Neutral palette (warm grays) */
|
|
31
|
+
--color-bg: #f7f7f5;
|
|
32
|
+
--color-surface: #fff;
|
|
33
|
+
--color-surface-alt: #fafaf8;
|
|
34
|
+
--color-border: #e4e4e0;
|
|
35
|
+
--color-border-subtle: #e8e8e4;
|
|
36
|
+
--color-border-hover: #c8c8c4;
|
|
37
|
+
--color-text-primary: #1a1a1a;
|
|
38
|
+
--color-text-secondary: #555;
|
|
39
|
+
--color-text-muted: #999;
|
|
40
|
+
--color-text-faint: #aaa;
|
|
41
|
+
/* Colors — Semantic */
|
|
42
|
+
--color-success: #0a3622;
|
|
43
|
+
--color-success-bg: #d1e7dd;
|
|
44
|
+
--color-warning: #856404;
|
|
45
|
+
--color-warning-bg: #fff3cd;
|
|
46
|
+
--color-error: #842029;
|
|
47
|
+
--color-error-bg: #f8d7da;
|
|
48
|
+
--color-info: #084298;
|
|
49
|
+
--color-info-bg: #cfe2ff;
|
|
50
|
+
/* Colors — Status indicators (manifest sync) */
|
|
51
|
+
--color-status-synced: #16a34a;
|
|
52
|
+
--color-status-outdated: #ca8a04;
|
|
53
|
+
--color-status-missing: #dc2626;
|
|
54
|
+
/* Spacing scale (4px base) */
|
|
55
|
+
--space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px;
|
|
56
|
+
--space-5: 20px; --space-6: 24px; --space-8: 32px; --space-10: 40px;
|
|
57
|
+
/* Radii */
|
|
58
|
+
--radius-sm: 4px; --radius-md: 6px; --radius-lg: 8px; --radius-pill: 20px;
|
|
59
|
+
/* Shadows */
|
|
60
|
+
--shadow-card-hover: 0 4px 16px rgba(0,0,0,0.07);
|
|
61
|
+
--shadow-modal: 0 8px 32px rgba(0,0,0,0.12);
|
|
62
|
+
--shadow-toast: 0 4px 12px rgba(0,0,0,0.1);
|
|
63
|
+
}
|
|
64
|
+
*, *::before, *::after {
|
|
65
|
+
margin: 0;
|
|
66
|
+
padding: 0;
|
|
67
|
+
box-sizing: border-box;
|
|
68
|
+
}
|
|
69
|
+
body {
|
|
70
|
+
font-family: var(--font-body);
|
|
71
|
+
background: #f7f7f5;
|
|
72
|
+
color: #1a1a1a;
|
|
73
|
+
line-height: 1.5;
|
|
74
|
+
-webkit-font-smoothing: antialiased;
|
|
75
|
+
-moz-osx-font-smoothing: grayscale;
|
|
76
|
+
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
|
|
77
|
+
}
|
|
78
|
+
code, pre, .mono {
|
|
79
|
+
font-family: "Berkeley Mono", "IBM Plex Mono", "JetBrains Mono", "Fira Code", Menlo, Monaco, Consolas, monospace;
|
|
80
|
+
}
|
|
81
|
+
header {
|
|
82
|
+
padding: 16px 24px;
|
|
83
|
+
border-bottom: 1px solid #e4e4e0;
|
|
84
|
+
background: #fff;
|
|
85
|
+
display: flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
gap: 10px;
|
|
88
|
+
}
|
|
89
|
+
h1 {
|
|
90
|
+
font-family: var(--font-display);
|
|
91
|
+
font-size: 1.05rem;
|
|
92
|
+
font-weight: 700;
|
|
93
|
+
letter-spacing: -0.01em;
|
|
94
|
+
color: #111;
|
|
95
|
+
}
|
|
96
|
+
.header-divider {
|
|
97
|
+
width: 1px;
|
|
98
|
+
height: 16px;
|
|
99
|
+
background: #e0e0dc;
|
|
100
|
+
}
|
|
101
|
+
#artifact-count {
|
|
102
|
+
font-size: 0.8rem;
|
|
103
|
+
color: #999;
|
|
104
|
+
font-weight: 400;
|
|
105
|
+
}
|
|
106
|
+
.filters {
|
|
107
|
+
display: flex;
|
|
108
|
+
flex-direction: column;
|
|
109
|
+
gap: 0;
|
|
110
|
+
border-bottom: 1px solid #e0e0dc;
|
|
111
|
+
background: #fff;
|
|
112
|
+
}
|
|
113
|
+
.filter-search-row {
|
|
114
|
+
padding: 12px 24px 10px;
|
|
115
|
+
display: flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
gap: 12px;
|
|
118
|
+
}
|
|
119
|
+
.filter-groups-row {
|
|
120
|
+
display: flex;
|
|
121
|
+
flex-wrap: wrap;
|
|
122
|
+
align-items: flex-start;
|
|
123
|
+
gap: 0;
|
|
124
|
+
padding: 0 24px 10px;
|
|
125
|
+
}
|
|
126
|
+
.filter-group {
|
|
127
|
+
display: flex;
|
|
128
|
+
flex-direction: column;
|
|
129
|
+
gap: 5px;
|
|
130
|
+
padding: 4px 16px 4px 0;
|
|
131
|
+
margin-right: 16px;
|
|
132
|
+
border-right: 1px solid #e8e8e4;
|
|
133
|
+
}
|
|
134
|
+
.filter-group:last-child {
|
|
135
|
+
border-right: none;
|
|
136
|
+
margin-right: 0;
|
|
137
|
+
}
|
|
138
|
+
.filter-group-label {
|
|
139
|
+
font-size: 0.65rem;
|
|
140
|
+
font-weight: 600;
|
|
141
|
+
letter-spacing: 0.06em;
|
|
142
|
+
text-transform: uppercase;
|
|
143
|
+
color: #aaa;
|
|
144
|
+
user-select: none;
|
|
145
|
+
}
|
|
146
|
+
.filter-group-items {
|
|
147
|
+
display: flex;
|
|
148
|
+
flex-wrap: wrap;
|
|
149
|
+
gap: 4px;
|
|
150
|
+
align-items: center;
|
|
151
|
+
}
|
|
152
|
+
#search-input {
|
|
153
|
+
font-family: inherit;
|
|
154
|
+
font-size: 0.875rem;
|
|
155
|
+
padding: 6px 12px;
|
|
156
|
+
border: 1px solid #d0d0cc;
|
|
157
|
+
border-radius: 6px;
|
|
158
|
+
background: #fafaf8;
|
|
159
|
+
color: #1a1a1a;
|
|
160
|
+
outline: none;
|
|
161
|
+
min-width: 260px;
|
|
162
|
+
transition: border-color 0.15s, box-shadow 0.15s;
|
|
163
|
+
}
|
|
164
|
+
#search-input:focus {
|
|
165
|
+
border-color: #999;
|
|
166
|
+
box-shadow: 0 0 0 2px rgba(0,0,0,0.05);
|
|
167
|
+
}
|
|
168
|
+
#search-input::placeholder {
|
|
169
|
+
color: #aaa;
|
|
170
|
+
}
|
|
171
|
+
.filter-group label {
|
|
172
|
+
font-size: 0.78rem;
|
|
173
|
+
color: #555;
|
|
174
|
+
cursor: pointer;
|
|
175
|
+
display: inline-flex;
|
|
176
|
+
align-items: center;
|
|
177
|
+
gap: 4px;
|
|
178
|
+
user-select: none;
|
|
179
|
+
padding: 2px 8px 2px 6px;
|
|
180
|
+
border-radius: 4px;
|
|
181
|
+
border: 1px solid transparent;
|
|
182
|
+
transition: background 0.1s, border-color 0.1s;
|
|
183
|
+
white-space: nowrap;
|
|
184
|
+
}
|
|
185
|
+
.filter-group label:hover {
|
|
186
|
+
background: #f2f2ef;
|
|
187
|
+
border-color: #ddd;
|
|
188
|
+
color: #1a1a1a;
|
|
189
|
+
}
|
|
190
|
+
.filter-group input[type="checkbox"] {
|
|
191
|
+
accent-color: #555;
|
|
192
|
+
width: 12px;
|
|
193
|
+
height: 12px;
|
|
194
|
+
cursor: pointer;
|
|
195
|
+
}
|
|
196
|
+
.filter-group label:has(input:checked) {
|
|
197
|
+
background: #1a1a1a;
|
|
198
|
+
border-color: #1a1a1a;
|
|
199
|
+
color: #fff;
|
|
200
|
+
}
|
|
201
|
+
.filter-group label:has(input:checked) input[type="checkbox"] {
|
|
202
|
+
accent-color: #fff;
|
|
203
|
+
}
|
|
204
|
+
#card-grid {
|
|
205
|
+
display: grid;
|
|
206
|
+
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
|
207
|
+
gap: 12px;
|
|
208
|
+
padding: 24px;
|
|
209
|
+
max-width: 1440px;
|
|
210
|
+
margin: 0 auto;
|
|
211
|
+
}
|
|
212
|
+
.card {
|
|
213
|
+
border: 1px solid #e4e4e0;
|
|
214
|
+
border-radius: 8px;
|
|
215
|
+
padding: 20px 22px 16px;
|
|
216
|
+
cursor: pointer;
|
|
217
|
+
background: #fff;
|
|
218
|
+
display: flex;
|
|
219
|
+
flex-direction: column;
|
|
220
|
+
gap: 0;
|
|
221
|
+
transition: border-color 0.15s, box-shadow 0.15s, transform 0.1s;
|
|
222
|
+
}
|
|
223
|
+
.card:hover {
|
|
224
|
+
border-color: #c8c8c4;
|
|
225
|
+
box-shadow: 0 4px 16px rgba(0,0,0,0.07);
|
|
226
|
+
transform: translateY(-1px);
|
|
227
|
+
}
|
|
228
|
+
.card-header {
|
|
229
|
+
display: flex;
|
|
230
|
+
align-items: baseline;
|
|
231
|
+
justify-content: space-between;
|
|
232
|
+
gap: 10px;
|
|
233
|
+
margin-bottom: 2px;
|
|
234
|
+
}
|
|
235
|
+
.card-display-name {
|
|
236
|
+
font-family: var(--font-display);
|
|
237
|
+
font-weight: 700;
|
|
238
|
+
font-size: 1rem;
|
|
239
|
+
color: #111;
|
|
240
|
+
letter-spacing: -0.01em;
|
|
241
|
+
line-height: 1.3;
|
|
242
|
+
}
|
|
243
|
+
.card-version {
|
|
244
|
+
font-size: 0.72rem;
|
|
245
|
+
color: #999;
|
|
246
|
+
font-family: "Berkeley Mono", "IBM Plex Mono", "JetBrains Mono", Menlo, monospace;
|
|
247
|
+
flex-shrink: 0;
|
|
248
|
+
}
|
|
249
|
+
.card-name {
|
|
250
|
+
font-size: 0.75rem;
|
|
251
|
+
color: #888;
|
|
252
|
+
font-family: "Berkeley Mono", "IBM Plex Mono", "JetBrains Mono", Menlo, monospace;
|
|
253
|
+
margin-bottom: 10px;
|
|
254
|
+
}
|
|
255
|
+
.card-description {
|
|
256
|
+
font-size: 0.875rem;
|
|
257
|
+
color: #555;
|
|
258
|
+
line-height: 1.5;
|
|
259
|
+
margin-bottom: 12px;
|
|
260
|
+
display: -webkit-box;
|
|
261
|
+
-webkit-line-clamp: 2;
|
|
262
|
+
-webkit-box-orient: vertical;
|
|
263
|
+
overflow: hidden;
|
|
264
|
+
}
|
|
265
|
+
.card-keywords {
|
|
266
|
+
display: flex;
|
|
267
|
+
flex-wrap: wrap;
|
|
268
|
+
gap: 4px;
|
|
269
|
+
margin-bottom: 14px;
|
|
270
|
+
}
|
|
271
|
+
.card-keywords span {
|
|
272
|
+
font-size: 0.7rem;
|
|
273
|
+
border: 1px solid #e4e4e0;
|
|
274
|
+
padding: 2px 7px;
|
|
275
|
+
border-radius: 20px;
|
|
276
|
+
background: #f7f7f5;
|
|
277
|
+
color: #666;
|
|
278
|
+
letter-spacing: 0.01em;
|
|
279
|
+
}
|
|
280
|
+
.card-footer {
|
|
281
|
+
display: flex;
|
|
282
|
+
align-items: center;
|
|
283
|
+
justify-content: space-between;
|
|
284
|
+
gap: 8px;
|
|
285
|
+
margin-top: auto;
|
|
286
|
+
padding-top: 12px;
|
|
287
|
+
border-top: 1px solid #f0f0ec;
|
|
288
|
+
}
|
|
289
|
+
.card-harnesses {
|
|
290
|
+
display: flex;
|
|
291
|
+
align-items: center;
|
|
292
|
+
gap: 2px;
|
|
293
|
+
line-height: 1;
|
|
294
|
+
}
|
|
295
|
+
.harness-icon {
|
|
296
|
+
font-size: 1rem;
|
|
297
|
+
cursor: default;
|
|
298
|
+
opacity: 0.65;
|
|
299
|
+
transition: opacity 0.1s;
|
|
300
|
+
display: inline-block;
|
|
301
|
+
}
|
|
302
|
+
.harness-icon:hover {
|
|
303
|
+
opacity: 1;
|
|
304
|
+
}
|
|
305
|
+
.card-badges {
|
|
306
|
+
display: flex;
|
|
307
|
+
gap: 4px;
|
|
308
|
+
flex-shrink: 0;
|
|
309
|
+
}
|
|
310
|
+
.badge {
|
|
311
|
+
font-size: 0.65rem;
|
|
312
|
+
font-weight: 600;
|
|
313
|
+
padding: 2px 6px;
|
|
314
|
+
border-radius: 4px;
|
|
315
|
+
text-transform: uppercase;
|
|
316
|
+
letter-spacing: 0.03em;
|
|
317
|
+
}
|
|
318
|
+
.badge-maturity-experimental { background: #fff3cd; color: #856404; }
|
|
319
|
+
.badge-maturity-beta { background: #cfe2ff; color: #084298; }
|
|
320
|
+
.badge-maturity-stable { background: #d1e7dd; color: #0a3622; }
|
|
321
|
+
.badge-maturity-deprecated { background: #f8d7da; color: #842029; }
|
|
322
|
+
.badge-trust-official { background: #d1e7dd; color: #0a3622; }
|
|
323
|
+
.badge-trust-partner { background: #e2d9f3; color: #41217a; }
|
|
324
|
+
.badge-trust-community { background: #e0e0dc; color: #444; }
|
|
325
|
+
.badge-trust-experimental { background: #fff3cd; color: #856404; }
|
|
326
|
+
.badge-attribution {
|
|
327
|
+
background: #eef2ff;
|
|
328
|
+
color: #3730a3;
|
|
329
|
+
text-transform: none;
|
|
330
|
+
letter-spacing: 0.02em;
|
|
331
|
+
white-space: nowrap;
|
|
332
|
+
margin-left: 6px;
|
|
333
|
+
}
|
|
334
|
+
#detail-view {
|
|
335
|
+
padding: 32px 40px;
|
|
336
|
+
max-width: 860px;
|
|
337
|
+
margin: 0 auto;
|
|
338
|
+
}
|
|
339
|
+
.detail-back {
|
|
340
|
+
cursor: pointer;
|
|
341
|
+
text-decoration: none;
|
|
342
|
+
color: #999;
|
|
343
|
+
font-size: 0.75rem;
|
|
344
|
+
font-weight: 500;
|
|
345
|
+
letter-spacing: 0.02em;
|
|
346
|
+
text-transform: uppercase;
|
|
347
|
+
margin-bottom: 28px;
|
|
348
|
+
display: inline-flex;
|
|
349
|
+
align-items: center;
|
|
350
|
+
gap: 6px;
|
|
351
|
+
padding: 5px 10px 5px 7px;
|
|
352
|
+
border-radius: 5px;
|
|
353
|
+
border: 1px solid #e8e8e4;
|
|
354
|
+
background: #fafaf8;
|
|
355
|
+
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
|
356
|
+
}
|
|
357
|
+
.detail-back:hover {
|
|
358
|
+
color: #1a1a1a;
|
|
359
|
+
border-color: #ccc;
|
|
360
|
+
background: #f2f2ef;
|
|
361
|
+
}
|
|
362
|
+
.detail-back-arrow {
|
|
363
|
+
font-size: 0.9rem;
|
|
364
|
+
line-height: 1;
|
|
365
|
+
color: #bbb;
|
|
366
|
+
transition: color 0.15s, transform 0.15s;
|
|
367
|
+
display: inline-block;
|
|
368
|
+
}
|
|
369
|
+
.detail-back:hover .detail-back-arrow {
|
|
370
|
+
color: #666;
|
|
371
|
+
transform: translateX(-2px);
|
|
372
|
+
}
|
|
373
|
+
.detail-title-row {
|
|
374
|
+
display: flex;
|
|
375
|
+
align-items: baseline;
|
|
376
|
+
gap: 12px;
|
|
377
|
+
margin-bottom: 4px;
|
|
378
|
+
flex-wrap: wrap;
|
|
379
|
+
}
|
|
380
|
+
#detail-view h2 {
|
|
381
|
+
font-family: var(--font-display);
|
|
382
|
+
font-size: 1.5rem;
|
|
383
|
+
font-weight: 700;
|
|
384
|
+
letter-spacing: -0.02em;
|
|
385
|
+
color: #111;
|
|
386
|
+
margin: 0;
|
|
387
|
+
}
|
|
388
|
+
.detail-pkg-name {
|
|
389
|
+
font-size: 0.85rem;
|
|
390
|
+
font-family: "Berkeley Mono", "IBM Plex Mono", "JetBrains Mono", Menlo, monospace;
|
|
391
|
+
color: #888;
|
|
392
|
+
margin-bottom: 16px;
|
|
393
|
+
}
|
|
394
|
+
.detail-description {
|
|
395
|
+
font-size: 1rem;
|
|
396
|
+
color: #444;
|
|
397
|
+
line-height: 1.6;
|
|
398
|
+
margin-bottom: 20px;
|
|
399
|
+
}
|
|
400
|
+
.detail-badges {
|
|
401
|
+
display: flex;
|
|
402
|
+
gap: 6px;
|
|
403
|
+
flex-wrap: wrap;
|
|
404
|
+
margin-bottom: 20px;
|
|
405
|
+
}
|
|
406
|
+
.detail-meta {
|
|
407
|
+
display: grid;
|
|
408
|
+
grid-template-columns: auto 1fr;
|
|
409
|
+
gap: 6px 16px;
|
|
410
|
+
margin-bottom: 24px;
|
|
411
|
+
padding: 16px 20px;
|
|
412
|
+
background: #f7f7f5;
|
|
413
|
+
border-radius: 8px;
|
|
414
|
+
border: 1px solid #e8e8e4;
|
|
415
|
+
}
|
|
416
|
+
.detail-meta-label {
|
|
417
|
+
font-size: 0.75rem;
|
|
418
|
+
font-weight: 600;
|
|
419
|
+
color: #999;
|
|
420
|
+
text-transform: uppercase;
|
|
421
|
+
letter-spacing: 0.05em;
|
|
422
|
+
white-space: nowrap;
|
|
423
|
+
padding-top: 1px;
|
|
424
|
+
}
|
|
425
|
+
.detail-meta-value {
|
|
426
|
+
font-size: 0.825rem;
|
|
427
|
+
color: #333;
|
|
428
|
+
line-height: 1.5;
|
|
429
|
+
}
|
|
430
|
+
.detail-section-label {
|
|
431
|
+
font-size: 0.75rem;
|
|
432
|
+
font-weight: 600;
|
|
433
|
+
color: #999;
|
|
434
|
+
text-transform: uppercase;
|
|
435
|
+
letter-spacing: 0.05em;
|
|
436
|
+
margin-bottom: 8px;
|
|
437
|
+
margin-top: 20px;
|
|
438
|
+
}
|
|
439
|
+
.detail-content pre {
|
|
440
|
+
font-family: "Berkeley Mono", "IBM Plex Mono", "JetBrains Mono", Menlo, monospace;
|
|
441
|
+
font-size: 0.8rem;
|
|
442
|
+
border: 1px solid #e0e0dc;
|
|
443
|
+
border-radius: 6px;
|
|
444
|
+
padding: 16px;
|
|
445
|
+
overflow-x: auto;
|
|
446
|
+
background: #fafaf8;
|
|
447
|
+
white-space: pre-wrap;
|
|
448
|
+
word-wrap: break-word;
|
|
449
|
+
line-height: 1.5;
|
|
450
|
+
}
|
|
451
|
+
.empty-state {
|
|
452
|
+
text-align: center;
|
|
453
|
+
color: #999;
|
|
454
|
+
padding: 48px 24px;
|
|
455
|
+
font-size: 0.875rem;
|
|
456
|
+
}
|
|
457
|
+
.error-message {
|
|
458
|
+
color: #b44;
|
|
459
|
+
font-size: 0.825rem;
|
|
460
|
+
padding: 12px 0;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/* --- Card: collection modifier --- */
|
|
464
|
+
.card--collection {
|
|
465
|
+
border-left: 3px solid var(--color-border);
|
|
466
|
+
}
|
|
467
|
+
.card--collection:hover {
|
|
468
|
+
border-left-color: var(--color-border-hover);
|
|
469
|
+
}
|
|
470
|
+
.card-tags {
|
|
471
|
+
display: flex;
|
|
472
|
+
flex-wrap: wrap;
|
|
473
|
+
gap: 4px;
|
|
474
|
+
margin-bottom: 14px;
|
|
475
|
+
}
|
|
476
|
+
.card-tags span {
|
|
477
|
+
font-size: 0.7rem;
|
|
478
|
+
border: 1px solid var(--color-border);
|
|
479
|
+
padding: 2px 7px;
|
|
480
|
+
border-radius: var(--radius-pill);
|
|
481
|
+
background: var(--color-bg);
|
|
482
|
+
color: #666;
|
|
483
|
+
letter-spacing: 0.01em;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/* --- Form panel --- */
|
|
487
|
+
.form-panel {
|
|
488
|
+
max-width: 640px;
|
|
489
|
+
margin: 0 auto;
|
|
490
|
+
padding: var(--space-8) var(--space-10);
|
|
491
|
+
}
|
|
492
|
+
.form-panel-header {
|
|
493
|
+
display: flex;
|
|
494
|
+
align-items: center;
|
|
495
|
+
justify-content: space-between;
|
|
496
|
+
margin-bottom: var(--space-6);
|
|
497
|
+
}
|
|
498
|
+
.form-panel-header h2 {
|
|
499
|
+
font-family: var(--font-display);
|
|
500
|
+
font-size: 1.25rem;
|
|
501
|
+
font-weight: 700;
|
|
502
|
+
color: var(--color-text-primary);
|
|
503
|
+
}
|
|
504
|
+
.form-panel-header .close-btn {
|
|
505
|
+
background: none;
|
|
506
|
+
border: 1px solid var(--color-border);
|
|
507
|
+
border-radius: var(--radius-sm);
|
|
508
|
+
padding: 4px 10px;
|
|
509
|
+
cursor: pointer;
|
|
510
|
+
font-size: 0.8rem;
|
|
511
|
+
color: var(--color-text-muted);
|
|
512
|
+
transition: color 0.15s, border-color 0.15s;
|
|
513
|
+
}
|
|
514
|
+
.form-panel-header .close-btn:hover {
|
|
515
|
+
color: var(--color-text-primary);
|
|
516
|
+
border-color: var(--color-border-hover);
|
|
517
|
+
}
|
|
518
|
+
.form-group {
|
|
519
|
+
margin-bottom: var(--space-5);
|
|
520
|
+
}
|
|
521
|
+
.form-label {
|
|
522
|
+
display: block;
|
|
523
|
+
font-size: 0.78rem;
|
|
524
|
+
font-weight: 600;
|
|
525
|
+
color: var(--color-text-secondary);
|
|
526
|
+
margin-bottom: var(--space-1);
|
|
527
|
+
letter-spacing: 0.01em;
|
|
528
|
+
}
|
|
529
|
+
.form-input,
|
|
530
|
+
.form-textarea,
|
|
531
|
+
.form-select {
|
|
532
|
+
width: 100%;
|
|
533
|
+
font-family: var(--font-body);
|
|
534
|
+
font-size: 0.875rem;
|
|
535
|
+
padding: 8px 12px;
|
|
536
|
+
border: 1px solid var(--color-border);
|
|
537
|
+
border-radius: var(--radius-md);
|
|
538
|
+
background: var(--color-surface-alt);
|
|
539
|
+
color: var(--color-text-primary);
|
|
540
|
+
outline: none;
|
|
541
|
+
transition: border-color 0.15s, box-shadow 0.15s;
|
|
542
|
+
}
|
|
543
|
+
.form-input:focus,
|
|
544
|
+
.form-textarea:focus,
|
|
545
|
+
.form-select:focus {
|
|
546
|
+
border-color: var(--color-text-muted);
|
|
547
|
+
box-shadow: 0 0 0 2px rgba(0,0,0,0.05);
|
|
548
|
+
}
|
|
549
|
+
.form-textarea {
|
|
550
|
+
min-height: 120px;
|
|
551
|
+
resize: vertical;
|
|
552
|
+
line-height: 1.5;
|
|
553
|
+
}
|
|
554
|
+
.form-textarea.mono {
|
|
555
|
+
font-family: var(--font-mono);
|
|
556
|
+
font-size: 0.8rem;
|
|
557
|
+
}
|
|
558
|
+
.form-checkbox-group,
|
|
559
|
+
.form-radio-group {
|
|
560
|
+
display: flex;
|
|
561
|
+
flex-wrap: wrap;
|
|
562
|
+
gap: 4px;
|
|
563
|
+
}
|
|
564
|
+
.form-checkbox-group label,
|
|
565
|
+
.form-radio-group label {
|
|
566
|
+
font-size: 0.78rem;
|
|
567
|
+
color: var(--color-text-secondary);
|
|
568
|
+
cursor: pointer;
|
|
569
|
+
display: inline-flex;
|
|
570
|
+
align-items: center;
|
|
571
|
+
gap: 4px;
|
|
572
|
+
padding: 2px 8px 2px 6px;
|
|
573
|
+
border-radius: var(--radius-sm);
|
|
574
|
+
border: 1px solid transparent;
|
|
575
|
+
transition: background 0.1s, border-color 0.1s;
|
|
576
|
+
user-select: none;
|
|
577
|
+
}
|
|
578
|
+
.form-checkbox-group label:hover,
|
|
579
|
+
.form-radio-group label:hover {
|
|
580
|
+
background: #f2f2ef;
|
|
581
|
+
border-color: #ddd;
|
|
582
|
+
}
|
|
583
|
+
.form-checkbox-group input[type="checkbox"],
|
|
584
|
+
.form-radio-group input[type="radio"] {
|
|
585
|
+
accent-color: #555;
|
|
586
|
+
width: 12px;
|
|
587
|
+
height: 12px;
|
|
588
|
+
cursor: pointer;
|
|
589
|
+
}
|
|
590
|
+
.form-checkbox-group label:has(input:checked),
|
|
591
|
+
.form-radio-group label:has(input:checked) {
|
|
592
|
+
background: var(--color-text-primary);
|
|
593
|
+
border-color: var(--color-text-primary);
|
|
594
|
+
color: #fff;
|
|
595
|
+
}
|
|
596
|
+
.form-select {
|
|
597
|
+
cursor: pointer;
|
|
598
|
+
appearance: auto;
|
|
599
|
+
}
|
|
600
|
+
.form-error {
|
|
601
|
+
font-size: 0.75rem;
|
|
602
|
+
color: var(--color-error);
|
|
603
|
+
margin-top: var(--space-1);
|
|
604
|
+
}
|
|
605
|
+
.form-input.has-error,
|
|
606
|
+
.form-textarea.has-error,
|
|
607
|
+
.form-select.has-error {
|
|
608
|
+
border-color: var(--color-error);
|
|
609
|
+
}
|
|
610
|
+
.form-actions {
|
|
611
|
+
display: flex;
|
|
612
|
+
gap: var(--space-3);
|
|
613
|
+
justify-content: flex-end;
|
|
614
|
+
padding-top: var(--space-6);
|
|
615
|
+
border-top: 1px solid var(--color-border);
|
|
616
|
+
margin-top: var(--space-6);
|
|
617
|
+
}
|
|
618
|
+
.form-actions button {
|
|
619
|
+
font-family: var(--font-body);
|
|
620
|
+
font-size: 0.825rem;
|
|
621
|
+
font-weight: 500;
|
|
622
|
+
padding: 8px 18px;
|
|
623
|
+
border-radius: var(--radius-md);
|
|
624
|
+
cursor: pointer;
|
|
625
|
+
transition: background 0.15s, border-color 0.15s;
|
|
626
|
+
}
|
|
627
|
+
.form-actions .btn-cancel {
|
|
628
|
+
background: var(--color-surface);
|
|
629
|
+
border: 1px solid var(--color-border);
|
|
630
|
+
color: var(--color-text-secondary);
|
|
631
|
+
}
|
|
632
|
+
.form-actions .btn-cancel:hover {
|
|
633
|
+
border-color: var(--color-border-hover);
|
|
634
|
+
color: var(--color-text-primary);
|
|
635
|
+
}
|
|
636
|
+
.form-actions .btn-submit {
|
|
637
|
+
background: var(--color-text-primary);
|
|
638
|
+
border: 1px solid var(--color-text-primary);
|
|
639
|
+
color: #fff;
|
|
640
|
+
}
|
|
641
|
+
.form-actions .btn-submit:hover {
|
|
642
|
+
background: #333;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/* --- Badge: type and status modifiers --- */
|
|
646
|
+
.badge-type-artifact { background: #e8e8e4; color: #555; }
|
|
647
|
+
.badge-type-collection { background: #ede9f6; color: #5b3ea3; }
|
|
648
|
+
.badge-status-synced { background: #d1e7dd; color: var(--color-status-synced); }
|
|
649
|
+
.badge-status-outdated { background: #fff3cd; color: var(--color-status-outdated); }
|
|
650
|
+
.badge-status-missing { background: #f8d7da; color: var(--color-status-missing); }
|
|
651
|
+
|
|
652
|
+
/* --- Toast notifications --- */
|
|
653
|
+
.toast-container {
|
|
654
|
+
position: fixed;
|
|
655
|
+
top: 16px;
|
|
656
|
+
right: 16px;
|
|
657
|
+
z-index: 1000;
|
|
658
|
+
display: flex;
|
|
659
|
+
flex-direction: column;
|
|
660
|
+
gap: 8px;
|
|
661
|
+
pointer-events: none;
|
|
662
|
+
}
|
|
663
|
+
.toast {
|
|
664
|
+
padding: 12px 16px;
|
|
665
|
+
border-radius: var(--radius-md);
|
|
666
|
+
font-size: 0.825rem;
|
|
667
|
+
box-shadow: var(--shadow-toast);
|
|
668
|
+
pointer-events: auto;
|
|
669
|
+
animation: toast-in 0.2s ease-out;
|
|
670
|
+
}
|
|
671
|
+
.toast--success {
|
|
672
|
+
background: var(--color-success-bg);
|
|
673
|
+
color: var(--color-success);
|
|
674
|
+
border: 1px solid #a3cfbb;
|
|
675
|
+
}
|
|
676
|
+
.toast--error {
|
|
677
|
+
background: var(--color-error-bg);
|
|
678
|
+
color: var(--color-error);
|
|
679
|
+
border: 1px solid #f1aeb5;
|
|
680
|
+
}
|
|
681
|
+
@keyframes toast-in {
|
|
682
|
+
from { opacity: 0; transform: translateY(-8px); }
|
|
683
|
+
to { opacity: 1; transform: translateY(0); }
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/* --- Modal --- */
|
|
687
|
+
.modal {
|
|
688
|
+
display: none;
|
|
689
|
+
position: fixed;
|
|
690
|
+
inset: 0;
|
|
691
|
+
z-index: 900;
|
|
692
|
+
}
|
|
693
|
+
.modal.active {
|
|
694
|
+
display: flex;
|
|
695
|
+
align-items: center;
|
|
696
|
+
justify-content: center;
|
|
697
|
+
}
|
|
698
|
+
.modal-overlay {
|
|
699
|
+
position: absolute;
|
|
700
|
+
inset: 0;
|
|
701
|
+
background: rgba(0,0,0,0.3);
|
|
702
|
+
}
|
|
703
|
+
.modal-dialog {
|
|
704
|
+
position: relative;
|
|
705
|
+
background: var(--color-surface);
|
|
706
|
+
border-radius: var(--radius-lg);
|
|
707
|
+
padding: var(--space-6);
|
|
708
|
+
max-width: 420px;
|
|
709
|
+
width: 90%;
|
|
710
|
+
box-shadow: var(--shadow-modal);
|
|
711
|
+
}
|
|
712
|
+
.modal-dialog h3 {
|
|
713
|
+
font-family: var(--font-display);
|
|
714
|
+
font-size: 1.05rem;
|
|
715
|
+
font-weight: 700;
|
|
716
|
+
margin-bottom: var(--space-3);
|
|
717
|
+
color: var(--color-text-primary);
|
|
718
|
+
}
|
|
719
|
+
.modal-dialog p {
|
|
720
|
+
font-size: 0.875rem;
|
|
721
|
+
color: var(--color-text-secondary);
|
|
722
|
+
line-height: 1.5;
|
|
723
|
+
margin-bottom: var(--space-5);
|
|
724
|
+
}
|
|
725
|
+
.modal-actions {
|
|
726
|
+
display: flex;
|
|
727
|
+
gap: var(--space-3);
|
|
728
|
+
justify-content: flex-end;
|
|
729
|
+
}
|
|
730
|
+
.modal-actions button {
|
|
731
|
+
font-family: var(--font-body);
|
|
732
|
+
font-size: 0.825rem;
|
|
733
|
+
font-weight: 500;
|
|
734
|
+
padding: 8px 18px;
|
|
735
|
+
border-radius: var(--radius-md);
|
|
736
|
+
cursor: pointer;
|
|
737
|
+
transition: background 0.15s, border-color 0.15s;
|
|
738
|
+
}
|
|
739
|
+
.modal-actions .btn-cancel {
|
|
740
|
+
background: var(--color-surface);
|
|
741
|
+
border: 1px solid var(--color-border);
|
|
742
|
+
color: var(--color-text-secondary);
|
|
743
|
+
}
|
|
744
|
+
.modal-actions .btn-cancel:hover {
|
|
745
|
+
border-color: var(--color-border-hover);
|
|
746
|
+
}
|
|
747
|
+
.modal-actions .btn-danger {
|
|
748
|
+
background: var(--color-error);
|
|
749
|
+
border: 1px solid var(--color-error);
|
|
750
|
+
color: #fff;
|
|
751
|
+
}
|
|
752
|
+
.modal-actions .btn-danger:hover {
|
|
753
|
+
background: #6b1a21;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/* --- Tab navigation --- */
|
|
757
|
+
.tab-nav {
|
|
758
|
+
display: flex;
|
|
759
|
+
gap: 0;
|
|
760
|
+
border-bottom: 1px solid var(--color-border);
|
|
761
|
+
background: var(--color-surface);
|
|
762
|
+
padding: 0 24px;
|
|
763
|
+
}
|
|
764
|
+
.tab-nav-item {
|
|
765
|
+
padding: 12px 16px;
|
|
766
|
+
font-size: 0.825rem;
|
|
767
|
+
font-weight: 500;
|
|
768
|
+
color: var(--color-text-muted);
|
|
769
|
+
border-bottom: 2px solid transparent;
|
|
770
|
+
cursor: pointer;
|
|
771
|
+
transition: color 0.15s, border-color 0.15s;
|
|
772
|
+
user-select: none;
|
|
773
|
+
}
|
|
774
|
+
.tab-nav-item:hover {
|
|
775
|
+
color: var(--color-text-primary);
|
|
776
|
+
}
|
|
777
|
+
.tab-nav-item--active {
|
|
778
|
+
color: var(--color-text-primary);
|
|
779
|
+
border-bottom-color: var(--color-text-primary);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/* --- Capability badges (Task 17.1) --- */
|
|
783
|
+
.badge-cap-full { background: #d1e7dd; color: #0a3622; }
|
|
784
|
+
.badge-cap-partial { background: #fff3cd; color: #856404; }
|
|
785
|
+
.badge-cap-none { background: #f8d7da; color: #842029; }
|
|
786
|
+
.cap-matrix { display: grid; gap: 2px; font-size: 0.72rem; margin-bottom: 20px; overflow-x: auto; }
|
|
787
|
+
.cap-matrix-header { font-weight: 600; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.04em; font-size: 0.65rem; padding: 6px 8px; background: var(--color-surface-alt); border-radius: var(--radius-sm); white-space: nowrap; }
|
|
788
|
+
.cap-matrix-cell { padding: 4px 8px; border-radius: var(--radius-sm); text-align: center; font-size: 0.68rem; font-weight: 500; white-space: nowrap; }
|
|
789
|
+
.cap-matrix-label { padding: 6px 8px; font-size: 0.72rem; color: var(--color-text-secondary); white-space: nowrap; }
|
|
790
|
+
.cap-card { border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: 12px 16px; margin-bottom: 8px; }
|
|
791
|
+
.cap-card-title { font-weight: 600; font-size: 0.825rem; margin-bottom: 8px; }
|
|
792
|
+
.cap-card-badges { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
793
|
+
|
|
794
|
+
/* --- Temper panel (Task 17.2) --- */
|
|
795
|
+
.temper-panel { border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: 20px; margin-top: 20px; background: var(--color-surface); }
|
|
796
|
+
.temper-panel-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; gap: 12px; flex-wrap: wrap; }
|
|
797
|
+
.temper-panel-header h3 { font-family: var(--font-display); font-size: 1rem; font-weight: 600; margin: 0; }
|
|
798
|
+
.temper-section { margin-bottom: 16px; }
|
|
799
|
+
.temper-section-title { font-size: 0.75rem; font-weight: 600; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 6px; cursor: pointer; user-select: none; }
|
|
800
|
+
.temper-section-title:hover { color: var(--color-text-primary); }
|
|
801
|
+
.temper-section-content { font-family: var(--font-mono); font-size: 0.78rem; background: var(--color-surface-alt); border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: 12px; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; }
|
|
802
|
+
.temper-deg-inline { color: #0a3622; }
|
|
803
|
+
.temper-deg-comment { color: #856404; }
|
|
804
|
+
.temper-deg-omit { color: #842029; }
|
|
805
|
+
|
|
806
|
+
/* --- Import modal (Task 17.3) --- */
|
|
807
|
+
.import-modal .modal-dialog { max-width: 560px; }
|
|
808
|
+
.import-file-list { max-height: 300px; overflow-y: auto; margin-bottom: 16px; }
|
|
809
|
+
.import-harness-group { margin-bottom: 12px; }
|
|
810
|
+
.import-harness-label { font-size: 0.78rem; font-weight: 600; color: var(--color-text-secondary); margin-bottom: 4px; }
|
|
811
|
+
.import-file-item { display: flex; align-items: center; gap: 6px; padding: 4px 0; font-size: 0.8rem; color: var(--color-text-primary); }
|
|
812
|
+
.import-file-item input { accent-color: #555; }
|
|
813
|
+
.import-toggle-row { display: flex; align-items: center; gap: 16px; margin-bottom: 12px; font-size: 0.8rem; }
|
|
814
|
+
.import-toggle-row label { display: flex; align-items: center; gap: 4px; cursor: pointer; }
|
|
815
|
+
|
|
816
|
+
/* --- Version display (Task 17.4) --- */
|
|
817
|
+
.version-section { margin-bottom: 20px; }
|
|
818
|
+
.version-badge-upgrade { background: #cfe2ff; color: #084298; font-size: 0.68rem; font-weight: 600; padding: 2px 8px; border-radius: var(--radius-pill); }
|
|
819
|
+
.version-not-installed { font-size: 0.8rem; color: var(--color-text-muted); font-style: italic; }
|
|
820
|
+
.changelog-section { border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: 12px 16px; margin-top: 8px; font-size: 0.825rem; }
|
|
821
|
+
.changelog-section pre { font-family: var(--font-mono); font-size: 0.78rem; white-space: pre-wrap; word-wrap: break-word; }
|
|
822
|
+
|
|
823
|
+
/* --- Workspace tab (Task 17.5) --- */
|
|
824
|
+
.ws-project-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 12px; padding: 24px; max-width: 1440px; margin: 0 auto; }
|
|
825
|
+
.ws-project-card { border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: 16px 20px; cursor: pointer; background: var(--color-surface); transition: border-color 0.15s, box-shadow 0.15s; }
|
|
826
|
+
.ws-project-card:hover { border-color: var(--color-border-hover); box-shadow: var(--shadow-card-hover); }
|
|
827
|
+
.ws-project-name { font-family: var(--font-display); font-weight: 700; font-size: 1rem; margin-bottom: 4px; }
|
|
828
|
+
.ws-project-root { font-family: var(--font-mono); font-size: 0.75rem; color: var(--color-text-muted); margin-bottom: 8px; }
|
|
829
|
+
.ws-project-meta { display: flex; gap: 12px; font-size: 0.78rem; color: var(--color-text-secondary); }
|
|
830
|
+
|
|
831
|
+
/* --- Dependency graph (Task 18.1) --- */
|
|
832
|
+
.dep-graph-container { padding: 24px; max-width: 1440px; margin: 0 auto; }
|
|
833
|
+
.dep-graph-controls { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
|
|
834
|
+
.dep-graph-controls select { font-family: var(--font-body); font-size: 0.8rem; padding: 4px 8px; border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-surface); }
|
|
835
|
+
.dep-graph-svg { border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-surface); width: 100%; min-height: 400px; cursor: grab; }
|
|
836
|
+
.dep-graph-svg:active { cursor: grabbing; }
|
|
837
|
+
.dep-graph-svg text { font-family: var(--font-body); font-size: 11px; fill: var(--color-text-primary); pointer-events: none; }
|
|
838
|
+
.dep-graph-svg circle { cursor: pointer; transition: opacity 0.15s; }
|
|
839
|
+
.dep-graph-svg circle:hover { stroke-width: 3; }
|
|
840
|
+
.dep-graph-svg .dimmed { opacity: 0.2; }
|
|
841
|
+
.dep-graph-svg .highlighted { opacity: 1; }
|
|
842
|
+
|
|
843
|
+
/* --- Build dashboard (Task 19.1) --- */
|
|
844
|
+
.build-panel { padding: 24px; max-width: 960px; margin: 0 auto; }
|
|
845
|
+
.build-config { border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: 20px; margin-bottom: 20px; background: var(--color-surface); }
|
|
846
|
+
.build-config h3 { font-family: var(--font-display); font-size: 1rem; font-weight: 600; margin-bottom: 16px; }
|
|
847
|
+
.build-config-row { margin-bottom: 12px; }
|
|
848
|
+
.build-config-label { font-size: 0.75rem; font-weight: 600; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 6px; }
|
|
849
|
+
.build-result { border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: 20px; margin-bottom: 20px; }
|
|
850
|
+
.build-result-banner { padding: 12px 16px; border-radius: var(--radius-md); font-weight: 600; font-size: 0.875rem; margin-bottom: 12px; }
|
|
851
|
+
.build-result-banner.success { background: var(--color-success-bg); color: var(--color-success); }
|
|
852
|
+
.build-result-banner.failure { background: var(--color-error-bg); color: var(--color-error); }
|
|
853
|
+
.build-warnings, .build-errors { margin-top: 8px; }
|
|
854
|
+
.build-warnings summary, .build-errors summary { font-size: 0.8rem; font-weight: 600; cursor: pointer; padding: 4px 0; }
|
|
855
|
+
.build-warning-item, .build-error-item { font-size: 0.78rem; padding: 4px 0; border-bottom: 1px solid var(--color-border-subtle); }
|
|
856
|
+
.build-history { border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: 20px; }
|
|
857
|
+
.build-history h3 { font-family: var(--font-display); font-size: 1rem; font-weight: 600; margin-bottom: 12px; }
|
|
858
|
+
.build-history-item { display: flex; align-items: center; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--color-border-subtle); font-size: 0.8rem; cursor: pointer; }
|
|
859
|
+
.build-history-item:hover { background: var(--color-surface-alt); }
|
|
860
|
+
.build-status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; }
|
|
861
|
+
.build-status-dot.success { background: var(--color-status-synced); }
|
|
862
|
+
.build-status-dot.failure { background: var(--color-status-missing); }
|
|
863
|
+
.build-status-dot.none { background: var(--color-text-muted); }
|
|
864
|
+
.build-status-indicator { display: inline-flex; align-items: center; gap: 4px; font-size: 0.75rem; color: var(--color-text-muted); margin-left: 8px; }
|
|
865
|
+
</style>`;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function generateMarkup(): string {
|
|
869
|
+
return ` <header>
|
|
870
|
+
<h1>Kanon</h1>
|
|
871
|
+
<div class="header-divider"></div>
|
|
872
|
+
<span id="artifact-count"></span>
|
|
873
|
+
<span style="flex:1"></span>
|
|
874
|
+
<span id="build-status-indicator" class="build-status-indicator"></span>
|
|
875
|
+
<button id="import-btn" style="font-family:var(--font-body);font-size:0.8rem;font-weight:500;padding:6px 14px;border-radius:var(--radius-md);background:var(--color-surface);color:var(--color-text-secondary);border:1px solid var(--color-border);cursor:pointer;transition:background 0.15s;margin-right:6px" onclick="openImportModal()">Import</button>
|
|
876
|
+
<button id="build-btn" style="font-family:var(--font-body);font-size:0.8rem;font-weight:500;padding:6px 14px;border-radius:var(--radius-md);background:var(--color-surface);color:var(--color-text-secondary);border:1px solid var(--color-border);cursor:pointer;transition:background 0.15s;margin-right:6px" onclick="showView('build')">Build</button>
|
|
877
|
+
<button id="new-btn" style="font-family:var(--font-body);font-size:0.8rem;font-weight:500;padding:6px 14px;border-radius:var(--radius-md);background:var(--color-text-primary);color:#fff;border:1px solid var(--color-text-primary);cursor:pointer;transition:background 0.15s" onclick="handleNewButton()">+ New Artifact</button>
|
|
878
|
+
</header>
|
|
879
|
+
<nav class="tab-nav" id="tab-nav">
|
|
880
|
+
<div class="tab-nav-item tab-nav-item--active" data-view="artifacts" onclick="showView('artifacts')">Artifacts</div>
|
|
881
|
+
<div class="tab-nav-item" data-view="collections" onclick="showView('collections')">Collections</div>
|
|
882
|
+
<div class="tab-nav-item" data-view="manifest" onclick="showView('manifest')">Manifest</div>
|
|
883
|
+
<div class="tab-nav-item" data-view="graph" onclick="showView('graph')">Dependencies</div>
|
|
884
|
+
<div class="tab-nav-item" data-view="workspace" id="workspace-tab" style="display:none" onclick="showView('workspace')">Workspace</div>
|
|
885
|
+
</nav>
|
|
886
|
+
<div class="filters" id="artifact-filters">
|
|
887
|
+
<div class="filter-search-row">
|
|
888
|
+
<input type="text" id="search-input" placeholder="Search artifacts...">
|
|
889
|
+
</div>
|
|
890
|
+
<div class="filter-groups-row">
|
|
891
|
+
<div class="filter-group">
|
|
892
|
+
<div class="filter-group-label">Harness</div>
|
|
893
|
+
<div class="filter-group-items" id="harness-filter"></div>
|
|
894
|
+
</div>
|
|
895
|
+
<div class="filter-group">
|
|
896
|
+
<div class="filter-group-label">Features</div>
|
|
897
|
+
<div class="filter-group-items" id="features-filter"></div>
|
|
898
|
+
</div>
|
|
899
|
+
<div class="filter-group">
|
|
900
|
+
<div class="filter-group-label">Maturity</div>
|
|
901
|
+
<div class="filter-group-items" id="maturity-filter"></div>
|
|
902
|
+
</div>
|
|
903
|
+
<div class="filter-group" id="collection-filter-group" style="display:none">
|
|
904
|
+
<div class="filter-group-label">Collection</div>
|
|
905
|
+
<div class="filter-group-items" id="collection-filter"></div>
|
|
906
|
+
</div>
|
|
907
|
+
</div>
|
|
908
|
+
</div>
|
|
909
|
+
<div id="card-grid"></div>
|
|
910
|
+
<div id="detail-view" style="display:none"></div>
|
|
911
|
+
<div id="collection-filters" style="display:none" class="filters">
|
|
912
|
+
<div class="filter-search-row">
|
|
913
|
+
<input type="text" id="collection-search-input" placeholder="Search collections..." style="font-family:inherit;font-size:0.875rem;padding:6px 12px;border:1px solid #d0d0cc;border-radius:6px;background:#fafaf8;color:#1a1a1a;outline:none;min-width:260px">
|
|
914
|
+
</div>
|
|
915
|
+
<div class="filter-groups-row">
|
|
916
|
+
<div class="filter-group">
|
|
917
|
+
<div class="filter-group-label">Trust</div>
|
|
918
|
+
<div class="filter-group-items" id="trust-filter"></div>
|
|
919
|
+
</div>
|
|
920
|
+
<div class="filter-group">
|
|
921
|
+
<div class="filter-group-label">Tags</div>
|
|
922
|
+
<div class="filter-group-items" id="tag-filter-input-wrap">
|
|
923
|
+
<input type="text" id="tag-filter-input" placeholder="Filter by tag..." style="font-family:inherit;font-size:0.78rem;padding:4px 8px;border:1px solid #d0d0cc;border-radius:4px;background:#fafaf8;color:#1a1a1a;outline:none;width:140px">
|
|
924
|
+
</div>
|
|
925
|
+
</div>
|
|
926
|
+
</div>
|
|
927
|
+
</div>
|
|
928
|
+
<div id="collection-grid" style="display:none"></div>
|
|
929
|
+
<div id="collection-detail-view" style="display:none"></div>
|
|
930
|
+
<div id="manifest-view" style="display:none"></div>
|
|
931
|
+
<div id="form-panel-container" style="display:none"></div>
|
|
932
|
+
<div id="workspace-view" style="display:none"></div>
|
|
933
|
+
<div id="graph-view" style="display:none"></div>
|
|
934
|
+
<div id="build-view" style="display:none"></div>
|
|
935
|
+
<div class="toast-container" id="toast-container"></div>
|
|
936
|
+
<div class="modal" id="modal">
|
|
937
|
+
<div class="modal-overlay" onclick="hideModal()"></div>
|
|
938
|
+
<div class="modal-dialog">
|
|
939
|
+
<h3 id="modal-title"></h3>
|
|
940
|
+
<p id="modal-message"></p>
|
|
941
|
+
<div class="modal-actions">
|
|
942
|
+
<button class="btn-cancel" onclick="hideModal()">Cancel</button>
|
|
943
|
+
<button class="btn-danger" id="modal-confirm-btn">Confirm</button>
|
|
944
|
+
</div>
|
|
945
|
+
</div>
|
|
946
|
+
</div>
|
|
947
|
+
<div class="modal import-modal" id="import-modal">
|
|
948
|
+
<div class="modal-overlay" onclick="hideImportModal()"></div>
|
|
949
|
+
<div class="modal-dialog" style="max-width:560px">
|
|
950
|
+
<h3>Import Harness Files</h3>
|
|
951
|
+
<div id="import-scan-status" style="font-size:0.825rem;color:var(--color-text-secondary);margin-bottom:12px">Scanning...</div>
|
|
952
|
+
<div id="import-file-list" class="import-file-list"></div>
|
|
953
|
+
<div class="import-toggle-row">
|
|
954
|
+
<label><input type="checkbox" id="import-dry-run"> Dry Run</label>
|
|
955
|
+
<label><input type="checkbox" id="import-force"> Force Overwrite</label>
|
|
956
|
+
</div>
|
|
957
|
+
<div class="modal-actions">
|
|
958
|
+
<button class="btn-cancel" onclick="hideImportModal()">Cancel</button>
|
|
959
|
+
<button class="btn-submit" id="import-confirm-btn" onclick="executeImport()">Import</button>
|
|
960
|
+
</div>
|
|
961
|
+
</div>
|
|
962
|
+
</div>`;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function generateClientScript(): string {
|
|
966
|
+
return ` <script>
|
|
967
|
+
// --- Task 5.1: Catalog fetch and card rendering ---
|
|
968
|
+
|
|
969
|
+
var catalogData = [];
|
|
970
|
+
|
|
971
|
+
function renderCards(entries, hasActiveFilters) {
|
|
972
|
+
var grid = document.getElementById('card-grid');
|
|
973
|
+
grid.innerHTML = '';
|
|
974
|
+
|
|
975
|
+
if (entries.length === 0) {
|
|
976
|
+
var msg = hasActiveFilters ? 'No matching artifacts' : 'No artifacts found';
|
|
977
|
+
grid.innerHTML = '<div class="empty-state">' + msg + '</div>';
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
entries.forEach(function(entry) {
|
|
982
|
+
var card = document.createElement('div');
|
|
983
|
+
card.className = 'card';
|
|
984
|
+
card.setAttribute('data-name', entry.name);
|
|
985
|
+
|
|
986
|
+
var keywordsHtml = '';
|
|
987
|
+
if (entry.keywords && entry.keywords.length > 0) {
|
|
988
|
+
keywordsHtml = entry.keywords.map(function(kw) {
|
|
989
|
+
return '<span>' + escapeHtmlJs(kw) + '</span>';
|
|
990
|
+
}).join('');
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Card footer: icons only — compact, detail view shows full names
|
|
994
|
+
var harnessesHtml = '';
|
|
995
|
+
if (entry.harnesses && entry.harnesses.length > 0) {
|
|
996
|
+
harnessesHtml = entry.harnesses.map(function(h) {
|
|
997
|
+
var icon = HARNESS_ICONS[h] || h.charAt(0).toUpperCase();
|
|
998
|
+
var fmt = entry.formatByHarness && entry.formatByHarness[h] ? entry.formatByHarness[h] : '';
|
|
999
|
+
var title = h + (fmt ? ': ' + fmt : '');
|
|
1000
|
+
return '<span class="harness-icon" title="' + escapeHtmlJs(title) + '">' + icon + '</span>';
|
|
1001
|
+
}).join('');
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
var maturity = entry.maturity || 'experimental';
|
|
1005
|
+
var maturityBadge = '<span class="badge badge-maturity-' + escapeHtmlJs(maturity) + '">' + escapeHtmlJs(maturity) + '</span>';
|
|
1006
|
+
var trustBadge = entry.trust ? '<span class="badge badge-trust-' + escapeHtmlJs(entry.trust) + '">' + escapeHtmlJs(entry.trust) + '</span>' : '';
|
|
1007
|
+
|
|
1008
|
+
// Attribution relationship chip (Option A): shown only when the entry
|
|
1009
|
+
// carries projected upstream attribution. One upstream → "↳ <work> ·
|
|
1010
|
+
// <relationship>"; two or more → "↳ N sources". Absent for in-house.
|
|
1011
|
+
var attributionChip = '';
|
|
1012
|
+
var upstream = entry.attribution && entry.attribution.upstream;
|
|
1013
|
+
if (upstream && upstream.length > 0) {
|
|
1014
|
+
var chipText;
|
|
1015
|
+
if (upstream.length === 1) {
|
|
1016
|
+
var u0 = upstream[0];
|
|
1017
|
+
chipText = '↳ ' + u0.work + ' · ' + (u0.relationship || 'verbatim');
|
|
1018
|
+
} else {
|
|
1019
|
+
chipText = '↳ ' + upstream.length + ' sources';
|
|
1020
|
+
}
|
|
1021
|
+
attributionChip = '<span class="badge badge-attribution" title="' + escapeHtmlJs(chipText) + '">' + escapeHtmlJs(chipText) + '</span>';
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
var descHtml = entry.description ? '<div class="card-description">' + escapeHtmlJs(entry.description) + '</div>' : '';
|
|
1025
|
+
var kwHtml = keywordsHtml ? '<div class="card-keywords">' + keywordsHtml + '</div>' : '';
|
|
1026
|
+
|
|
1027
|
+
card.innerHTML =
|
|
1028
|
+
'<div class="card-header">' +
|
|
1029
|
+
'<span class="card-display-name">' + escapeHtmlJs(entry.displayName) + '</span>' +
|
|
1030
|
+
'<span class="card-version">' + escapeHtmlJs(entry.version || '') + '</span>' +
|
|
1031
|
+
'</div>' +
|
|
1032
|
+
'<div class="card-name">' + escapeHtmlJs(entry.name) + '</div>' +
|
|
1033
|
+
descHtml +
|
|
1034
|
+
kwHtml +
|
|
1035
|
+
'<div class="card-footer">' +
|
|
1036
|
+
'<span class="card-harnesses">' + harnessesHtml + attributionChip + '</span>' +
|
|
1037
|
+
'<div class="card-badges">' + maturityBadge + trustBadge + '</div>' +
|
|
1038
|
+
'</div>';
|
|
1039
|
+
|
|
1040
|
+
card.addEventListener('click', function() {
|
|
1041
|
+
showDetailView(entry.name);
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
grid.appendChild(card);
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function updateArtifactCount(count) {
|
|
1049
|
+
var el = document.getElementById('artifact-count');
|
|
1050
|
+
el.textContent = '(' + count + ' artifact' + (count !== 1 ? 's' : '') + ')';
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
var HARNESS_ICONS = {
|
|
1054
|
+
'kiro': '👻',
|
|
1055
|
+
'claude-code': '🤖',
|
|
1056
|
+
'copilot': '🐙',
|
|
1057
|
+
'cursor': '🖱️',
|
|
1058
|
+
'windsurf': '🏄',
|
|
1059
|
+
'cline': '🔧',
|
|
1060
|
+
'qdeveloper': '☁️',
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
function populateHarnessFilter(entries) {
|
|
1064
|
+
var harnesses = {};
|
|
1065
|
+
entries.forEach(function(entry) {
|
|
1066
|
+
if (entry.harnesses) {
|
|
1067
|
+
entry.harnesses.forEach(function(h) {
|
|
1068
|
+
harnesses[h] = true;
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
var container = document.getElementById('harness-filter');
|
|
1073
|
+
container.innerHTML = '';
|
|
1074
|
+
Object.keys(harnesses).sort().forEach(function(h) {
|
|
1075
|
+
var label = document.createElement('label');
|
|
1076
|
+
var cb = document.createElement('input');
|
|
1077
|
+
cb.type = 'checkbox';
|
|
1078
|
+
cb.value = h;
|
|
1079
|
+
cb.className = 'harness-cb';
|
|
1080
|
+
label.appendChild(cb);
|
|
1081
|
+
var icon = HARNESS_ICONS[h] || '';
|
|
1082
|
+
label.appendChild(document.createTextNode(' ' + (icon ? icon + ' ' : '') + h));
|
|
1083
|
+
container.appendChild(label);
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function populateFeaturesFilter(entries) {
|
|
1088
|
+
var featureNames = ['hooks', 'mcp', 'workflows', 'conditionalInclusion'];
|
|
1089
|
+
var featureLabels = { hooks: 'Hooks', mcp: 'MCP Servers', workflows: 'Workflows', conditionalInclusion: 'Conditional Inclusion' };
|
|
1090
|
+
var present = {};
|
|
1091
|
+
entries.forEach(function(entry) {
|
|
1092
|
+
if (entry.features) {
|
|
1093
|
+
for (var i = 0; i < featureNames.length; i++) {
|
|
1094
|
+
if (entry.features[featureNames[i]]) present[featureNames[i]] = true;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
var container = document.getElementById('features-filter');
|
|
1099
|
+
container.innerHTML = '';
|
|
1100
|
+
featureNames.forEach(function(f) {
|
|
1101
|
+
if (!present[f]) return;
|
|
1102
|
+
var label = document.createElement('label');
|
|
1103
|
+
var cb = document.createElement('input');
|
|
1104
|
+
cb.type = 'checkbox';
|
|
1105
|
+
cb.value = f;
|
|
1106
|
+
cb.className = 'features-cb';
|
|
1107
|
+
label.appendChild(cb);
|
|
1108
|
+
label.appendChild(document.createTextNode(' ' + featureLabels[f]));
|
|
1109
|
+
container.appendChild(label);
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function populateMaturityFilter(entries) {
|
|
1114
|
+
var maturities = {};
|
|
1115
|
+
entries.forEach(function(entry) {
|
|
1116
|
+
var m = entry.maturity || 'experimental';
|
|
1117
|
+
maturities[m] = true;
|
|
1118
|
+
});
|
|
1119
|
+
var container = document.getElementById('maturity-filter');
|
|
1120
|
+
container.innerHTML = '';
|
|
1121
|
+
['experimental', 'beta', 'stable', 'deprecated'].forEach(function(m) {
|
|
1122
|
+
if (!maturities[m]) return;
|
|
1123
|
+
var label = document.createElement('label');
|
|
1124
|
+
var cb = document.createElement('input');
|
|
1125
|
+
cb.type = 'checkbox';
|
|
1126
|
+
cb.value = m;
|
|
1127
|
+
cb.className = 'maturity-cb';
|
|
1128
|
+
label.appendChild(cb);
|
|
1129
|
+
label.appendChild(document.createTextNode(' ' + m));
|
|
1130
|
+
container.appendChild(label);
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function populateCollectionFilter(entries) {
|
|
1135
|
+
var collections = {};
|
|
1136
|
+
entries.forEach(function(entry) {
|
|
1137
|
+
if (entry.collections && entry.collections.length > 0) {
|
|
1138
|
+
entry.collections.forEach(function(c) { collections[c] = true; });
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
var names = Object.keys(collections).sort();
|
|
1142
|
+
var group = document.getElementById('collection-filter-group');
|
|
1143
|
+
var container = document.getElementById('collection-filter');
|
|
1144
|
+
container.innerHTML = '';
|
|
1145
|
+
if (names.length === 0) {
|
|
1146
|
+
group.style.display = 'none';
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
group.style.display = '';
|
|
1150
|
+
names.forEach(function(c) {
|
|
1151
|
+
var label = document.createElement('label');
|
|
1152
|
+
var cb = document.createElement('input');
|
|
1153
|
+
cb.type = 'checkbox';
|
|
1154
|
+
cb.value = c;
|
|
1155
|
+
cb.className = 'collection-cb';
|
|
1156
|
+
label.appendChild(cb);
|
|
1157
|
+
// Humanise: replace hyphens with spaces and title-case
|
|
1158
|
+
var display = c.replace(/-/g, ' ').replace(/\bw/g, function(l) { return l.toUpperCase(); });
|
|
1159
|
+
label.appendChild(document.createTextNode(' ' + display));
|
|
1160
|
+
container.appendChild(label);
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function escapeHtmlJs(str) {
|
|
1165
|
+
if (!str) return '';
|
|
1166
|
+
return String(str)
|
|
1167
|
+
.replace(/&/g, '&')
|
|
1168
|
+
.replace(/</g, '<')
|
|
1169
|
+
.replace(/>/g, '>')
|
|
1170
|
+
.replace(/"/g, '"')
|
|
1171
|
+
.replace(/'/g, ''');
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
1175
|
+
var isStaticMode = !!window.__CATALOG_DATA__;
|
|
1176
|
+
|
|
1177
|
+
if (isStaticMode) {
|
|
1178
|
+
// Hide interactive tabs that require live API endpoints
|
|
1179
|
+
var tabItems = document.querySelectorAll('.tab-nav-item');
|
|
1180
|
+
var hiddenViews = ['collections', 'manifest', 'graph', 'workspace', 'build'];
|
|
1181
|
+
for (var t = 0; t < tabItems.length; t++) {
|
|
1182
|
+
var view = tabItems[t].getAttribute('data-view');
|
|
1183
|
+
if (hiddenViews.indexOf(view) !== -1) {
|
|
1184
|
+
tabItems[t].style.display = 'none';
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// Hide header action buttons and build status indicator
|
|
1189
|
+
var hideIds = ['import-btn', 'build-btn', 'new-btn', 'build-status-indicator'];
|
|
1190
|
+
for (var h = 0; h < hideIds.length; h++) {
|
|
1191
|
+
var el = document.getElementById(hideIds[h]);
|
|
1192
|
+
if (el) el.style.display = 'none';
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function initCatalog(data) {
|
|
1197
|
+
catalogData = data;
|
|
1198
|
+
updateArtifactCount(catalogData.length);
|
|
1199
|
+
populateHarnessFilter(catalogData);
|
|
1200
|
+
populateFeaturesFilter(catalogData);
|
|
1201
|
+
populateMaturityFilter(catalogData);
|
|
1202
|
+
populateCollectionFilter(catalogData);
|
|
1203
|
+
|
|
1204
|
+
// In static mode, pre-select the "jh-drcc" collection filter if it exists
|
|
1205
|
+
if (isStaticMode) {
|
|
1206
|
+
var defaultCb = document.querySelector('.collection-cb[value="jh-drcc"]');
|
|
1207
|
+
if (defaultCb) {
|
|
1208
|
+
defaultCb.checked = true;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
renderCards(catalogData, false);
|
|
1213
|
+
|
|
1214
|
+
// Wire up search and filter event listeners
|
|
1215
|
+
document.getElementById('search-input').addEventListener('input', filterAndRender);
|
|
1216
|
+
document.getElementById('harness-filter').addEventListener('change', filterAndRender);
|
|
1217
|
+
document.getElementById('features-filter').addEventListener('change', filterAndRender);
|
|
1218
|
+
document.getElementById('maturity-filter').addEventListener('change', filterAndRender);
|
|
1219
|
+
document.getElementById('collection-filter').addEventListener('change', filterAndRender);
|
|
1220
|
+
|
|
1221
|
+
// Apply the jhu filter after event listeners are wired
|
|
1222
|
+
if (isStaticMode) {
|
|
1223
|
+
filterAndRender();
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
if (window.__CATALOG_DATA__) {
|
|
1228
|
+
initCatalog(window.__CATALOG_DATA__);
|
|
1229
|
+
navigateFromHash();
|
|
1230
|
+
} else {
|
|
1231
|
+
fetch('/api/catalog')
|
|
1232
|
+
.then(function(res) { return res.json(); })
|
|
1233
|
+
.then(function(data) {
|
|
1234
|
+
initCatalog(data);
|
|
1235
|
+
navigateFromHash();
|
|
1236
|
+
})
|
|
1237
|
+
.catch(function(err) {
|
|
1238
|
+
console.error('Failed to load catalog:', err);
|
|
1239
|
+
var grid = document.getElementById('card-grid');
|
|
1240
|
+
grid.innerHTML = '<div class="empty-state">Failed to load catalog data</div>';
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
window.addEventListener('hashchange', function() {
|
|
1245
|
+
navigateFromHash();
|
|
1246
|
+
});
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
function navigateFromHash() {
|
|
1250
|
+
var hash = window.location.hash.replace(/^#/, '');
|
|
1251
|
+
if (hash.indexOf('artifact/') === 0) {
|
|
1252
|
+
var name = decodeURIComponent(hash.substring('artifact/'.length));
|
|
1253
|
+
if (catalogData.some(function(e) { return e.name === name; })) {
|
|
1254
|
+
// Avoid re-render if already showing this artifact
|
|
1255
|
+
var detailView = document.getElementById('detail-view');
|
|
1256
|
+
if (detailView && detailView.style.display === 'block' && detailView.getAttribute('data-current') === name) return;
|
|
1257
|
+
showDetailView(name);
|
|
1258
|
+
}
|
|
1259
|
+
} else if (!hash) {
|
|
1260
|
+
var dv = document.getElementById('detail-view');
|
|
1261
|
+
if (dv && dv.style.display === 'block') {
|
|
1262
|
+
dv.style.display = 'none';
|
|
1263
|
+
showView(currentView);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// --- Task 5.2: Search and filter functionality ---
|
|
1269
|
+
|
|
1270
|
+
function filterAndRender() {
|
|
1271
|
+
var query = document.getElementById('search-input').value.toLowerCase();
|
|
1272
|
+
var harnessCheckboxes = document.querySelectorAll('.harness-cb:checked');
|
|
1273
|
+
var selectedHarnesses = [];
|
|
1274
|
+
for (var i = 0; i < harnessCheckboxes.length; i++) {
|
|
1275
|
+
selectedHarnesses.push(harnessCheckboxes[i].value);
|
|
1276
|
+
}
|
|
1277
|
+
var featuresCheckboxes = document.querySelectorAll('.features-cb:checked');
|
|
1278
|
+
var selectedFeatures = [];
|
|
1279
|
+
for (var i = 0; i < featuresCheckboxes.length; i++) {
|
|
1280
|
+
selectedFeatures.push(featuresCheckboxes[i].value);
|
|
1281
|
+
}
|
|
1282
|
+
var maturityCheckboxes = document.querySelectorAll('.maturity-cb:checked');
|
|
1283
|
+
var selectedMaturities = [];
|
|
1284
|
+
for (var i = 0; i < maturityCheckboxes.length; i++) {
|
|
1285
|
+
selectedMaturities.push(maturityCheckboxes[i].value);
|
|
1286
|
+
}
|
|
1287
|
+
var collectionCheckboxes = document.querySelectorAll('.collection-cb:checked');
|
|
1288
|
+
var selectedCollections = [];
|
|
1289
|
+
for (var i = 0; i < collectionCheckboxes.length; i++) {
|
|
1290
|
+
selectedCollections.push(collectionCheckboxes[i].value);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
var filtered = catalogData.filter(function(entry) {
|
|
1294
|
+
// Search filter
|
|
1295
|
+
if (query) {
|
|
1296
|
+
var nameMatch = (entry.name || '').toLowerCase().indexOf(query) !== -1;
|
|
1297
|
+
var displayNameMatch = (entry.displayName || '').toLowerCase().indexOf(query) !== -1;
|
|
1298
|
+
var descMatch = (entry.description || '').toLowerCase().indexOf(query) !== -1;
|
|
1299
|
+
var keywordMatch = false;
|
|
1300
|
+
if (entry.keywords) {
|
|
1301
|
+
for (var k = 0; k < entry.keywords.length; k++) {
|
|
1302
|
+
if (entry.keywords[k].toLowerCase().indexOf(query) !== -1) {
|
|
1303
|
+
keywordMatch = true;
|
|
1304
|
+
break;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
if (!nameMatch && !displayNameMatch && !descMatch && !keywordMatch) {
|
|
1309
|
+
return false;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
// Harness filter
|
|
1313
|
+
if (selectedHarnesses.length > 0) {
|
|
1314
|
+
var hasHarness = false;
|
|
1315
|
+
if (entry.harnesses) {
|
|
1316
|
+
for (var h = 0; h < selectedHarnesses.length; h++) {
|
|
1317
|
+
if (entry.harnesses.indexOf(selectedHarnesses[h]) !== -1) {
|
|
1318
|
+
hasHarness = true;
|
|
1319
|
+
break;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
if (!hasHarness) return false;
|
|
1324
|
+
}
|
|
1325
|
+
// Features filter
|
|
1326
|
+
if (selectedFeatures.length > 0) {
|
|
1327
|
+
var hasFeature = false;
|
|
1328
|
+
if (entry.features) {
|
|
1329
|
+
for (var f = 0; f < selectedFeatures.length; f++) {
|
|
1330
|
+
if (entry.features[selectedFeatures[f]]) {
|
|
1331
|
+
hasFeature = true;
|
|
1332
|
+
break;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
if (!hasFeature) return false;
|
|
1337
|
+
}
|
|
1338
|
+
// Maturity filter
|
|
1339
|
+
if (selectedMaturities.length > 0) {
|
|
1340
|
+
var entryMaturity = entry.maturity || 'experimental';
|
|
1341
|
+
if (selectedMaturities.indexOf(entryMaturity) === -1) return false;
|
|
1342
|
+
}
|
|
1343
|
+
// Collection filter
|
|
1344
|
+
if (selectedCollections.length > 0) {
|
|
1345
|
+
var entryCollections = entry.collections || [];
|
|
1346
|
+
var inCollection = selectedCollections.some(function(c) {
|
|
1347
|
+
return entryCollections.indexOf(c) !== -1;
|
|
1348
|
+
});
|
|
1349
|
+
if (!inCollection) return false;
|
|
1350
|
+
}
|
|
1351
|
+
return true;
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
var hasActiveFilters = query || selectedHarnesses.length > 0 || selectedFeatures.length > 0 || selectedMaturities.length > 0 || selectedCollections.length > 0;
|
|
1355
|
+
renderCards(filtered, hasActiveFilters);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// END task 5.2
|
|
1359
|
+
|
|
1360
|
+
// --- Task 5.3: Detail view ---
|
|
1361
|
+
|
|
1362
|
+
function showDetailView(name) {
|
|
1363
|
+
var entry = null;
|
|
1364
|
+
for (var i = 0; i < catalogData.length; i++) {
|
|
1365
|
+
if (catalogData[i].name === name) {
|
|
1366
|
+
entry = catalogData[i];
|
|
1367
|
+
break;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
if (!entry) return;
|
|
1371
|
+
|
|
1372
|
+
window.location.hash = 'artifact/' + encodeURIComponent(name);
|
|
1373
|
+
|
|
1374
|
+
document.getElementById('card-grid').style.display = 'none';
|
|
1375
|
+
document.getElementById('artifact-filters').style.display = 'none';
|
|
1376
|
+
var detailView = document.getElementById('detail-view');
|
|
1377
|
+
detailView.style.display = 'block';
|
|
1378
|
+
detailView.setAttribute('data-current', name);
|
|
1379
|
+
|
|
1380
|
+
var harnessesHtml = '';
|
|
1381
|
+
if (entry.harnesses && entry.harnesses.length > 0) {
|
|
1382
|
+
harnessesHtml = entry.harnesses.map(function(h) {
|
|
1383
|
+
var icon = HARNESS_ICONS[h] ? HARNESS_ICONS[h] + ' ' : '';
|
|
1384
|
+
if (entry.formatByHarness && entry.formatByHarness[h]) {
|
|
1385
|
+
return icon + escapeHtmlJs(h) + ':' + escapeHtmlJs(entry.formatByHarness[h]);
|
|
1386
|
+
}
|
|
1387
|
+
return icon + escapeHtmlJs(h);
|
|
1388
|
+
}).join(' ');
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
var keywordsHtml = '';
|
|
1392
|
+
if (entry.keywords && entry.keywords.length > 0) {
|
|
1393
|
+
keywordsHtml = entry.keywords.map(function(kw) {
|
|
1394
|
+
return '<span>' + escapeHtmlJs(kw) + '</span>';
|
|
1395
|
+
}).join('');
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
|
|
1399
|
+
var evalsStatus = entry.evals ? 'yes' : 'no';
|
|
1400
|
+
|
|
1401
|
+
var detailMaturity = entry.maturity || 'experimental';
|
|
1402
|
+
var detailMaturityBadge = '<span class="badge badge-maturity-' + escapeHtmlJs(detailMaturity) + '">' + escapeHtmlJs(detailMaturity) + '</span>';
|
|
1403
|
+
var detailTrustBadge = entry.trust ? ' <span class="badge badge-trust-' + escapeHtmlJs(entry.trust) + '">' + escapeHtmlJs(entry.trust) + '</span>' : '';
|
|
1404
|
+
|
|
1405
|
+
// Build metadata grid rows (label + value pairs)
|
|
1406
|
+
var metaRows = '';
|
|
1407
|
+
function metaRow(label, value) {
|
|
1408
|
+
return '<div class="detail-meta-label">' + label + '</div><div class="detail-meta-value">' + value + '</div>';
|
|
1409
|
+
}
|
|
1410
|
+
metaRows += metaRow('Package', '<span style="font-family:monospace">' + escapeHtmlJs(entry.name) + '</span>');
|
|
1411
|
+
metaRows += metaRow('Version', escapeHtmlJs(entry.version));
|
|
1412
|
+
if (entry.author) metaRows += metaRow('Author', escapeHtmlJs(entry.author));
|
|
1413
|
+
if (entry.license) metaRows += metaRow('License', escapeHtmlJs(entry.license));
|
|
1414
|
+
metaRows += metaRow('Type', escapeHtmlJs(entry.type || ''));
|
|
1415
|
+
if (entry.audience) metaRows += metaRow('Audience', escapeHtmlJs(entry.audience));
|
|
1416
|
+
if (entry['risk-level']) metaRows += metaRow('Risk', escapeHtmlJs(entry['risk-level']));
|
|
1417
|
+
metaRows += metaRow('Evals', evalsStatus);
|
|
1418
|
+
metaRows += metaRow('Path', '<span style="font-family:monospace;font-size:0.75rem">' + escapeHtmlJs(entry.path) + '</span>');
|
|
1419
|
+
if (entry.successor) metaRows += metaRow('Succeeded by', escapeHtmlJs(entry.successor));
|
|
1420
|
+
if (entry.replaces) metaRows += metaRow('Replaces', escapeHtmlJs(entry.replaces));
|
|
1421
|
+
|
|
1422
|
+
// Build collection badges for this artifact (task 9.4)
|
|
1423
|
+
var collectionBadgesHtml = '';
|
|
1424
|
+
if (entry.collections && entry.collections.length > 0) {
|
|
1425
|
+
collectionBadgesHtml = '<div class="detail-section-label">Collections</div><div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:20px">';
|
|
1426
|
+
for (var ci = 0; ci < entry.collections.length; ci++) {
|
|
1427
|
+
var cName = entry.collections[ci];
|
|
1428
|
+
collectionBadgesHtml += '<a href="#" class="artifact-collection-badge" data-collection="' + escapeHtmlJs(cName) + '" style="text-decoration:none">' +
|
|
1429
|
+
'<span class="badge badge-trust-community" style="cursor:pointer">' + escapeHtmlJs(cName) + '</span></a>';
|
|
1430
|
+
}
|
|
1431
|
+
collectionBadgesHtml += '</div>';
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// Sources & credits — human/legal upstream attribution (ADR-0064, Req 7).
|
|
1435
|
+
// Rendered from the projected attribution block; absent for in-house
|
|
1436
|
+
// artifacts. Replaces hand-written body "Source and adaptation" banners.
|
|
1437
|
+
var attributionHtml = '';
|
|
1438
|
+
var detailUpstream = entry.attribution && entry.attribution.upstream;
|
|
1439
|
+
if (detailUpstream && detailUpstream.length > 0) {
|
|
1440
|
+
attributionHtml = '<div class="detail-section-label">Sources & credits</div>' +
|
|
1441
|
+
'<div style="font-size:0.875rem;color:#444;margin-bottom:20px;line-height:1.7">';
|
|
1442
|
+
for (var ui = 0; ui < detailUpstream.length; ui++) {
|
|
1443
|
+
var uw = detailUpstream[ui];
|
|
1444
|
+
var authorsStr = (uw.authors && uw.authors.length > 0) ? uw.authors.join(', ') : '(author unknown)';
|
|
1445
|
+
var titleStr = uw.url
|
|
1446
|
+
? '<a href="' + escapeHtmlJs(uw.url) + '" target="_blank" rel="noopener" style="color:#3730a3;text-decoration:none">' + escapeHtmlJs(uw.work) + '</a>'
|
|
1447
|
+
: '<strong style="font-weight:600">' + escapeHtmlJs(uw.work) + '</strong>';
|
|
1448
|
+
attributionHtml += '<div style="margin-bottom:6px">' +
|
|
1449
|
+
'<span class="badge badge-attribution" style="margin-right:8px">' + escapeHtmlJs(uw.relationship || 'verbatim') + '</span>' +
|
|
1450
|
+
titleStr + ' — ' + escapeHtmlJs(authorsStr) +
|
|
1451
|
+
(uw.license ? ' <span style="color:#999">(' + escapeHtmlJs(uw.license) + ')</span>' : '') +
|
|
1452
|
+
'</div>';
|
|
1453
|
+
}
|
|
1454
|
+
if (entry.attribution['curated-by']) {
|
|
1455
|
+
attributionHtml += '<div style="margin-top:8px;color:#888">Curated by ' + escapeHtmlJs(entry.attribution['curated-by']) + '</div>';
|
|
1456
|
+
}
|
|
1457
|
+
if (entry.attribution.notice) {
|
|
1458
|
+
attributionHtml += '<div style="margin-top:6px;color:#888;font-style:italic">' + escapeHtmlJs(entry.attribution.notice) + '</div>';
|
|
1459
|
+
}
|
|
1460
|
+
attributionHtml += '</div>';
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
var html =
|
|
1464
|
+
'<div class="detail-back" id="detail-back-link"><span class="detail-back-arrow">\u2190</span> Catalog</div>' +
|
|
1465
|
+
'<div class="detail-title-row">' +
|
|
1466
|
+
'<h2>' + escapeHtmlJs(entry.displayName) + '</h2>' +
|
|
1467
|
+
'<span style="font-size:0.85rem;color:#999">' + escapeHtmlJs(entry.version) + '</span>' +
|
|
1468
|
+
'</div>' +
|
|
1469
|
+
'<div class="detail-pkg-name">' + escapeHtmlJs(entry.name) + '</div>' +
|
|
1470
|
+
(entry.description ? '<div class="detail-description">' + escapeHtmlJs(entry.description) + '</div>' : '') +
|
|
1471
|
+
'<div class="detail-badges">' + detailMaturityBadge + detailTrustBadge + '</div>' +
|
|
1472
|
+
(keywordsHtml ? '<div class="card-keywords" style="margin-bottom:20px">' + keywordsHtml + '</div>' : '') +
|
|
1473
|
+
(window.__CATALOG_DATA__ ? '' :
|
|
1474
|
+
'<div style="margin-bottom:16px">' +
|
|
1475
|
+
'<button id="detail-edit-btn" style="font-size:0.78rem;padding:5px 12px;border:1px solid var(--color-border);border-radius:var(--radius-md);background:var(--color-surface);cursor:pointer;margin-right:8px">Edit</button>' +
|
|
1476
|
+
'<button id="detail-delete-btn" style="font-size:0.78rem;padding:5px 12px;border:1px solid var(--color-error);border-radius:var(--radius-md);background:var(--color-surface);color:var(--color-error);cursor:pointer;margin-right:8px">Delete</button>' +
|
|
1477
|
+
'<button id="detail-preview-btn" style="font-size:0.78rem;padding:5px 12px;border:1px solid var(--color-border);border-radius:var(--radius-md);background:var(--color-surface);cursor:pointer">Preview</button>' +
|
|
1478
|
+
'</div>') +
|
|
1479
|
+
'<div id="detail-version-section" class="version-section"></div>' +
|
|
1480
|
+
collectionBadgesHtml +
|
|
1481
|
+
attributionHtml +
|
|
1482
|
+
'<div class="detail-section-label">Targets</div>' +
|
|
1483
|
+
'<div style="font-size:0.875rem;color:#444;margin-bottom:20px;line-height:2">' +
|
|
1484
|
+
entry.harnesses.map(function(h) {
|
|
1485
|
+
var icon = HARNESS_ICONS[h] ? HARNESS_ICONS[h] + ' ' : '';
|
|
1486
|
+
var fmt = entry.formatByHarness && entry.formatByHarness[h];
|
|
1487
|
+
return icon + '<strong style="font-weight:500">' + escapeHtmlJs(h) + '</strong>' +
|
|
1488
|
+
(fmt ? ' <span style="color:#bbb;font-size:0.8rem">→</span> <span style="color:#888">' + escapeHtmlJs(fmt) + '</span>' : '');
|
|
1489
|
+
}).join('<br>') +
|
|
1490
|
+
'</div>' +
|
|
1491
|
+
'<div id="detail-capabilities-section"></div>' +
|
|
1492
|
+
'<div id="detail-dependencies-section"></div>' +
|
|
1493
|
+
'<div class="detail-section-label">Details</div>' +
|
|
1494
|
+
'<div class="detail-meta">' + metaRows + '</div>' +
|
|
1495
|
+
'<div class="detail-section-label">Source</div>' +
|
|
1496
|
+
'<div class="detail-content"><pre>Loading...</pre></div>' +
|
|
1497
|
+
'<div id="detail-temper-panel"></div>';
|
|
1498
|
+
|
|
1499
|
+
detailView.innerHTML = html;
|
|
1500
|
+
|
|
1501
|
+
document.getElementById('detail-back-link').addEventListener('click', function() {
|
|
1502
|
+
hideDetailView();
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
// Wire edit/delete buttons (task 8.2)
|
|
1506
|
+
wireDetailActions(entry);
|
|
1507
|
+
|
|
1508
|
+
// Wire preview button (task 17.2)
|
|
1509
|
+
var previewBtn = document.getElementById('detail-preview-btn');
|
|
1510
|
+
if (previewBtn) {
|
|
1511
|
+
previewBtn.addEventListener('click', function(e) {
|
|
1512
|
+
e.stopPropagation();
|
|
1513
|
+
loadTemperPanel(entry);
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// Load capabilities (task 17.1)
|
|
1518
|
+
loadCapabilities(entry);
|
|
1519
|
+
|
|
1520
|
+
// Load version info (task 17.4)
|
|
1521
|
+
loadVersionInfo(entry);
|
|
1522
|
+
|
|
1523
|
+
// Load dependencies (task 18.3)
|
|
1524
|
+
loadDependencySection(entry);
|
|
1525
|
+
|
|
1526
|
+
// Wire collection badge clicks (task 9.4)
|
|
1527
|
+
var colBadges = detailView.querySelectorAll('.artifact-collection-badge');
|
|
1528
|
+
for (var bi = 0; bi < colBadges.length; bi++) {
|
|
1529
|
+
colBadges[bi].addEventListener('click', function(e) {
|
|
1530
|
+
e.preventDefault();
|
|
1531
|
+
var colName = this.getAttribute('data-collection');
|
|
1532
|
+
document.getElementById('detail-view').style.display = 'none';
|
|
1533
|
+
currentView = 'collections';
|
|
1534
|
+
showView('collections');
|
|
1535
|
+
setTimeout(function() { showCollectionDetail(colName); }, 200);
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
var inlineContent = null;
|
|
1540
|
+
if (window.__ARTIFACT_CONTENT__ &&
|
|
1541
|
+
Object.prototype.hasOwnProperty.call(window.__ARTIFACT_CONTENT__, name)) {
|
|
1542
|
+
inlineContent = window.__ARTIFACT_CONTENT__[name];
|
|
1543
|
+
}
|
|
1544
|
+
if (inlineContent !== null) {
|
|
1545
|
+
var contentEl = detailView.querySelector('.detail-content');
|
|
1546
|
+
if (contentEl) {
|
|
1547
|
+
contentEl.innerHTML = '<pre>' + escapeHtmlJs(inlineContent) + '</pre>';
|
|
1548
|
+
}
|
|
1549
|
+
} else {
|
|
1550
|
+
fetch('/api/artifact/' + encodeURIComponent(name) + '/content')
|
|
1551
|
+
.then(function(res) {
|
|
1552
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
1553
|
+
return res.text();
|
|
1554
|
+
})
|
|
1555
|
+
.then(function(text) {
|
|
1556
|
+
var contentEl = detailView.querySelector('.detail-content');
|
|
1557
|
+
if (contentEl) {
|
|
1558
|
+
contentEl.innerHTML = '<pre>' + escapeHtmlJs(text) + '</pre>';
|
|
1559
|
+
}
|
|
1560
|
+
})
|
|
1561
|
+
.catch(function() {
|
|
1562
|
+
var contentEl = detailView.querySelector('.detail-content');
|
|
1563
|
+
if (contentEl) {
|
|
1564
|
+
contentEl.innerHTML = '<div class="error-message">Failed to load content</div>';
|
|
1565
|
+
}
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
function hideDetailView() {
|
|
1571
|
+
history.replaceState(null, '', window.location.pathname + window.location.search);
|
|
1572
|
+
document.getElementById('detail-view').style.display = 'none';
|
|
1573
|
+
showView(currentView);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// END task 5.3
|
|
1577
|
+
|
|
1578
|
+
// --- Task 7.2: Shared render functions ---
|
|
1579
|
+
|
|
1580
|
+
var currentView = 'artifacts';
|
|
1581
|
+
var collectionsData = [];
|
|
1582
|
+
var manifestData = null;
|
|
1583
|
+
var manifestStatusData = null;
|
|
1584
|
+
|
|
1585
|
+
var ALL_HARNESSES = ['kiro','claude-code','copilot','cursor','windsurf','cline','qdeveloper'];
|
|
1586
|
+
var ALL_CATEGORIES = ['testing','security','code-style','devops','documentation','architecture','debugging','performance','accessibility'];
|
|
1587
|
+
var ARTIFACT_TYPES = ['skill','power','rule'];
|
|
1588
|
+
var INCLUSION_MODES = ['always','auto','fileMatch','manual'];
|
|
1589
|
+
|
|
1590
|
+
function renderBadge(text, variant) {
|
|
1591
|
+
return '<span class="badge badge-' + escapeHtmlJs(variant) + '">' + escapeHtmlJs(text) + '</span>';
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
function showToast(message, type) {
|
|
1595
|
+
var container = document.getElementById('toast-container');
|
|
1596
|
+
var toast = document.createElement('div');
|
|
1597
|
+
toast.className = 'toast toast--' + (type || 'success');
|
|
1598
|
+
toast.textContent = message;
|
|
1599
|
+
container.appendChild(toast);
|
|
1600
|
+
setTimeout(function() {
|
|
1601
|
+
if (toast.parentNode) toast.parentNode.removeChild(toast);
|
|
1602
|
+
}, 4000);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
var _modalConfirmCb = null;
|
|
1606
|
+
function showModal(title, message, onConfirm) {
|
|
1607
|
+
var modal = document.getElementById('modal');
|
|
1608
|
+
document.getElementById('modal-title').textContent = title;
|
|
1609
|
+
document.getElementById('modal-message').textContent = message;
|
|
1610
|
+
_modalConfirmCb = onConfirm;
|
|
1611
|
+
var confirmBtn = document.getElementById('modal-confirm-btn');
|
|
1612
|
+
var newBtn = confirmBtn.cloneNode(true);
|
|
1613
|
+
confirmBtn.parentNode.replaceChild(newBtn, confirmBtn);
|
|
1614
|
+
newBtn.id = 'modal-confirm-btn';
|
|
1615
|
+
newBtn.addEventListener('click', function() {
|
|
1616
|
+
hideModal();
|
|
1617
|
+
if (_modalConfirmCb) _modalConfirmCb();
|
|
1618
|
+
});
|
|
1619
|
+
modal.classList.add('active');
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
function hideModal() {
|
|
1623
|
+
document.getElementById('modal').classList.remove('active');
|
|
1624
|
+
_modalConfirmCb = null;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
function showView(viewName) {
|
|
1628
|
+
currentView = viewName;
|
|
1629
|
+
// Hide all content areas
|
|
1630
|
+
document.getElementById('card-grid').style.display = 'none';
|
|
1631
|
+
document.getElementById('detail-view').style.display = 'none';
|
|
1632
|
+
document.getElementById('collection-grid').style.display = 'none';
|
|
1633
|
+
document.getElementById('collection-detail-view').style.display = 'none';
|
|
1634
|
+
document.getElementById('manifest-view').style.display = 'none';
|
|
1635
|
+
document.getElementById('form-panel-container').style.display = 'none';
|
|
1636
|
+
document.getElementById('artifact-filters').style.display = 'none';
|
|
1637
|
+
document.getElementById('collection-filters').style.display = 'none';
|
|
1638
|
+
document.getElementById('workspace-view').style.display = 'none';
|
|
1639
|
+
document.getElementById('graph-view').style.display = 'none';
|
|
1640
|
+
document.getElementById('build-view').style.display = 'none';
|
|
1641
|
+
|
|
1642
|
+
// Update tab active state
|
|
1643
|
+
var tabs = document.querySelectorAll('.tab-nav-item');
|
|
1644
|
+
for (var i = 0; i < tabs.length; i++) {
|
|
1645
|
+
if (tabs[i].getAttribute('data-view') === viewName) {
|
|
1646
|
+
tabs[i].classList.add('tab-nav-item--active');
|
|
1647
|
+
} else {
|
|
1648
|
+
tabs[i].classList.remove('tab-nav-item--active');
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// Update new button label
|
|
1653
|
+
var newBtn = document.getElementById('new-btn');
|
|
1654
|
+
if (viewName === 'artifacts') {
|
|
1655
|
+
newBtn.textContent = '+ New Artifact';
|
|
1656
|
+
newBtn.style.display = '';
|
|
1657
|
+
} else if (viewName === 'collections') {
|
|
1658
|
+
newBtn.textContent = '+ New Collection';
|
|
1659
|
+
newBtn.style.display = '';
|
|
1660
|
+
} else if (viewName === 'manifest') {
|
|
1661
|
+
newBtn.textContent = '+ Add Entry';
|
|
1662
|
+
newBtn.style.display = '';
|
|
1663
|
+
} else {
|
|
1664
|
+
newBtn.style.display = 'none';
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
if (viewName === 'artifacts') {
|
|
1668
|
+
document.getElementById('artifact-filters').style.display = 'flex';
|
|
1669
|
+
document.getElementById('card-grid').style.display = 'grid';
|
|
1670
|
+
} else if (viewName === 'collections') {
|
|
1671
|
+
document.getElementById('collection-filters').style.display = 'flex';
|
|
1672
|
+
document.getElementById('collection-grid').style.display = 'grid';
|
|
1673
|
+
loadCollections();
|
|
1674
|
+
} else if (viewName === 'manifest') {
|
|
1675
|
+
document.getElementById('manifest-view').style.display = 'block';
|
|
1676
|
+
loadManifest();
|
|
1677
|
+
} else if (viewName === 'workspace') {
|
|
1678
|
+
document.getElementById('workspace-view').style.display = 'block';
|
|
1679
|
+
loadWorkspace();
|
|
1680
|
+
} else if (viewName === 'graph') {
|
|
1681
|
+
document.getElementById('graph-view').style.display = 'block';
|
|
1682
|
+
loadGraph();
|
|
1683
|
+
} else if (viewName === 'build') {
|
|
1684
|
+
document.getElementById('build-view').style.display = 'block';
|
|
1685
|
+
renderBuildPanel();
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function handleNewButton() {
|
|
1690
|
+
if (currentView === 'artifacts') {
|
|
1691
|
+
showArtifactForm('create', null);
|
|
1692
|
+
} else if (currentView === 'collections') {
|
|
1693
|
+
showCollectionForm('create', null);
|
|
1694
|
+
} else if (currentView === 'manifest') {
|
|
1695
|
+
showManifestEntryForm('create', null);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
function renderCard(data, type) {
|
|
1700
|
+
var card = document.createElement('div');
|
|
1701
|
+
card.className = 'card' + (type === 'collection' ? ' card--collection' : '');
|
|
1702
|
+
card.setAttribute('data-name', data.name);
|
|
1703
|
+
|
|
1704
|
+
if (type === 'collection') {
|
|
1705
|
+
var trustBadge = data.trust ? renderBadge(data.trust, 'trust-' + data.trust) : '';
|
|
1706
|
+
var tagsHtml = '';
|
|
1707
|
+
if (data.tags && data.tags.length > 0) {
|
|
1708
|
+
tagsHtml = '<div class="card-tags">' + data.tags.map(function(t) {
|
|
1709
|
+
return '<span>' + escapeHtmlJs(t) + '</span>';
|
|
1710
|
+
}).join('') + '</div>';
|
|
1711
|
+
}
|
|
1712
|
+
card.innerHTML =
|
|
1713
|
+
'<div class="card-header">' +
|
|
1714
|
+
'<span class="card-display-name">' + escapeHtmlJs(data.displayName || data.name) + '</span>' +
|
|
1715
|
+
'<span class="card-version">' + escapeHtmlJs(data.version || '') + '</span>' +
|
|
1716
|
+
'</div>' +
|
|
1717
|
+
'<div class="card-name">' + escapeHtmlJs(data.name) + '</div>' +
|
|
1718
|
+
(data.description ? '<div class="card-description">' + escapeHtmlJs(data.description) + '</div>' : '') +
|
|
1719
|
+
tagsHtml +
|
|
1720
|
+
'<div class="card-footer">' +
|
|
1721
|
+
'<span></span>' +
|
|
1722
|
+
'<div class="card-badges">' + trustBadge + '</div>' +
|
|
1723
|
+
'</div>';
|
|
1724
|
+
card.addEventListener('click', function() { showCollectionDetail(data.name); });
|
|
1725
|
+
} else {
|
|
1726
|
+
// artifact card - reuse existing renderCards logic inline
|
|
1727
|
+
var keywordsHtml = '';
|
|
1728
|
+
if (data.keywords && data.keywords.length > 0) {
|
|
1729
|
+
keywordsHtml = data.keywords.map(function(kw) {
|
|
1730
|
+
return '<span>' + escapeHtmlJs(kw) + '</span>';
|
|
1731
|
+
}).join('');
|
|
1732
|
+
}
|
|
1733
|
+
var harnessesHtml = '';
|
|
1734
|
+
if (data.harnesses && data.harnesses.length > 0) {
|
|
1735
|
+
harnessesHtml = data.harnesses.map(function(h) {
|
|
1736
|
+
var icon = HARNESS_ICONS[h] || h.charAt(0).toUpperCase();
|
|
1737
|
+
var fmt = data.formatByHarness && data.formatByHarness[h] ? data.formatByHarness[h] : '';
|
|
1738
|
+
var title = h + (fmt ? ': ' + fmt : '');
|
|
1739
|
+
return '<span class="harness-icon" title="' + escapeHtmlJs(title) + '">' + icon + '</span>';
|
|
1740
|
+
}).join('');
|
|
1741
|
+
}
|
|
1742
|
+
var maturity = data.maturity || 'experimental';
|
|
1743
|
+
var maturityBadge = renderBadge(maturity, 'maturity-' + maturity);
|
|
1744
|
+
var trustBadge2 = data.trust ? renderBadge(data.trust, 'trust-' + data.trust) : '';
|
|
1745
|
+
card.innerHTML =
|
|
1746
|
+
'<div class="card-header">' +
|
|
1747
|
+
'<span class="card-display-name">' + escapeHtmlJs(data.displayName) + '</span>' +
|
|
1748
|
+
'<span class="card-version">' + escapeHtmlJs(data.version || '') + '</span>' +
|
|
1749
|
+
'</div>' +
|
|
1750
|
+
'<div class="card-name">' + escapeHtmlJs(data.name) + '</div>' +
|
|
1751
|
+
(data.description ? '<div class="card-description">' + escapeHtmlJs(data.description) + '</div>' : '') +
|
|
1752
|
+
(keywordsHtml ? '<div class="card-keywords">' + keywordsHtml + '</div>' : '') +
|
|
1753
|
+
'<div class="card-footer">' +
|
|
1754
|
+
'<span class="card-harnesses">' + harnessesHtml + '</span>' +
|
|
1755
|
+
'<div class="card-badges">' + maturityBadge + trustBadge2 + '</div>' +
|
|
1756
|
+
'</div>';
|
|
1757
|
+
card.addEventListener('click', function() { showDetailView(data.name); });
|
|
1758
|
+
}
|
|
1759
|
+
return card;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
function renderForm(fields, mode, onSubmit) {
|
|
1763
|
+
var container = document.getElementById('form-panel-container');
|
|
1764
|
+
var title = mode === 'create' ? fields._title || 'Create' : fields._title || 'Edit';
|
|
1765
|
+
var html = '<div class="form-panel">' +
|
|
1766
|
+
'<div class="form-panel-header"><h2>' + escapeHtmlJs(title) + '</h2>' +
|
|
1767
|
+
'<button class="close-btn" id="form-close-btn">Cancel</button></div>' +
|
|
1768
|
+
'<form id="admin-form">';
|
|
1769
|
+
|
|
1770
|
+
var fieldDefs = fields._fields || [];
|
|
1771
|
+
for (var i = 0; i < fieldDefs.length; i++) {
|
|
1772
|
+
var f = fieldDefs[i];
|
|
1773
|
+
html += '<div class="form-group">';
|
|
1774
|
+
html += '<label class="form-label" for="field-' + f.key + '">' + escapeHtmlJs(f.label) + '</label>';
|
|
1775
|
+
|
|
1776
|
+
if (f.type === 'text') {
|
|
1777
|
+
html += '<input class="form-input" type="text" id="field-' + f.key + '" name="' + f.key + '" value="' + escapeHtmlJs(f.value || '') + '"' + (f.mono ? ' style="font-family:var(--font-mono);font-size:0.8rem"' : '') + '>';
|
|
1778
|
+
} else if (f.type === 'textarea') {
|
|
1779
|
+
html += '<textarea class="form-textarea' + (f.mono ? ' mono' : '') + '" id="field-' + f.key + '" name="' + f.key + '">' + escapeHtmlJs(f.value || '') + '</textarea>';
|
|
1780
|
+
} else if (f.type === 'checkbox-group') {
|
|
1781
|
+
html += '<div class="form-checkbox-group" id="field-' + f.key + '">';
|
|
1782
|
+
for (var j = 0; j < f.options.length; j++) {
|
|
1783
|
+
var opt = f.options[j];
|
|
1784
|
+
var checked = f.value && f.value.indexOf(opt) !== -1 ? ' checked' : '';
|
|
1785
|
+
html += '<label><input type="checkbox" name="' + f.key + '" value="' + escapeHtmlJs(opt) + '"' + checked + '> ' + escapeHtmlJs(opt) + '</label>';
|
|
1786
|
+
}
|
|
1787
|
+
html += '</div>';
|
|
1788
|
+
} else if (f.type === 'radio-group') {
|
|
1789
|
+
html += '<div class="form-radio-group" id="field-' + f.key + '">';
|
|
1790
|
+
for (var j = 0; j < f.options.length; j++) {
|
|
1791
|
+
var opt = f.options[j];
|
|
1792
|
+
var checked = f.value === opt ? ' checked' : '';
|
|
1793
|
+
html += '<label><input type="radio" name="' + f.key + '" value="' + escapeHtmlJs(opt) + '"' + checked + '> ' + escapeHtmlJs(opt) + '</label>';
|
|
1794
|
+
}
|
|
1795
|
+
html += '</div>';
|
|
1796
|
+
} else if (f.type === 'select') {
|
|
1797
|
+
html += '<select class="form-select" id="field-' + f.key + '" name="' + f.key + '">';
|
|
1798
|
+
for (var j = 0; j < f.options.length; j++) {
|
|
1799
|
+
var opt = f.options[j];
|
|
1800
|
+
var selected = f.value === opt ? ' selected' : '';
|
|
1801
|
+
html += '<option value="' + escapeHtmlJs(opt) + '"' + selected + '>' + escapeHtmlJs(opt) + '</option>';
|
|
1802
|
+
}
|
|
1803
|
+
html += '</select>';
|
|
1804
|
+
}
|
|
1805
|
+
html += '<div class="form-error" id="error-' + f.key + '"></div>';
|
|
1806
|
+
html += '</div>';
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
html += '<div class="form-actions">' +
|
|
1810
|
+
'<button type="button" class="btn-cancel" id="form-cancel-btn">Cancel</button>' +
|
|
1811
|
+
'<button type="submit" class="btn-submit">' + (mode === 'create' ? 'Create' : 'Save') + '</button>' +
|
|
1812
|
+
'</div></form></div>';
|
|
1813
|
+
|
|
1814
|
+
container.innerHTML = html;
|
|
1815
|
+
container.style.display = 'block';
|
|
1816
|
+
|
|
1817
|
+
document.getElementById('form-close-btn').addEventListener('click', function() { closeForm(); });
|
|
1818
|
+
document.getElementById('form-cancel-btn').addEventListener('click', function() { closeForm(); });
|
|
1819
|
+
document.getElementById('admin-form').addEventListener('submit', function(e) {
|
|
1820
|
+
e.preventDefault();
|
|
1821
|
+
onSubmit();
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
function closeForm() {
|
|
1826
|
+
document.getElementById('form-panel-container').style.display = 'none';
|
|
1827
|
+
showView(currentView);
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
function clearFormErrors() {
|
|
1831
|
+
var errors = document.querySelectorAll('.form-error');
|
|
1832
|
+
for (var i = 0; i < errors.length; i++) errors[i].textContent = '';
|
|
1833
|
+
var inputs = document.querySelectorAll('.has-error');
|
|
1834
|
+
for (var i = 0; i < inputs.length; i++) inputs[i].classList.remove('has-error');
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
function showFieldError(key, msg) {
|
|
1838
|
+
var el = document.getElementById('error-' + key);
|
|
1839
|
+
if (el) el.textContent = msg;
|
|
1840
|
+
var input = document.getElementById('field-' + key);
|
|
1841
|
+
if (input) input.classList.add('has-error');
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
function toKebabCase(str) {
|
|
1845
|
+
return str.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').replace(/-{2,}/g, '-');
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
function isKebabCase(str) {
|
|
1849
|
+
return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(str);
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
// END task 7.2
|
|
1853
|
+
// --- Task 8.1: Artifact create/edit form ---
|
|
1854
|
+
|
|
1855
|
+
function showArtifactForm(mode, artifactName) {
|
|
1856
|
+
// Hide other views
|
|
1857
|
+
document.getElementById('card-grid').style.display = 'none';
|
|
1858
|
+
document.getElementById('detail-view').style.display = 'none';
|
|
1859
|
+
document.getElementById('artifact-filters').style.display = 'none';
|
|
1860
|
+
document.getElementById('collection-grid').style.display = 'none';
|
|
1861
|
+
document.getElementById('collection-filters').style.display = 'none';
|
|
1862
|
+
document.getElementById('manifest-view').style.display = 'none';
|
|
1863
|
+
|
|
1864
|
+
var defaults = {
|
|
1865
|
+
name: '', displayName: '', description: '', keywords: '',
|
|
1866
|
+
author: '', version: '0.1.0', harnesses: ALL_HARNESSES.slice(),
|
|
1867
|
+
type: 'skill', inclusion: 'always', categories: [],
|
|
1868
|
+
ecosystem: '', depends: '', enhances: '', body: ''
|
|
1869
|
+
};
|
|
1870
|
+
|
|
1871
|
+
if (mode === 'edit' && artifactName) {
|
|
1872
|
+
var entry = null;
|
|
1873
|
+
for (var i = 0; i < catalogData.length; i++) {
|
|
1874
|
+
if (catalogData[i].name === artifactName) { entry = catalogData[i]; break; }
|
|
1875
|
+
}
|
|
1876
|
+
if (entry) {
|
|
1877
|
+
defaults.name = entry.name || '';
|
|
1878
|
+
defaults.displayName = entry.displayName || '';
|
|
1879
|
+
defaults.description = entry.description || '';
|
|
1880
|
+
defaults.keywords = (entry.keywords || []).join(', ');
|
|
1881
|
+
defaults.author = entry.author || '';
|
|
1882
|
+
defaults.version = entry.version || '0.1.0';
|
|
1883
|
+
defaults.harnesses = entry.harnesses || ALL_HARNESSES.slice();
|
|
1884
|
+
defaults.type = entry.type || 'skill';
|
|
1885
|
+
defaults.inclusion = entry.inclusion || 'always';
|
|
1886
|
+
defaults.categories = entry.categories || [];
|
|
1887
|
+
defaults.ecosystem = (entry.ecosystem || []).join(', ');
|
|
1888
|
+
defaults.depends = (entry.depends || []).join(', ');
|
|
1889
|
+
defaults.enhances = (entry.enhances || []).join(', ');
|
|
1890
|
+
defaults.body = '';
|
|
1891
|
+
}
|
|
1892
|
+
// Fetch body content
|
|
1893
|
+
fetch('/api/artifact/' + encodeURIComponent(artifactName) + '/content')
|
|
1894
|
+
.then(function(res) { return res.ok ? res.text() : ''; })
|
|
1895
|
+
.then(function(text) {
|
|
1896
|
+
// Extract body after frontmatter
|
|
1897
|
+
var parts = text.split('---');
|
|
1898
|
+
if (parts.length >= 3) {
|
|
1899
|
+
defaults.body = parts.slice(2).join('---').trim();
|
|
1900
|
+
}
|
|
1901
|
+
var bodyEl = document.getElementById('field-body');
|
|
1902
|
+
if (bodyEl) bodyEl.value = defaults.body;
|
|
1903
|
+
})
|
|
1904
|
+
.catch(function() {});
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
var fields = {
|
|
1908
|
+
_title: mode === 'create' ? 'New Artifact' : 'Edit Artifact',
|
|
1909
|
+
_fields: [
|
|
1910
|
+
{ key: 'displayName', label: 'Display Name', type: 'text', value: defaults.displayName },
|
|
1911
|
+
{ key: 'name', label: 'Name (kebab-case)', type: 'text', value: defaults.name, mono: true },
|
|
1912
|
+
{ key: 'description', label: 'Description', type: 'text', value: defaults.description },
|
|
1913
|
+
{ key: 'author', label: 'Author', type: 'text', value: defaults.author },
|
|
1914
|
+
{ key: 'version', label: 'Version', type: 'text', value: defaults.version },
|
|
1915
|
+
{ key: 'harnesses', label: 'Harnesses', type: 'checkbox-group', options: ALL_HARNESSES, value: defaults.harnesses },
|
|
1916
|
+
{ key: 'type', label: 'Type', type: 'radio-group', options: ARTIFACT_TYPES, value: defaults.type },
|
|
1917
|
+
{ key: 'inclusion', label: 'Inclusion', type: 'radio-group', options: INCLUSION_MODES, value: defaults.inclusion },
|
|
1918
|
+
{ key: 'categories', label: 'Categories', type: 'checkbox-group', options: ALL_CATEGORIES, value: defaults.categories },
|
|
1919
|
+
{ key: 'keywords', label: 'Keywords (comma-separated)', type: 'text', value: defaults.keywords },
|
|
1920
|
+
{ key: 'ecosystem', label: 'Ecosystem (comma-separated)', type: 'text', value: defaults.ecosystem },
|
|
1921
|
+
{ key: 'depends', label: 'Depends (comma-separated)', type: 'text', value: defaults.depends },
|
|
1922
|
+
{ key: 'enhances', label: 'Enhances (comma-separated)', type: 'text', value: defaults.enhances },
|
|
1923
|
+
{ key: 'body', label: 'Body (Markdown)', type: 'textarea', value: defaults.body, mono: true }
|
|
1924
|
+
]
|
|
1925
|
+
};
|
|
1926
|
+
|
|
1927
|
+
renderForm(fields, mode, function() { submitArtifactForm(mode, artifactName); });
|
|
1928
|
+
|
|
1929
|
+
// Wire up auto-generate name from displayName
|
|
1930
|
+
var displayNameInput = document.getElementById('field-displayName');
|
|
1931
|
+
var nameInput = document.getElementById('field-name');
|
|
1932
|
+
if (displayNameInput && nameInput) {
|
|
1933
|
+
displayNameInput.addEventListener('input', function() {
|
|
1934
|
+
if (!nameInput.value || nameInput.dataset.autoGenerated === 'true') {
|
|
1935
|
+
nameInput.value = toKebabCase(displayNameInput.value);
|
|
1936
|
+
nameInput.dataset.autoGenerated = 'true';
|
|
1937
|
+
validateNameField(nameInput);
|
|
1938
|
+
}
|
|
1939
|
+
});
|
|
1940
|
+
nameInput.addEventListener('input', function() {
|
|
1941
|
+
nameInput.dataset.autoGenerated = '';
|
|
1942
|
+
validateNameField(nameInput);
|
|
1943
|
+
});
|
|
1944
|
+
if (mode === 'create') nameInput.dataset.autoGenerated = 'true';
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
function validateNameField(input) {
|
|
1949
|
+
var errEl = document.getElementById('error-name');
|
|
1950
|
+
if (!errEl) return;
|
|
1951
|
+
if (input.value && !isKebabCase(input.value)) {
|
|
1952
|
+
errEl.textContent = 'Must be kebab-case (e.g. my-artifact-name)';
|
|
1953
|
+
input.classList.add('has-error');
|
|
1954
|
+
} else {
|
|
1955
|
+
errEl.textContent = '';
|
|
1956
|
+
input.classList.remove('has-error');
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
function parseCommaSeparated(str) {
|
|
1961
|
+
if (!str || !str.trim()) return [];
|
|
1962
|
+
return str.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s.length > 0; });
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
function getFormData() {
|
|
1966
|
+
var data = {};
|
|
1967
|
+
data.name = (document.getElementById('field-name') || {}).value || '';
|
|
1968
|
+
data.displayName = (document.getElementById('field-displayName') || {}).value || '';
|
|
1969
|
+
data.description = (document.getElementById('field-description') || {}).value || '';
|
|
1970
|
+
data.author = (document.getElementById('field-author') || {}).value || '';
|
|
1971
|
+
data.version = (document.getElementById('field-version') || {}).value || '';
|
|
1972
|
+
data.body = (document.getElementById('field-body') || {}).value || '';
|
|
1973
|
+
|
|
1974
|
+
// Checkboxes: harnesses
|
|
1975
|
+
var hCbs = document.querySelectorAll('input[name="harnesses"]:checked');
|
|
1976
|
+
data.harnesses = [];
|
|
1977
|
+
for (var i = 0; i < hCbs.length; i++) data.harnesses.push(hCbs[i].value);
|
|
1978
|
+
|
|
1979
|
+
// Radio: type
|
|
1980
|
+
var typeRadio = document.querySelector('input[name="type"]:checked');
|
|
1981
|
+
data.type = typeRadio ? typeRadio.value : 'skill';
|
|
1982
|
+
|
|
1983
|
+
// Radio: inclusion
|
|
1984
|
+
var inclRadio = document.querySelector('input[name="inclusion"]:checked');
|
|
1985
|
+
data.inclusion = inclRadio ? inclRadio.value : 'always';
|
|
1986
|
+
|
|
1987
|
+
// Checkboxes: categories
|
|
1988
|
+
var cCbs = document.querySelectorAll('input[name="categories"]:checked');
|
|
1989
|
+
data.categories = [];
|
|
1990
|
+
for (var i = 0; i < cCbs.length; i++) data.categories.push(cCbs[i].value);
|
|
1991
|
+
|
|
1992
|
+
// Comma-separated fields
|
|
1993
|
+
data.keywords = parseCommaSeparated((document.getElementById('field-keywords') || {}).value);
|
|
1994
|
+
data.ecosystem = parseCommaSeparated((document.getElementById('field-ecosystem') || {}).value);
|
|
1995
|
+
data.depends = parseCommaSeparated((document.getElementById('field-depends') || {}).value);
|
|
1996
|
+
data.enhances = parseCommaSeparated((document.getElementById('field-enhances') || {}).value);
|
|
1997
|
+
|
|
1998
|
+
return data;
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
function submitArtifactForm(mode, originalName) {
|
|
2002
|
+
clearFormErrors();
|
|
2003
|
+
var data = getFormData();
|
|
2004
|
+
|
|
2005
|
+
// Client-side validation
|
|
2006
|
+
if (!data.name) { showFieldError('name', 'Name is required'); return; }
|
|
2007
|
+
if (!isKebabCase(data.name)) { showFieldError('name', 'Must be kebab-case'); return; }
|
|
2008
|
+
|
|
2009
|
+
var url, method;
|
|
2010
|
+
if (mode === 'create') {
|
|
2011
|
+
url = '/api/artifact';
|
|
2012
|
+
method = 'POST';
|
|
2013
|
+
} else {
|
|
2014
|
+
url = '/api/artifact/' + encodeURIComponent(originalName);
|
|
2015
|
+
method = 'PUT';
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
fetch(url, {
|
|
2019
|
+
method: method,
|
|
2020
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2021
|
+
body: JSON.stringify(data)
|
|
2022
|
+
})
|
|
2023
|
+
.then(function(res) {
|
|
2024
|
+
if (res.ok) return res.json();
|
|
2025
|
+
return res.json().then(function(err) { throw { status: res.status, data: err }; });
|
|
2026
|
+
})
|
|
2027
|
+
.then(function(result) {
|
|
2028
|
+
showToast(mode === 'create' ? 'Artifact created' : 'Artifact updated', 'success');
|
|
2029
|
+
// Refresh catalog
|
|
2030
|
+
fetch('/api/catalog').then(function(r) { return r.json(); }).then(function(d) {
|
|
2031
|
+
catalogData = d;
|
|
2032
|
+
updateArtifactCount(catalogData.length);
|
|
2033
|
+
populateHarnessFilter(catalogData);
|
|
2034
|
+
populateFeaturesFilter(catalogData);
|
|
2035
|
+
populateMaturityFilter(catalogData);
|
|
2036
|
+
populateCollectionFilter(catalogData);
|
|
2037
|
+
document.getElementById('form-panel-container').style.display = 'none';
|
|
2038
|
+
showView('artifacts');
|
|
2039
|
+
showDetailView(data.name);
|
|
2040
|
+
});
|
|
2041
|
+
})
|
|
2042
|
+
.catch(function(err) {
|
|
2043
|
+
handleApiError(err);
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
// END task 8.1
|
|
2048
|
+
// --- Task 8.2: Artifact delete and error handling ---
|
|
2049
|
+
|
|
2050
|
+
function handleApiError(err) {
|
|
2051
|
+
if (!err || !err.status) {
|
|
2052
|
+
showToast('An unexpected error occurred', 'error');
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
if (err.status === 400 && err.data && err.data.details) {
|
|
2056
|
+
// Validation errors - show next to fields
|
|
2057
|
+
var details = err.data.details;
|
|
2058
|
+
if (Array.isArray(details)) {
|
|
2059
|
+
for (var i = 0; i < details.length; i++) {
|
|
2060
|
+
var d = details[i];
|
|
2061
|
+
if (d.path && d.path.length > 0) {
|
|
2062
|
+
showFieldError(d.path[d.path.length - 1], d.message || 'Invalid');
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
} else if (typeof details === 'object') {
|
|
2066
|
+
var keys = Object.keys(details);
|
|
2067
|
+
for (var i = 0; i < keys.length; i++) {
|
|
2068
|
+
showFieldError(keys[i], details[keys[i]]);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
showToast(err.data.error || 'Validation failed', 'error');
|
|
2072
|
+
} else if (err.status === 409) {
|
|
2073
|
+
showToast(err.data.error || 'Name already exists', 'error');
|
|
2074
|
+
} else if (err.status === 404) {
|
|
2075
|
+
showToast(err.data.error || 'Not found', 'error');
|
|
2076
|
+
closeForm();
|
|
2077
|
+
showView(currentView);
|
|
2078
|
+
} else {
|
|
2079
|
+
showToast(err.data.error || 'Operation failed', 'error');
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
function wireDetailActions(entry) {
|
|
2084
|
+
var editBtn = document.getElementById('detail-edit-btn');
|
|
2085
|
+
var deleteBtn = document.getElementById('detail-delete-btn');
|
|
2086
|
+
if (editBtn) {
|
|
2087
|
+
editBtn.addEventListener('click', function(e) {
|
|
2088
|
+
e.stopPropagation();
|
|
2089
|
+
showArtifactForm('edit', entry.name);
|
|
2090
|
+
});
|
|
2091
|
+
}
|
|
2092
|
+
if (deleteBtn) {
|
|
2093
|
+
deleteBtn.addEventListener('click', function(e) {
|
|
2094
|
+
e.stopPropagation();
|
|
2095
|
+
showModal(
|
|
2096
|
+
'Delete Artifact',
|
|
2097
|
+
'Are you sure you want to permanently delete "' + entry.name + '"? This action cannot be undone.',
|
|
2098
|
+
function() {
|
|
2099
|
+
fetch('/api/artifact/' + encodeURIComponent(entry.name), { method: 'DELETE' })
|
|
2100
|
+
.then(function(res) {
|
|
2101
|
+
if (res.ok) {
|
|
2102
|
+
showToast('Artifact deleted', 'success');
|
|
2103
|
+
fetch('/api/catalog').then(function(r) { return r.json(); }).then(function(d) {
|
|
2104
|
+
catalogData = d;
|
|
2105
|
+
updateArtifactCount(catalogData.length);
|
|
2106
|
+
populateHarnessFilter(catalogData);
|
|
2107
|
+
populateFeaturesFilter(catalogData);
|
|
2108
|
+
populateMaturityFilter(catalogData);
|
|
2109
|
+
populateCollectionFilter(catalogData);
|
|
2110
|
+
showView('artifacts');
|
|
2111
|
+
});
|
|
2112
|
+
} else {
|
|
2113
|
+
return res.json().then(function(err) { throw { status: res.status, data: err }; });
|
|
2114
|
+
}
|
|
2115
|
+
})
|
|
2116
|
+
.catch(function(err) { handleApiError(err); });
|
|
2117
|
+
}
|
|
2118
|
+
);
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
// END task 8.2
|
|
2124
|
+
|
|
2125
|
+
// --- Task 9.1: Collection list and detail views ---
|
|
2126
|
+
|
|
2127
|
+
function loadCollections() {
|
|
2128
|
+
fetch('/api/collections')
|
|
2129
|
+
.then(function(res) { return res.json(); })
|
|
2130
|
+
.then(function(data) {
|
|
2131
|
+
collectionsData = data;
|
|
2132
|
+
renderCollectionGrid(collectionsData);
|
|
2133
|
+
})
|
|
2134
|
+
.catch(function() {
|
|
2135
|
+
document.getElementById('collection-grid').innerHTML = '<div class="empty-state">Failed to load collections</div>';
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
function renderCollectionGrid(collections) {
|
|
2140
|
+
var grid = document.getElementById('collection-grid');
|
|
2141
|
+
grid.innerHTML = '';
|
|
2142
|
+
grid.style.display = 'grid';
|
|
2143
|
+
grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(360px, 1fr))';
|
|
2144
|
+
grid.style.gap = '12px';
|
|
2145
|
+
grid.style.padding = '24px';
|
|
2146
|
+
grid.style.maxWidth = '1440px';
|
|
2147
|
+
grid.style.margin = '0 auto';
|
|
2148
|
+
|
|
2149
|
+
if (collections.length === 0) {
|
|
2150
|
+
grid.innerHTML = '<div class="empty-state">No collections found</div>';
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
for (var i = 0; i < collections.length; i++) {
|
|
2154
|
+
grid.appendChild(renderCard(collections[i], 'collection'));
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
function showCollectionDetail(name) {
|
|
2159
|
+
document.getElementById('collection-grid').style.display = 'none';
|
|
2160
|
+
document.getElementById('collection-filters').style.display = 'none';
|
|
2161
|
+
var detailView = document.getElementById('collection-detail-view');
|
|
2162
|
+
detailView.style.display = 'block';
|
|
2163
|
+
detailView.style.padding = '32px 40px';
|
|
2164
|
+
detailView.style.maxWidth = '860px';
|
|
2165
|
+
detailView.style.margin = '0 auto';
|
|
2166
|
+
detailView.innerHTML = '<div class="empty-state">Loading...</div>';
|
|
2167
|
+
|
|
2168
|
+
fetch('/api/collections/' + encodeURIComponent(name))
|
|
2169
|
+
.then(function(res) { return res.json(); })
|
|
2170
|
+
.then(function(data) {
|
|
2171
|
+
var col = data.collection;
|
|
2172
|
+
var members = data.members || [];
|
|
2173
|
+
var trustBadge = col.trust ? renderBadge(col.trust, 'trust-' + col.trust) : '';
|
|
2174
|
+
var tagsHtml = '';
|
|
2175
|
+
if (col.tags && col.tags.length > 0) {
|
|
2176
|
+
tagsHtml = '<div class="card-tags" style="margin-bottom:20px">' + col.tags.map(function(t) {
|
|
2177
|
+
return '<span>' + escapeHtmlJs(t) + '</span>';
|
|
2178
|
+
}).join('') + '</div>';
|
|
2179
|
+
}
|
|
2180
|
+
var membersHtml = '';
|
|
2181
|
+
if (members.length > 0) {
|
|
2182
|
+
membersHtml = '<div class="detail-section-label">Member Artifacts</div><div style="display:flex;flex-direction:column;gap:6px">';
|
|
2183
|
+
for (var i = 0; i < members.length; i++) {
|
|
2184
|
+
membersHtml += '<a href="#" class="collection-member-link" data-name="' + escapeHtmlJs(members[i]) + '" style="font-family:var(--font-mono);font-size:0.825rem;color:var(--color-info);text-decoration:none">' + escapeHtmlJs(members[i]) + '</a>';
|
|
2185
|
+
}
|
|
2186
|
+
membersHtml += '</div>';
|
|
2187
|
+
} else {
|
|
2188
|
+
membersHtml = '<div class="detail-section-label">Member Artifacts</div><div style="font-size:0.875rem;color:#999">No member artifacts</div>';
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
detailView.innerHTML =
|
|
2192
|
+
'<div class="detail-back" id="collection-back-link"><span class="detail-back-arrow">\u2190</span> Collections</div>' +
|
|
2193
|
+
'<div class="detail-title-row"><h2>' + escapeHtmlJs(col.displayName || col.name) + '</h2><span style="font-size:0.85rem;color:#999">' + escapeHtmlJs(col.version || '') + '</span></div>' +
|
|
2194
|
+
'<div class="detail-pkg-name">' + escapeHtmlJs(col.name) + '</div>' +
|
|
2195
|
+
(col.description ? '<div class="detail-description">' + escapeHtmlJs(col.description) + '</div>' : '') +
|
|
2196
|
+
'<div class="detail-badges" style="margin-bottom:20px">' + trustBadge + '</div>' +
|
|
2197
|
+
tagsHtml +
|
|
2198
|
+
(window.__CATALOG_DATA__ ? '' :
|
|
2199
|
+
'<div style="margin-bottom:16px">' +
|
|
2200
|
+
'<button id="col-edit-btn" style="font-size:0.78rem;padding:5px 12px;border:1px solid var(--color-border);border-radius:var(--radius-md);background:var(--color-surface);cursor:pointer;margin-right:8px">Edit</button>' +
|
|
2201
|
+
'<button id="col-delete-btn" style="font-size:0.78rem;padding:5px 12px;border:1px solid var(--color-error);border-radius:var(--radius-md);background:var(--color-surface);color:var(--color-error);cursor:pointer">Delete</button>' +
|
|
2202
|
+
'</div>') +
|
|
2203
|
+
membersHtml;
|
|
2204
|
+
|
|
2205
|
+
document.getElementById('collection-back-link').addEventListener('click', function() {
|
|
2206
|
+
detailView.style.display = 'none';
|
|
2207
|
+
showView('collections');
|
|
2208
|
+
});
|
|
2209
|
+
|
|
2210
|
+
// Wire member links
|
|
2211
|
+
var links = detailView.querySelectorAll('.collection-member-link');
|
|
2212
|
+
for (var i = 0; i < links.length; i++) {
|
|
2213
|
+
links[i].addEventListener('click', function(e) {
|
|
2214
|
+
e.preventDefault();
|
|
2215
|
+
var artifactName = this.getAttribute('data-name');
|
|
2216
|
+
detailView.style.display = 'none';
|
|
2217
|
+
currentView = 'artifacts';
|
|
2218
|
+
showView('artifacts');
|
|
2219
|
+
showDetailView(artifactName);
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
// Wire edit/delete
|
|
2224
|
+
document.getElementById('col-edit-btn').addEventListener('click', function() {
|
|
2225
|
+
showCollectionForm('edit', col.name);
|
|
2226
|
+
});
|
|
2227
|
+
document.getElementById('col-delete-btn').addEventListener('click', function() {
|
|
2228
|
+
showModal('Delete Collection', 'Are you sure you want to permanently delete "' + col.name + '"?', function() {
|
|
2229
|
+
fetch('/api/collections/' + encodeURIComponent(col.name), { method: 'DELETE' })
|
|
2230
|
+
.then(function(res) {
|
|
2231
|
+
if (res.ok) {
|
|
2232
|
+
showToast('Collection deleted', 'success');
|
|
2233
|
+
detailView.style.display = 'none';
|
|
2234
|
+
showView('collections');
|
|
2235
|
+
} else {
|
|
2236
|
+
return res.json().then(function(err) { throw { status: res.status, data: err }; });
|
|
2237
|
+
}
|
|
2238
|
+
})
|
|
2239
|
+
.catch(function(err) { handleApiError(err); });
|
|
2240
|
+
});
|
|
2241
|
+
});
|
|
2242
|
+
})
|
|
2243
|
+
.catch(function() {
|
|
2244
|
+
detailView.innerHTML = '<div class="empty-state">Failed to load collection</div>';
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
// END task 9.1
|
|
2249
|
+
// --- Task 9.2: Collection create/edit form ---
|
|
2250
|
+
|
|
2251
|
+
function showCollectionForm(mode, collectionName) {
|
|
2252
|
+
document.getElementById('collection-grid').style.display = 'none';
|
|
2253
|
+
document.getElementById('collection-detail-view').style.display = 'none';
|
|
2254
|
+
document.getElementById('collection-filters').style.display = 'none';
|
|
2255
|
+
|
|
2256
|
+
var defaults = { name: '', displayName: '', description: '', version: '0.1.0', trust: 'community', tags: '' };
|
|
2257
|
+
|
|
2258
|
+
if (mode === 'edit' && collectionName) {
|
|
2259
|
+
var col = null;
|
|
2260
|
+
for (var i = 0; i < collectionsData.length; i++) {
|
|
2261
|
+
if (collectionsData[i].name === collectionName) { col = collectionsData[i]; break; }
|
|
2262
|
+
}
|
|
2263
|
+
if (col) {
|
|
2264
|
+
defaults.name = col.name || '';
|
|
2265
|
+
defaults.displayName = col.displayName || '';
|
|
2266
|
+
defaults.description = col.description || '';
|
|
2267
|
+
defaults.version = col.version || '0.1.0';
|
|
2268
|
+
defaults.trust = col.trust || 'community';
|
|
2269
|
+
defaults.tags = (col.tags || []).join(', ');
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
var fields = {
|
|
2274
|
+
_title: mode === 'create' ? 'New Collection' : 'Edit Collection',
|
|
2275
|
+
_fields: [
|
|
2276
|
+
{ key: 'displayName', label: 'Display Name', type: 'text', value: defaults.displayName },
|
|
2277
|
+
{ key: 'name', label: 'Name (kebab-case)', type: 'text', value: defaults.name, mono: true },
|
|
2278
|
+
{ key: 'description', label: 'Description', type: 'text', value: defaults.description },
|
|
2279
|
+
{ key: 'version', label: 'Version', type: 'text', value: defaults.version },
|
|
2280
|
+
{ key: 'trust', label: 'Trust Level', type: 'select', options: ['official', 'community'], value: defaults.trust },
|
|
2281
|
+
{ key: 'tags', label: 'Tags (comma-separated)', type: 'text', value: defaults.tags }
|
|
2282
|
+
]
|
|
2283
|
+
};
|
|
2284
|
+
|
|
2285
|
+
renderForm(fields, mode, function() { submitCollectionForm(mode, collectionName); });
|
|
2286
|
+
|
|
2287
|
+
// Wire auto-generate name
|
|
2288
|
+
var displayNameInput = document.getElementById('field-displayName');
|
|
2289
|
+
var nameInput = document.getElementById('field-name');
|
|
2290
|
+
if (displayNameInput && nameInput) {
|
|
2291
|
+
displayNameInput.addEventListener('input', function() {
|
|
2292
|
+
if (!nameInput.value || nameInput.dataset.autoGenerated === 'true') {
|
|
2293
|
+
nameInput.value = toKebabCase(displayNameInput.value);
|
|
2294
|
+
nameInput.dataset.autoGenerated = 'true';
|
|
2295
|
+
}
|
|
2296
|
+
});
|
|
2297
|
+
nameInput.addEventListener('input', function() { nameInput.dataset.autoGenerated = ''; });
|
|
2298
|
+
if (mode === 'create') nameInput.dataset.autoGenerated = 'true';
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
function submitCollectionForm(mode, originalName) {
|
|
2303
|
+
clearFormErrors();
|
|
2304
|
+
var data = {
|
|
2305
|
+
name: (document.getElementById('field-name') || {}).value || '',
|
|
2306
|
+
displayName: (document.getElementById('field-displayName') || {}).value || '',
|
|
2307
|
+
description: (document.getElementById('field-description') || {}).value || '',
|
|
2308
|
+
version: (document.getElementById('field-version') || {}).value || '',
|
|
2309
|
+
trust: (document.getElementById('field-trust') || {}).value || 'community',
|
|
2310
|
+
tags: parseCommaSeparated((document.getElementById('field-tags') || {}).value)
|
|
2311
|
+
};
|
|
2312
|
+
|
|
2313
|
+
if (!data.name) { showFieldError('name', 'Name is required'); return; }
|
|
2314
|
+
if (!isKebabCase(data.name)) { showFieldError('name', 'Must be kebab-case'); return; }
|
|
2315
|
+
|
|
2316
|
+
var url, method;
|
|
2317
|
+
if (mode === 'create') {
|
|
2318
|
+
url = '/api/collections';
|
|
2319
|
+
method = 'POST';
|
|
2320
|
+
} else {
|
|
2321
|
+
url = '/api/collections/' + encodeURIComponent(originalName);
|
|
2322
|
+
method = 'PUT';
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
fetch(url, {
|
|
2326
|
+
method: method,
|
|
2327
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2328
|
+
body: JSON.stringify(data)
|
|
2329
|
+
})
|
|
2330
|
+
.then(function(res) {
|
|
2331
|
+
if (res.ok) return res.json();
|
|
2332
|
+
return res.json().then(function(err) { throw { status: res.status, data: err }; });
|
|
2333
|
+
})
|
|
2334
|
+
.then(function(result) {
|
|
2335
|
+
showToast(mode === 'create' ? 'Collection created' : 'Collection updated', 'success');
|
|
2336
|
+
document.getElementById('form-panel-container').style.display = 'none';
|
|
2337
|
+
showView('collections');
|
|
2338
|
+
setTimeout(function() { showCollectionDetail(data.name); }, 200);
|
|
2339
|
+
})
|
|
2340
|
+
.catch(function(err) { handleApiError(err); });
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
// END task 9.2
|
|
2344
|
+
|
|
2345
|
+
// --- Task 9.3: Collection filtering ---
|
|
2346
|
+
|
|
2347
|
+
function filterCollections() {
|
|
2348
|
+
var query = (document.getElementById('collection-search-input') || {}).value || '';
|
|
2349
|
+
query = query.toLowerCase();
|
|
2350
|
+
|
|
2351
|
+
var trustCbs = document.querySelectorAll('#trust-filter input:checked');
|
|
2352
|
+
var selectedTrusts = [];
|
|
2353
|
+
for (var i = 0; i < trustCbs.length; i++) selectedTrusts.push(trustCbs[i].value);
|
|
2354
|
+
|
|
2355
|
+
var tagInput = (document.getElementById('tag-filter-input') || {}).value || '';
|
|
2356
|
+
var tagFilter = tagInput.trim().toLowerCase();
|
|
2357
|
+
|
|
2358
|
+
var filtered = collectionsData.filter(function(col) {
|
|
2359
|
+
if (query) {
|
|
2360
|
+
var nameMatch = (col.name || '').toLowerCase().indexOf(query) !== -1;
|
|
2361
|
+
var displayMatch = (col.displayName || '').toLowerCase().indexOf(query) !== -1;
|
|
2362
|
+
var descMatch = (col.description || '').toLowerCase().indexOf(query) !== -1;
|
|
2363
|
+
if (!nameMatch && !displayMatch && !descMatch) return false;
|
|
2364
|
+
}
|
|
2365
|
+
if (selectedTrusts.length > 0) {
|
|
2366
|
+
if (selectedTrusts.indexOf(col.trust || '') === -1) return false;
|
|
2367
|
+
}
|
|
2368
|
+
if (tagFilter) {
|
|
2369
|
+
var tags = col.tags || [];
|
|
2370
|
+
var hasTag = false;
|
|
2371
|
+
for (var i = 0; i < tags.length; i++) {
|
|
2372
|
+
if (tags[i].toLowerCase().indexOf(tagFilter) !== -1) { hasTag = true; break; }
|
|
2373
|
+
}
|
|
2374
|
+
if (!hasTag) return false;
|
|
2375
|
+
}
|
|
2376
|
+
return true;
|
|
2377
|
+
});
|
|
2378
|
+
|
|
2379
|
+
renderCollectionGrid(filtered);
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
function populateTrustFilter() {
|
|
2383
|
+
var trusts = {};
|
|
2384
|
+
for (var i = 0; i < collectionsData.length; i++) {
|
|
2385
|
+
var t = collectionsData[i].trust || 'community';
|
|
2386
|
+
trusts[t] = true;
|
|
2387
|
+
}
|
|
2388
|
+
var container = document.getElementById('trust-filter');
|
|
2389
|
+
container.innerHTML = '';
|
|
2390
|
+
Object.keys(trusts).sort().forEach(function(t) {
|
|
2391
|
+
var label = document.createElement('label');
|
|
2392
|
+
var cb = document.createElement('input');
|
|
2393
|
+
cb.type = 'checkbox';
|
|
2394
|
+
cb.value = t;
|
|
2395
|
+
label.appendChild(cb);
|
|
2396
|
+
label.appendChild(document.createTextNode(' ' + t));
|
|
2397
|
+
container.appendChild(label);
|
|
2398
|
+
});
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
// Wire collection filter events
|
|
2402
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
2403
|
+
var searchEl = document.getElementById('collection-search-input');
|
|
2404
|
+
if (searchEl) searchEl.addEventListener('input', filterCollections);
|
|
2405
|
+
var trustEl = document.getElementById('trust-filter');
|
|
2406
|
+
if (trustEl) trustEl.addEventListener('change', filterCollections);
|
|
2407
|
+
var tagEl = document.getElementById('tag-filter-input');
|
|
2408
|
+
if (tagEl) tagEl.addEventListener('input', filterCollections);
|
|
2409
|
+
});
|
|
2410
|
+
|
|
2411
|
+
// Override loadCollections to also populate trust filter
|
|
2412
|
+
var _origLoadCollections = loadCollections;
|
|
2413
|
+
loadCollections = function() {
|
|
2414
|
+
fetch('/api/collections')
|
|
2415
|
+
.then(function(res) { return res.json(); })
|
|
2416
|
+
.then(function(data) {
|
|
2417
|
+
collectionsData = data;
|
|
2418
|
+
populateTrustFilter();
|
|
2419
|
+
filterCollections();
|
|
2420
|
+
})
|
|
2421
|
+
.catch(function() {
|
|
2422
|
+
document.getElementById('collection-grid').innerHTML = '<div class="empty-state">Failed to load collections</div>';
|
|
2423
|
+
});
|
|
2424
|
+
};
|
|
2425
|
+
|
|
2426
|
+
// END task 9.3
|
|
2427
|
+
|
|
2428
|
+
// --- Task 9.4: Collection badges on artifact detail ---
|
|
2429
|
+
// (Integrated into showDetailView override below)
|
|
2430
|
+
|
|
2431
|
+
// END task 9.4
|
|
2432
|
+
// --- Task 10.1: Manifest table view ---
|
|
2433
|
+
|
|
2434
|
+
function loadManifest() {
|
|
2435
|
+
var view = document.getElementById('manifest-view');
|
|
2436
|
+
view.style.padding = '24px';
|
|
2437
|
+
view.style.maxWidth = '1200px';
|
|
2438
|
+
view.style.margin = '0 auto';
|
|
2439
|
+
view.innerHTML = '<div class="empty-state">Loading manifest...</div>';
|
|
2440
|
+
|
|
2441
|
+
Promise.all([
|
|
2442
|
+
fetch('/api/manifest').then(function(r) { return r.ok ? r.json() : { artifacts: [] }; }),
|
|
2443
|
+
fetch('/api/manifest/status').then(function(r) { return r.ok ? r.json() : { entries: [], syncedAt: null }; })
|
|
2444
|
+
]).then(function(results) {
|
|
2445
|
+
manifestData = results[0];
|
|
2446
|
+
manifestStatusData = results[1];
|
|
2447
|
+
renderManifestTable();
|
|
2448
|
+
}).catch(function() {
|
|
2449
|
+
view.innerHTML = '<div class="empty-state">Failed to load manifest</div>';
|
|
2450
|
+
});
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
function renderManifestTable() {
|
|
2454
|
+
var view = document.getElementById('manifest-view');
|
|
2455
|
+
var entries = (manifestData && manifestData.artifacts) || [];
|
|
2456
|
+
var statusEntries = (manifestStatusData && manifestStatusData.entries) || [];
|
|
2457
|
+
|
|
2458
|
+
if (entries.length === 0) {
|
|
2459
|
+
view.innerHTML = '<div class="empty-state">No manifest entries. Add entries to declare artifact dependencies.</div>';
|
|
2460
|
+
return;
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
var html = '<div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse;font-size:0.825rem">' +
|
|
2464
|
+
'<thead><tr style="border-bottom:2px solid var(--color-border);text-align:left">' +
|
|
2465
|
+
'<th style="padding:8px 12px;font-size:0.7rem;font-weight:600;color:var(--color-text-muted);text-transform:uppercase;letter-spacing:0.05em">Type</th>' +
|
|
2466
|
+
'<th style="padding:8px 12px;font-size:0.7rem;font-weight:600;color:var(--color-text-muted);text-transform:uppercase;letter-spacing:0.05em">Identifier</th>' +
|
|
2467
|
+
'<th style="padding:8px 12px;font-size:0.7rem;font-weight:600;color:var(--color-text-muted);text-transform:uppercase;letter-spacing:0.05em">Version</th>' +
|
|
2468
|
+
'<th style="padding:8px 12px;font-size:0.7rem;font-weight:600;color:var(--color-text-muted);text-transform:uppercase;letter-spacing:0.05em">Mode</th>' +
|
|
2469
|
+
'<th style="padding:8px 12px;font-size:0.7rem;font-weight:600;color:var(--color-text-muted);text-transform:uppercase;letter-spacing:0.05em">Harnesses</th>' +
|
|
2470
|
+
'<th style="padding:8px 12px;font-size:0.7rem;font-weight:600;color:var(--color-text-muted);text-transform:uppercase;letter-spacing:0.05em">Backend</th>' +
|
|
2471
|
+
'<th style="padding:8px 12px;font-size:0.7rem;font-weight:600;color:var(--color-text-muted);text-transform:uppercase;letter-spacing:0.05em">Status</th>' +
|
|
2472
|
+
'<th style="padding:8px 12px;font-size:0.7rem;font-weight:600;color:var(--color-text-muted);text-transform:uppercase;letter-spacing:0.05em">Actions</th>' +
|
|
2473
|
+
'</tr></thead><tbody>';
|
|
2474
|
+
|
|
2475
|
+
for (var i = 0; i < entries.length; i++) {
|
|
2476
|
+
var e = entries[i];
|
|
2477
|
+
var isCollection = !!e.collection;
|
|
2478
|
+
var identifier = isCollection ? e.collection : e.name;
|
|
2479
|
+
var typeIcon = isCollection ? '\u25C6' : '\u25CF';
|
|
2480
|
+
var typeLabel = isCollection ? 'collection' : 'artifact';
|
|
2481
|
+
var typeBadge = renderBadge(typeLabel, 'type-' + typeLabel);
|
|
2482
|
+
|
|
2483
|
+
var harnessIcons = '';
|
|
2484
|
+
if (e.harnesses && e.harnesses.length > 0) {
|
|
2485
|
+
harnessIcons = e.harnesses.map(function(h) {
|
|
2486
|
+
var icon = HARNESS_ICONS[h] || h.charAt(0).toUpperCase();
|
|
2487
|
+
return '<span class="harness-icon" title="' + escapeHtmlJs(h) + '" style="font-size:0.85rem">' + icon + '</span>';
|
|
2488
|
+
}).join('');
|
|
2489
|
+
} else {
|
|
2490
|
+
harnessIcons = '<span style="color:var(--color-text-muted);font-size:0.75rem">all</span>';
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
var modeBadge = e.mode === 'required'
|
|
2494
|
+
? '<span style="font-size:0.7rem;font-weight:600;padding:2px 6px;border-radius:4px;background:var(--color-text-primary);color:#fff">' + escapeHtmlJs(e.mode) + '</span>'
|
|
2495
|
+
: '<span style="font-size:0.7rem;font-weight:600;padding:2px 6px;border-radius:4px;border:1px solid var(--color-border);color:var(--color-text-secondary)">' + escapeHtmlJs(e.mode || 'optional') + '</span>';
|
|
2496
|
+
|
|
2497
|
+
// Find status
|
|
2498
|
+
var status = 'missing';
|
|
2499
|
+
for (var s = 0; s < statusEntries.length; s++) {
|
|
2500
|
+
if (statusEntries[s].identifier === identifier) {
|
|
2501
|
+
status = statusEntries[s].status || 'missing';
|
|
2502
|
+
break;
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
var statusColor = status === 'synced' ? 'var(--color-status-synced)' : status === 'outdated' ? 'var(--color-status-outdated)' : 'var(--color-status-missing)';
|
|
2506
|
+
var statusDot = '<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:' + statusColor + '" title="' + escapeHtmlJs(status) + '"></span>';
|
|
2507
|
+
|
|
2508
|
+
html += '<tr style="border-bottom:1px solid var(--color-border-subtle)" data-identifier="' + escapeHtmlJs(identifier) + '">' +
|
|
2509
|
+
'<td style="padding:10px 12px">' + typeIcon + ' ' + typeBadge + '</td>' +
|
|
2510
|
+
'<td style="padding:10px 12px;font-family:var(--font-mono);font-size:0.8rem">' + escapeHtmlJs(identifier) + '</td>' +
|
|
2511
|
+
'<td style="padding:10px 12px;font-family:var(--font-mono);font-size:0.8rem">' + escapeHtmlJs(e.version || '') + '</td>' +
|
|
2512
|
+
'<td style="padding:10px 12px">' + modeBadge + '</td>' +
|
|
2513
|
+
'<td style="padding:10px 12px">' + harnessIcons + '</td>' +
|
|
2514
|
+
'<td style="padding:10px 12px;font-size:0.8rem;color:var(--color-text-secondary)">' + escapeHtmlJs(e.backend || '\u2014') + '</td>' +
|
|
2515
|
+
'<td style="padding:10px 12px">' + statusDot + '</td>' +
|
|
2516
|
+
'<td style="padding:10px 12px">' +
|
|
2517
|
+
'<button class="manifest-edit-btn" data-idx="' + i + '" style="background:none;border:1px solid var(--color-border);border-radius:var(--radius-sm);padding:3px 8px;cursor:pointer;font-size:0.75rem;margin-right:4px" title="Edit">\u270E</button>' +
|
|
2518
|
+
'<button class="manifest-remove-btn" data-identifier="' + escapeHtmlJs(identifier) + '" style="background:none;border:1px solid var(--color-error);border-radius:var(--radius-sm);padding:3px 8px;cursor:pointer;font-size:0.75rem;color:var(--color-error)" title="Remove">\u00D7</button>' +
|
|
2519
|
+
'</td></tr>';
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
html += '</tbody></table></div>';
|
|
2523
|
+
view.innerHTML = html;
|
|
2524
|
+
|
|
2525
|
+
// Wire edit buttons
|
|
2526
|
+
var editBtns = view.querySelectorAll('.manifest-edit-btn');
|
|
2527
|
+
for (var i = 0; i < editBtns.length; i++) {
|
|
2528
|
+
editBtns[i].addEventListener('click', function() {
|
|
2529
|
+
var idx = parseInt(this.getAttribute('data-idx'), 10);
|
|
2530
|
+
var entry = manifestData.artifacts[idx];
|
|
2531
|
+
if (entry) showManifestEntryForm('edit', entry);
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
// Wire remove buttons
|
|
2536
|
+
var removeBtns = view.querySelectorAll('.manifest-remove-btn');
|
|
2537
|
+
for (var i = 0; i < removeBtns.length; i++) {
|
|
2538
|
+
removeBtns[i].addEventListener('click', function() {
|
|
2539
|
+
var id = this.getAttribute('data-identifier');
|
|
2540
|
+
showModal('Remove Entry', 'Remove "' + id + '" from the manifest?', function() {
|
|
2541
|
+
fetch('/api/manifest/entries/' + encodeURIComponent(id), { method: 'DELETE' })
|
|
2542
|
+
.then(function(res) {
|
|
2543
|
+
if (res.ok) {
|
|
2544
|
+
showToast('Entry removed', 'success');
|
|
2545
|
+
loadManifest();
|
|
2546
|
+
} else {
|
|
2547
|
+
return res.json().then(function(err) { throw { status: res.status, data: err }; });
|
|
2548
|
+
}
|
|
2549
|
+
})
|
|
2550
|
+
.catch(function(err) { handleApiError(err); });
|
|
2551
|
+
});
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
// END task 10.1
|
|
2557
|
+
// --- Task 10.2: Manifest add/edit/remove controls ---
|
|
2558
|
+
|
|
2559
|
+
function showManifestEntryForm(mode, existingEntry) {
|
|
2560
|
+
document.getElementById('manifest-view').style.display = 'none';
|
|
2561
|
+
|
|
2562
|
+
var isCollection = existingEntry ? !!existingEntry.collection : false;
|
|
2563
|
+
var defaults = {
|
|
2564
|
+
refType: isCollection ? 'collection' : 'artifact',
|
|
2565
|
+
name: existingEntry ? (existingEntry.name || '') : '',
|
|
2566
|
+
collection: existingEntry ? (existingEntry.collection || '') : '',
|
|
2567
|
+
version: existingEntry ? (existingEntry.version || '') : '',
|
|
2568
|
+
mode: existingEntry ? (existingEntry.mode || 'required') : 'required',
|
|
2569
|
+
harnesses: existingEntry ? (existingEntry.harnesses || []) : [],
|
|
2570
|
+
backend: existingEntry ? (existingEntry.backend || '') : ''
|
|
2571
|
+
};
|
|
2572
|
+
|
|
2573
|
+
var fields = {
|
|
2574
|
+
_title: mode === 'create' ? 'Add Manifest Entry' : 'Edit Manifest Entry',
|
|
2575
|
+
_fields: [
|
|
2576
|
+
{ key: 'refType', label: 'Reference Type', type: 'radio-group', options: ['artifact', 'collection'], value: defaults.refType },
|
|
2577
|
+
{ key: 'name', label: 'Artifact Name', type: 'text', value: defaults.name, mono: true },
|
|
2578
|
+
{ key: 'collection', label: 'Collection Name', type: 'text', value: defaults.collection, mono: true },
|
|
2579
|
+
{ key: 'version', label: 'Version', type: 'text', value: defaults.version, mono: true },
|
|
2580
|
+
{ key: 'mode', label: 'Mode', type: 'radio-group', options: ['required', 'optional'], value: defaults.mode },
|
|
2581
|
+
{ key: 'harnesses', label: 'Harnesses', type: 'checkbox-group', options: ALL_HARNESSES, value: defaults.harnesses },
|
|
2582
|
+
{ key: 'backend', label: 'Backend (optional)', type: 'text', value: defaults.backend }
|
|
2583
|
+
]
|
|
2584
|
+
};
|
|
2585
|
+
|
|
2586
|
+
renderForm(fields, mode, function() { submitManifestEntryForm(mode, existingEntry); });
|
|
2587
|
+
|
|
2588
|
+
// Toggle name/collection visibility based on refType
|
|
2589
|
+
function updateRefTypeVisibility() {
|
|
2590
|
+
var refType = 'artifact';
|
|
2591
|
+
var radio = document.querySelector('input[name="refType"]:checked');
|
|
2592
|
+
if (radio) refType = radio.value;
|
|
2593
|
+
var nameGroup = document.getElementById('field-name');
|
|
2594
|
+
var colGroup = document.getElementById('field-collection');
|
|
2595
|
+
if (nameGroup) nameGroup.parentNode.style.display = refType === 'artifact' ? '' : 'none';
|
|
2596
|
+
if (colGroup) colGroup.parentNode.style.display = refType === 'collection' ? '' : 'none';
|
|
2597
|
+
}
|
|
2598
|
+
updateRefTypeVisibility();
|
|
2599
|
+
var refRadios = document.querySelectorAll('input[name="refType"]');
|
|
2600
|
+
for (var i = 0; i < refRadios.length; i++) {
|
|
2601
|
+
refRadios[i].addEventListener('change', updateRefTypeVisibility);
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
function submitManifestEntryForm(mode, existingEntry) {
|
|
2606
|
+
clearFormErrors();
|
|
2607
|
+
var refType = 'artifact';
|
|
2608
|
+
var radio = document.querySelector('input[name="refType"]:checked');
|
|
2609
|
+
if (radio) refType = radio.value;
|
|
2610
|
+
|
|
2611
|
+
var data = {};
|
|
2612
|
+
if (refType === 'artifact') {
|
|
2613
|
+
data.name = (document.getElementById('field-name') || {}).value || '';
|
|
2614
|
+
} else {
|
|
2615
|
+
data.collection = (document.getElementById('field-collection') || {}).value || '';
|
|
2616
|
+
}
|
|
2617
|
+
data.version = (document.getElementById('field-version') || {}).value || '';
|
|
2618
|
+
var modeRadio = document.querySelector('input[name="mode"]:checked');
|
|
2619
|
+
data.mode = modeRadio ? modeRadio.value : 'required';
|
|
2620
|
+
|
|
2621
|
+
var hCbs = document.querySelectorAll('input[name="harnesses"]:checked');
|
|
2622
|
+
data.harnesses = [];
|
|
2623
|
+
for (var i = 0; i < hCbs.length; i++) data.harnesses.push(hCbs[i].value);
|
|
2624
|
+
if (data.harnesses.length === 0) delete data.harnesses;
|
|
2625
|
+
|
|
2626
|
+
var backend = (document.getElementById('field-backend') || {}).value || '';
|
|
2627
|
+
if (backend) data.backend = backend;
|
|
2628
|
+
|
|
2629
|
+
if (refType === 'artifact' && !data.name) { showFieldError('name', 'Name is required'); return; }
|
|
2630
|
+
if (refType === 'collection' && !data.collection) { showFieldError('collection', 'Collection name is required'); return; }
|
|
2631
|
+
if (!data.version) { showFieldError('version', 'Version is required'); return; }
|
|
2632
|
+
|
|
2633
|
+
var url, method;
|
|
2634
|
+
if (mode === 'create') {
|
|
2635
|
+
url = '/api/manifest/entries';
|
|
2636
|
+
method = 'POST';
|
|
2637
|
+
} else {
|
|
2638
|
+
var identifier = existingEntry.collection || existingEntry.name;
|
|
2639
|
+
url = '/api/manifest/entries/' + encodeURIComponent(identifier);
|
|
2640
|
+
method = 'PUT';
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
fetch(url, {
|
|
2644
|
+
method: method,
|
|
2645
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2646
|
+
body: JSON.stringify(data)
|
|
2647
|
+
})
|
|
2648
|
+
.then(function(res) {
|
|
2649
|
+
if (res.ok) return res.json();
|
|
2650
|
+
return res.json().then(function(err) { throw { status: res.status, data: err }; });
|
|
2651
|
+
})
|
|
2652
|
+
.then(function() {
|
|
2653
|
+
showToast(mode === 'create' ? 'Entry added' : 'Entry updated', 'success');
|
|
2654
|
+
document.getElementById('form-panel-container').style.display = 'none';
|
|
2655
|
+
showView('manifest');
|
|
2656
|
+
})
|
|
2657
|
+
.catch(function(err) { handleApiError(err); });
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
// END task 10.2
|
|
2661
|
+
// END task 10.2
|
|
2662
|
+
|
|
2663
|
+
// --- Task 17.1: Capability badges and matrix grid ---
|
|
2664
|
+
var _capabilityCache = {};
|
|
2665
|
+
var CAP_NAMES = ['hooks','mcp','path_scoping','workflows','toggleable_rules','agents','file_match_inclusion','system_prompt_merging'];
|
|
2666
|
+
|
|
2667
|
+
function loadCapabilities(entry) {
|
|
2668
|
+
var section = document.getElementById('detail-capabilities-section');
|
|
2669
|
+
if (!section) return;
|
|
2670
|
+
if (!entry.harnesses || entry.harnesses.length === 0) { section.innerHTML = ''; return; }
|
|
2671
|
+
|
|
2672
|
+
function renderCaps(matrix) {
|
|
2673
|
+
var harnesses = entry.harnesses;
|
|
2674
|
+
var html = '<div class="detail-section-label">Harness Capabilities</div>';
|
|
2675
|
+
if (harnesses.length >= 3) {
|
|
2676
|
+
var cols = harnesses.length + 1;
|
|
2677
|
+
html += '<div class="cap-matrix" style="grid-template-columns: auto ' + harnesses.map(function() { return '1fr'; }).join(' ') + '">';
|
|
2678
|
+
html += '<div class="cap-matrix-header"></div>';
|
|
2679
|
+
for (var h = 0; h < harnesses.length; h++) {
|
|
2680
|
+
var icon = HARNESS_ICONS[harnesses[h]] || '';
|
|
2681
|
+
html += '<div class="cap-matrix-header" style="text-align:center">' + icon + ' ' + escapeHtmlJs(harnesses[h]) + '</div>';
|
|
2682
|
+
}
|
|
2683
|
+
for (var c = 0; c < CAP_NAMES.length; c++) {
|
|
2684
|
+
var cap = CAP_NAMES[c];
|
|
2685
|
+
html += '<div class="cap-matrix-label">' + escapeHtmlJs(cap.replace(/_/g, ' ')) + '</div>';
|
|
2686
|
+
for (var h = 0; h < harnesses.length; h++) {
|
|
2687
|
+
var hData = matrix[harnesses[h]];
|
|
2688
|
+
var capEntry = hData ? hData[cap] : null;
|
|
2689
|
+
var support = capEntry ? capEntry.support : 'none';
|
|
2690
|
+
var deg = capEntry && capEntry.degradation ? capEntry.degradation : '';
|
|
2691
|
+
var cls = 'badge-cap-' + support;
|
|
2692
|
+
var title = support + (deg ? ' (' + deg + ')' : '');
|
|
2693
|
+
html += '<div class="cap-matrix-cell ' + cls + '" title="' + escapeHtmlJs(title) + '">' + escapeHtmlJs(support) + (deg ? '<br><span style="font-size:0.6rem;opacity:0.8">' + escapeHtmlJs(deg) + '</span>' : '') + '</div>';
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
html += '</div>';
|
|
2697
|
+
} else {
|
|
2698
|
+
for (var h = 0; h < harnesses.length; h++) {
|
|
2699
|
+
var hName = harnesses[h];
|
|
2700
|
+
var icon = HARNESS_ICONS[hName] || '';
|
|
2701
|
+
var hData = matrix[hName];
|
|
2702
|
+
html += '<div class="cap-card"><div class="cap-card-title">' + icon + ' ' + escapeHtmlJs(hName) + '</div><div class="cap-card-badges">';
|
|
2703
|
+
for (var c = 0; c < CAP_NAMES.length; c++) {
|
|
2704
|
+
var cap = CAP_NAMES[c];
|
|
2705
|
+
var capEntry = hData ? hData[cap] : null;
|
|
2706
|
+
var support = capEntry ? capEntry.support : 'none';
|
|
2707
|
+
var deg = capEntry && capEntry.degradation ? capEntry.degradation : '';
|
|
2708
|
+
var cls = 'badge-cap-' + support;
|
|
2709
|
+
var title = cap.replace(/_/g, ' ') + ': ' + support + (deg ? ' (' + deg + ')' : '');
|
|
2710
|
+
html += '<span class="badge ' + cls + '" title="' + escapeHtmlJs(title) + '">' + escapeHtmlJs(cap.replace(/_/g, ' ')) + '</span>';
|
|
2711
|
+
}
|
|
2712
|
+
html += '</div></div>';
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
section.innerHTML = html;
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
if (Object.keys(_capabilityCache).length > 0) {
|
|
2719
|
+
renderCaps(_capabilityCache);
|
|
2720
|
+
} else if (window.__CAPABILITY_MATRIX__) {
|
|
2721
|
+
_capabilityCache = window.__CAPABILITY_MATRIX__;
|
|
2722
|
+
renderCaps(_capabilityCache);
|
|
2723
|
+
} else {
|
|
2724
|
+
fetch('/api/capabilities')
|
|
2725
|
+
.then(function(res) { return res.ok ? res.json() : {}; })
|
|
2726
|
+
.then(function(data) { _capabilityCache = data; renderCaps(data); })
|
|
2727
|
+
.catch(function() { section.innerHTML = ''; });
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
// END task 17.1
|
|
2731
|
+
|
|
2732
|
+
// --- Task 17.2: Inline temper panel ---
|
|
2733
|
+
function loadTemperPanel(entry) {
|
|
2734
|
+
var panel = document.getElementById('detail-temper-panel');
|
|
2735
|
+
if (!panel) return;
|
|
2736
|
+
var harnesses = entry.harnesses || [];
|
|
2737
|
+
if (harnesses.length === 0) { panel.innerHTML = ''; return; }
|
|
2738
|
+
|
|
2739
|
+
var selectedHarness = harnesses[0];
|
|
2740
|
+
function renderPanel() {
|
|
2741
|
+
panel.innerHTML = '<div class="temper-panel">' +
|
|
2742
|
+
'<div class="temper-panel-header"><h3>Preview</h3>' +
|
|
2743
|
+
'<select id="temper-harness-select" style="font-family:var(--font-body);font-size:0.8rem;padding:4px 8px;border:1px solid var(--color-border);border-radius:var(--radius-md)">' +
|
|
2744
|
+
harnesses.map(function(h) {
|
|
2745
|
+
return '<option value="' + escapeHtmlJs(h) + '"' + (h === selectedHarness ? ' selected' : '') + '>' + escapeHtmlJs(h) + '</option>';
|
|
2746
|
+
}).join('') +
|
|
2747
|
+
'</select></div>' +
|
|
2748
|
+
'<div id="temper-content" style="font-size:0.825rem;color:var(--color-text-secondary)">Loading...</div></div>';
|
|
2749
|
+
|
|
2750
|
+
document.getElementById('temper-harness-select').addEventListener('change', function() {
|
|
2751
|
+
selectedHarness = this.value;
|
|
2752
|
+
fetchTemper();
|
|
2753
|
+
});
|
|
2754
|
+
fetchTemper();
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
function fetchTemper() {
|
|
2758
|
+
var contentEl = document.getElementById('temper-content');
|
|
2759
|
+
if (contentEl) contentEl.innerHTML = '<div style="color:var(--color-text-muted)">Loading preview...</div>';
|
|
2760
|
+
fetch('/api/temper', {
|
|
2761
|
+
method: 'POST',
|
|
2762
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2763
|
+
body: JSON.stringify({ artifactName: entry.name, harness: selectedHarness })
|
|
2764
|
+
})
|
|
2765
|
+
.then(function(res) { return res.ok ? res.json() : null; })
|
|
2766
|
+
.then(function(data) {
|
|
2767
|
+
if (!data || !contentEl) return;
|
|
2768
|
+
var html = '';
|
|
2769
|
+
if (data.sections && data.sections.length > 0) {
|
|
2770
|
+
for (var i = 0; i < data.sections.length; i++) {
|
|
2771
|
+
var s = data.sections[i];
|
|
2772
|
+
html += '<div class="temper-section">';
|
|
2773
|
+
html += '<div class="temper-section-title">' + escapeHtmlJs(s.title || s.type) + '</div>';
|
|
2774
|
+
html += '<div class="temper-section-content">' + escapeHtmlJs(s.content || '') + '</div>';
|
|
2775
|
+
html += '</div>';
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
if (data.degradations && data.degradations.length > 0) {
|
|
2779
|
+
html += '<div class="temper-section"><div class="temper-section-title">Degradation Report</div><div style="font-size:0.8rem">';
|
|
2780
|
+
for (var i = 0; i < data.degradations.length; i++) {
|
|
2781
|
+
var d = data.degradations[i];
|
|
2782
|
+
var cls = d.strategy === 'inline' ? 'temper-deg-inline' : d.strategy === 'comment' ? 'temper-deg-comment' : 'temper-deg-omit';
|
|
2783
|
+
html += '<div style="padding:4px 0"><span class="' + cls + '" style="font-weight:600">' + escapeHtmlJs(d.strategy || '') + '</span> — ' + escapeHtmlJs(d.capability || d.message || '') + '</div>';
|
|
2784
|
+
}
|
|
2785
|
+
html += '</div></div>';
|
|
2786
|
+
}
|
|
2787
|
+
if (!html) html = '<div style="color:var(--color-text-muted)">No preview data available</div>';
|
|
2788
|
+
if (data.fileCount !== undefined) {
|
|
2789
|
+
html += '<div style="margin-top:12px;font-size:0.75rem;color:var(--color-text-muted)">' +
|
|
2790
|
+
'Files: ' + data.fileCount + ' | Hooks translated: ' + (data.hooksTranslated || 0) + ' | Hooks degraded: ' + (data.hooksDegraded || 0) +
|
|
2791
|
+
(data.mcpServers ? ' | MCP servers: ' + data.mcpServers : '') + '</div>';
|
|
2792
|
+
}
|
|
2793
|
+
contentEl.innerHTML = html;
|
|
2794
|
+
})
|
|
2795
|
+
.catch(function() {
|
|
2796
|
+
if (contentEl) contentEl.innerHTML = '<div style="color:var(--color-error)">Failed to load preview</div>';
|
|
2797
|
+
});
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
renderPanel();
|
|
2801
|
+
}
|
|
2802
|
+
// END task 17.2
|
|
2803
|
+
|
|
2804
|
+
// --- Task 17.3: Import modal ---
|
|
2805
|
+
function openImportModal() {
|
|
2806
|
+
var modal = document.getElementById('import-modal');
|
|
2807
|
+
modal.classList.add('active');
|
|
2808
|
+
document.getElementById('import-scan-status').textContent = 'Scanning for harness files...';
|
|
2809
|
+
document.getElementById('import-file-list').innerHTML = '';
|
|
2810
|
+
document.getElementById('import-dry-run').checked = false;
|
|
2811
|
+
document.getElementById('import-force').checked = false;
|
|
2812
|
+
|
|
2813
|
+
fetch('/api/import/scan', { method: 'POST' })
|
|
2814
|
+
.then(function(res) { return res.ok ? res.json() : null; })
|
|
2815
|
+
.then(function(data) {
|
|
2816
|
+
if (!data) {
|
|
2817
|
+
document.getElementById('import-scan-status').textContent = 'Scan failed';
|
|
2818
|
+
return;
|
|
2819
|
+
}
|
|
2820
|
+
var harnesses = Object.keys(data);
|
|
2821
|
+
var totalFiles = 0;
|
|
2822
|
+
harnesses.forEach(function(h) { totalFiles += (data[h] || []).length; });
|
|
2823
|
+
if (totalFiles === 0) {
|
|
2824
|
+
document.getElementById('import-scan-status').textContent = 'No harness files detected';
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
document.getElementById('import-scan-status').textContent = totalFiles + ' file(s) detected';
|
|
2828
|
+
var listHtml = '';
|
|
2829
|
+
for (var i = 0; i < harnesses.length; i++) {
|
|
2830
|
+
var h = harnesses[i];
|
|
2831
|
+
var files = data[h] || [];
|
|
2832
|
+
if (files.length === 0) continue;
|
|
2833
|
+
var icon = HARNESS_ICONS[h] || '';
|
|
2834
|
+
listHtml += '<div class="import-harness-group"><div class="import-harness-label">' + icon + ' ' + escapeHtmlJs(h) + '</div>';
|
|
2835
|
+
for (var j = 0; j < files.length; j++) {
|
|
2836
|
+
listHtml += '<div class="import-file-item"><input type="checkbox" class="import-file-cb" value="' + escapeHtmlJs(files[j]) + '" data-harness="' + escapeHtmlJs(h) + '" checked> ' + escapeHtmlJs(files[j]) + '</div>';
|
|
2837
|
+
}
|
|
2838
|
+
listHtml += '</div>';
|
|
2839
|
+
}
|
|
2840
|
+
document.getElementById('import-file-list').innerHTML = listHtml;
|
|
2841
|
+
})
|
|
2842
|
+
.catch(function() {
|
|
2843
|
+
document.getElementById('import-scan-status').textContent = 'Scan failed';
|
|
2844
|
+
});
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
function hideImportModal() {
|
|
2848
|
+
document.getElementById('import-modal').classList.remove('active');
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
function executeImport() {
|
|
2852
|
+
var cbs = document.querySelectorAll('.import-file-cb:checked');
|
|
2853
|
+
var files = [];
|
|
2854
|
+
for (var i = 0; i < cbs.length; i++) files.push(cbs[i].value);
|
|
2855
|
+
if (files.length === 0) { showToast('No files selected', 'error'); return; }
|
|
2856
|
+
|
|
2857
|
+
var dryRun = document.getElementById('import-dry-run').checked;
|
|
2858
|
+
var force = document.getElementById('import-force').checked;
|
|
2859
|
+
|
|
2860
|
+
var body = { files: files, force: force, dryRun: dryRun };
|
|
2861
|
+
fetch('/api/import', {
|
|
2862
|
+
method: 'POST',
|
|
2863
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2864
|
+
body: JSON.stringify(body)
|
|
2865
|
+
})
|
|
2866
|
+
.then(function(res) {
|
|
2867
|
+
if (res.status === 409) {
|
|
2868
|
+
return res.json().then(function(data) {
|
|
2869
|
+
hideImportModal();
|
|
2870
|
+
showModal('Import Conflict', 'Conflicting artifacts: ' + (data.conflicts || []).join(', ') + '. Retry with force?', function() {
|
|
2871
|
+
body.force = true;
|
|
2872
|
+
fetch('/api/import', {
|
|
2873
|
+
method: 'POST',
|
|
2874
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2875
|
+
body: JSON.stringify(body)
|
|
2876
|
+
}).then(function(r) { return r.json(); }).then(function(result) {
|
|
2877
|
+
showToast('Imported ' + (result.artifacts || []).length + ' artifact(s)', 'success');
|
|
2878
|
+
refreshCatalog();
|
|
2879
|
+
}).catch(function() { showToast('Import failed', 'error'); });
|
|
2880
|
+
});
|
|
2881
|
+
return null;
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2884
|
+
return res.ok ? res.json() : null;
|
|
2885
|
+
})
|
|
2886
|
+
.then(function(data) {
|
|
2887
|
+
if (!data) return;
|
|
2888
|
+
hideImportModal();
|
|
2889
|
+
if (dryRun) {
|
|
2890
|
+
showToast('Dry run: ' + (data.artifacts || []).length + ' artifact(s) would be imported', 'success');
|
|
2891
|
+
} else {
|
|
2892
|
+
showToast('Imported ' + (data.artifacts || []).length + ' artifact(s)', 'success');
|
|
2893
|
+
refreshCatalog();
|
|
2894
|
+
}
|
|
2895
|
+
})
|
|
2896
|
+
.catch(function() { showToast('Import failed', 'error'); });
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
function refreshCatalog() {
|
|
2900
|
+
fetch('/api/catalog').then(function(r) { return r.json(); }).then(function(d) {
|
|
2901
|
+
catalogData = d;
|
|
2902
|
+
updateArtifactCount(catalogData.length);
|
|
2903
|
+
populateHarnessFilter(catalogData);
|
|
2904
|
+
populateFeaturesFilter(catalogData);
|
|
2905
|
+
populateMaturityFilter(catalogData);
|
|
2906
|
+
populateCollectionFilter(catalogData);
|
|
2907
|
+
filterAndRender();
|
|
2908
|
+
});
|
|
2909
|
+
}
|
|
2910
|
+
// END task 17.3
|
|
2911
|
+
|
|
2912
|
+
// --- Task 17.4: Version display and upgrade button ---
|
|
2913
|
+
function loadVersionInfo(entry) {
|
|
2914
|
+
var section = document.getElementById('detail-version-section');
|
|
2915
|
+
if (!section) return;
|
|
2916
|
+
|
|
2917
|
+
fetch('/api/versions/' + encodeURIComponent(entry.name))
|
|
2918
|
+
.then(function(res) { return res.ok ? res.json() : null; })
|
|
2919
|
+
.then(function(data) {
|
|
2920
|
+
if (!data) { section.innerHTML = ''; return; }
|
|
2921
|
+
var html = '<div class="detail-section-label">Version</div>';
|
|
2922
|
+
html += '<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">';
|
|
2923
|
+
html += '<span style="font-family:var(--font-mono);font-size:0.875rem;font-weight:600">' + escapeHtmlJs(data.sourceVersion || entry.version || '0.1.0') + '</span>';
|
|
2924
|
+
if (data.installedVersion) {
|
|
2925
|
+
html += '<span style="font-size:0.78rem;color:var(--color-text-muted)">installed: ' + escapeHtmlJs(data.installedVersion) + '</span>';
|
|
2926
|
+
} else {
|
|
2927
|
+
html += '<span class="version-not-installed">Not installed</span>';
|
|
2928
|
+
}
|
|
2929
|
+
if (data.upgradeAvailable) {
|
|
2930
|
+
html += '<span class="version-badge-upgrade">Upgrade Available</span>';
|
|
2931
|
+
html += '<button id="detail-upgrade-btn" style="font-size:0.75rem;padding:4px 10px;border:1px solid var(--color-info);border-radius:var(--radius-md);background:var(--color-info-bg);color:var(--color-info);cursor:pointer">Upgrade</button>';
|
|
2932
|
+
}
|
|
2933
|
+
html += '</div>';
|
|
2934
|
+
if (data.changelog) {
|
|
2935
|
+
html += '<div class="changelog-section"><div class="detail-section-label" style="margin-top:0">Changelog</div><pre>' + escapeHtmlJs(data.changelog) + '</pre></div>';
|
|
2936
|
+
}
|
|
2937
|
+
section.innerHTML = html;
|
|
2938
|
+
|
|
2939
|
+
if (data.upgradeAvailable) {
|
|
2940
|
+
var upgradeBtn = document.getElementById('detail-upgrade-btn');
|
|
2941
|
+
if (upgradeBtn) {
|
|
2942
|
+
upgradeBtn.addEventListener('click', function() {
|
|
2943
|
+
upgradeBtn.textContent = 'Upgrading...';
|
|
2944
|
+
upgradeBtn.disabled = true;
|
|
2945
|
+
fetch('/api/upgrade/' + encodeURIComponent(entry.name), { method: 'POST' })
|
|
2946
|
+
.then(function(res) { return res.ok ? res.json() : null; })
|
|
2947
|
+
.then(function(result) {
|
|
2948
|
+
if (result) {
|
|
2949
|
+
showToast('Upgraded to ' + (result.version || 'latest'), 'success');
|
|
2950
|
+
loadVersionInfo(entry);
|
|
2951
|
+
} else {
|
|
2952
|
+
showToast('Upgrade failed', 'error');
|
|
2953
|
+
upgradeBtn.textContent = 'Upgrade';
|
|
2954
|
+
upgradeBtn.disabled = false;
|
|
2955
|
+
}
|
|
2956
|
+
})
|
|
2957
|
+
.catch(function() {
|
|
2958
|
+
showToast('Upgrade failed', 'error');
|
|
2959
|
+
upgradeBtn.textContent = 'Upgrade';
|
|
2960
|
+
upgradeBtn.disabled = false;
|
|
2961
|
+
});
|
|
2962
|
+
});
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
})
|
|
2966
|
+
.catch(function() { section.innerHTML = ''; });
|
|
2967
|
+
}
|
|
2968
|
+
// END task 17.4
|
|
2969
|
+
|
|
2970
|
+
// --- Task 17.5: Workspace tab ---
|
|
2971
|
+
var workspaceData = null;
|
|
2972
|
+
|
|
2973
|
+
function loadWorkspace() {
|
|
2974
|
+
var view = document.getElementById('workspace-view');
|
|
2975
|
+
view.style.padding = '24px';
|
|
2976
|
+
view.style.maxWidth = '1440px';
|
|
2977
|
+
view.style.margin = '0 auto';
|
|
2978
|
+
view.innerHTML = '<div class="empty-state">Loading workspace...</div>';
|
|
2979
|
+
|
|
2980
|
+
fetch('/api/workspace')
|
|
2981
|
+
.then(function(res) {
|
|
2982
|
+
if (res.status === 404) return null;
|
|
2983
|
+
return res.ok ? res.json() : null;
|
|
2984
|
+
})
|
|
2985
|
+
.then(function(data) {
|
|
2986
|
+
workspaceData = data;
|
|
2987
|
+
if (!data || !data.projects || data.projects.length === 0) {
|
|
2988
|
+
view.innerHTML = '<div class="empty-state">No workspace configuration found</div>';
|
|
2989
|
+
return;
|
|
2990
|
+
}
|
|
2991
|
+
renderWorkspaceList(data);
|
|
2992
|
+
})
|
|
2993
|
+
.catch(function() {
|
|
2994
|
+
view.innerHTML = '<div class="empty-state">Failed to load workspace</div>';
|
|
2995
|
+
});
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
function renderWorkspaceList(data) {
|
|
2999
|
+
var view = document.getElementById('workspace-view');
|
|
3000
|
+
var projects = data.projects || [];
|
|
3001
|
+
var html = '<div class="ws-project-list">';
|
|
3002
|
+
for (var i = 0; i < projects.length; i++) {
|
|
3003
|
+
var p = projects[i];
|
|
3004
|
+
var artifactCount = 0;
|
|
3005
|
+
if (p.artifacts && p.artifacts.include) artifactCount = p.artifacts.include.length;
|
|
3006
|
+
html += '<div class="ws-project-card" data-project="' + escapeHtmlJs(p.name) + '">' +
|
|
3007
|
+
'<div class="ws-project-name">' + escapeHtmlJs(p.name) + '</div>' +
|
|
3008
|
+
'<div class="ws-project-root">' + escapeHtmlJs(p.root) + '</div>' +
|
|
3009
|
+
'<div class="ws-project-meta">' +
|
|
3010
|
+
'<span>' + (p.harnesses || []).map(function(h) { return (HARNESS_ICONS[h] || '') + ' ' + h; }).join(', ') + '</span>' +
|
|
3011
|
+
'<span>' + artifactCount + ' artifact(s)</span>' +
|
|
3012
|
+
'</div></div>';
|
|
3013
|
+
}
|
|
3014
|
+
html += '</div>';
|
|
3015
|
+
view.innerHTML = html;
|
|
3016
|
+
|
|
3017
|
+
var cards = view.querySelectorAll('.ws-project-card');
|
|
3018
|
+
for (var i = 0; i < cards.length; i++) {
|
|
3019
|
+
cards[i].addEventListener('click', function() {
|
|
3020
|
+
showWorkspaceDetail(this.getAttribute('data-project'));
|
|
3021
|
+
});
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
function showWorkspaceDetail(projectName) {
|
|
3026
|
+
if (!workspaceData || !workspaceData.projects) return;
|
|
3027
|
+
var project = null;
|
|
3028
|
+
for (var i = 0; i < workspaceData.projects.length; i++) {
|
|
3029
|
+
if (workspaceData.projects[i].name === projectName) { project = workspaceData.projects[i]; break; }
|
|
3030
|
+
}
|
|
3031
|
+
if (!project) return;
|
|
3032
|
+
|
|
3033
|
+
var view = document.getElementById('workspace-view');
|
|
3034
|
+
var html = '<div style="max-width:860px;margin:0 auto">' +
|
|
3035
|
+
'<div class="detail-back" id="ws-back-link"><span class="detail-back-arrow">\u2190</span> Workspace</div>' +
|
|
3036
|
+
'<h2 style="font-family:var(--font-display);font-size:1.5rem;font-weight:700;margin-bottom:4px">' + escapeHtmlJs(project.name) + '</h2>' +
|
|
3037
|
+
'<div style="font-family:var(--font-mono);font-size:0.8rem;color:var(--color-text-muted);margin-bottom:16px">' + escapeHtmlJs(project.root) + '</div>' +
|
|
3038
|
+
'<div class="detail-section-label">Harnesses</div>' +
|
|
3039
|
+
'<div style="margin-bottom:16px">' + (project.harnesses || []).map(function(h) {
|
|
3040
|
+
return '<span class="badge badge-maturity-stable" style="margin-right:4px">' + (HARNESS_ICONS[h] || '') + ' ' + escapeHtmlJs(h) + '</span>';
|
|
3041
|
+
}).join('') + '</div>';
|
|
3042
|
+
|
|
3043
|
+
if (project.artifacts) {
|
|
3044
|
+
html += '<div class="detail-section-label">Artifacts</div><div style="margin-bottom:16px;font-size:0.825rem">';
|
|
3045
|
+
if (project.artifacts.include && project.artifacts.include.length > 0) {
|
|
3046
|
+
html += '<div style="margin-bottom:4px"><strong>Include:</strong> ' + project.artifacts.include.map(function(a) { return escapeHtmlJs(a); }).join(', ') + '</div>';
|
|
3047
|
+
}
|
|
3048
|
+
if (project.artifacts.exclude && project.artifacts.exclude.length > 0) {
|
|
3049
|
+
html += '<div><strong>Exclude:</strong> ' + project.artifacts.exclude.map(function(a) { return escapeHtmlJs(a); }).join(', ') + '</div>';
|
|
3050
|
+
}
|
|
3051
|
+
html += '</div>';
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
if (project.overrides && Object.keys(project.overrides).length > 0) {
|
|
3055
|
+
html += '<div class="detail-section-label">Overrides</div><div class="detail-content"><pre>' + escapeHtmlJs(JSON.stringify(project.overrides, null, 2)) + '</pre></div>';
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
html += '<div style="margin-top:20px"><button id="ws-edit-btn" style="font-size:0.78rem;padding:5px 12px;border:1px solid var(--color-border);border-radius:var(--radius-md);background:var(--color-surface);cursor:pointer">Edit Project</button></div>';
|
|
3059
|
+
html += '<div id="ws-edit-errors" style="margin-top:8px"></div>';
|
|
3060
|
+
html += '</div>';
|
|
3061
|
+
view.innerHTML = html;
|
|
3062
|
+
|
|
3063
|
+
document.getElementById('ws-back-link').addEventListener('click', function() {
|
|
3064
|
+
renderWorkspaceList(workspaceData);
|
|
3065
|
+
});
|
|
3066
|
+
|
|
3067
|
+
document.getElementById('ws-edit-btn').addEventListener('click', function() {
|
|
3068
|
+
showWorkspaceEditForm(project);
|
|
3069
|
+
});
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
function showWorkspaceEditForm(project) {
|
|
3073
|
+
var view = document.getElementById('workspace-view');
|
|
3074
|
+
var html = '<div style="max-width:640px;margin:0 auto;padding:32px 40px">' +
|
|
3075
|
+
'<div class="detail-back" id="ws-edit-back"><span class="detail-back-arrow">\u2190</span> Back</div>' +
|
|
3076
|
+
'<h2 style="font-family:var(--font-display);font-size:1.25rem;font-weight:700;margin-bottom:16px">Edit: ' + escapeHtmlJs(project.name) + '</h2>' +
|
|
3077
|
+
'<div class="form-group"><label class="form-label">Harnesses</label>' +
|
|
3078
|
+
'<div class="form-checkbox-group">' + ALL_HARNESSES.map(function(h) {
|
|
3079
|
+
var checked = (project.harnesses || []).indexOf(h) !== -1 ? ' checked' : '';
|
|
3080
|
+
return '<label><input type="checkbox" name="ws-harness" value="' + escapeHtmlJs(h) + '"' + checked + '> ' + escapeHtmlJs(h) + '</label>';
|
|
3081
|
+
}).join('') + '</div></div>' +
|
|
3082
|
+
'<div class="form-group"><label class="form-label">Include Artifacts (comma-separated)</label>' +
|
|
3083
|
+
'<input class="form-input" id="ws-include" value="' + escapeHtmlJs((project.artifacts && project.artifacts.include || []).join(', ')) + '"></div>' +
|
|
3084
|
+
'<div class="form-group"><label class="form-label">Exclude Artifacts (comma-separated)</label>' +
|
|
3085
|
+
'<input class="form-input" id="ws-exclude" value="' + escapeHtmlJs((project.artifacts && project.artifacts.exclude || []).join(', ')) + '"></div>' +
|
|
3086
|
+
'<div class="form-group"><label class="form-label">Overrides (JSON)</label>' +
|
|
3087
|
+
'<textarea class="form-textarea mono" id="ws-overrides">' + escapeHtmlJs(JSON.stringify(project.overrides || {}, null, 2)) + '</textarea></div>' +
|
|
3088
|
+
'<div id="ws-save-errors" style="color:var(--color-error);font-size:0.8rem;margin-bottom:8px"></div>' +
|
|
3089
|
+
'<div class="form-actions"><button type="button" class="btn-cancel" id="ws-cancel-btn">Cancel</button>' +
|
|
3090
|
+
'<button type="button" class="btn-submit" id="ws-save-btn">Save</button></div></div>';
|
|
3091
|
+
view.innerHTML = html;
|
|
3092
|
+
|
|
3093
|
+
document.getElementById('ws-edit-back').addEventListener('click', function() { showWorkspaceDetail(project.name); });
|
|
3094
|
+
document.getElementById('ws-cancel-btn').addEventListener('click', function() { showWorkspaceDetail(project.name); });
|
|
3095
|
+
document.getElementById('ws-save-btn').addEventListener('click', function() {
|
|
3096
|
+
var hCbs = document.querySelectorAll('input[name="ws-harness"]:checked');
|
|
3097
|
+
var harnesses = [];
|
|
3098
|
+
for (var i = 0; i < hCbs.length; i++) harnesses.push(hCbs[i].value);
|
|
3099
|
+
var include = parseCommaSeparated(document.getElementById('ws-include').value);
|
|
3100
|
+
var exclude = parseCommaSeparated(document.getElementById('ws-exclude').value);
|
|
3101
|
+
var overrides = {};
|
|
3102
|
+
try { overrides = JSON.parse(document.getElementById('ws-overrides').value || '{}'); } catch(e) {
|
|
3103
|
+
document.getElementById('ws-save-errors').textContent = 'Invalid JSON in overrides';
|
|
3104
|
+
return;
|
|
3105
|
+
}
|
|
3106
|
+
var body = { harnesses: harnesses, artifacts: { include: include, exclude: exclude }, overrides: overrides };
|
|
3107
|
+
fetch('/api/workspace/projects/' + encodeURIComponent(project.name), {
|
|
3108
|
+
method: 'PUT',
|
|
3109
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3110
|
+
body: JSON.stringify(body)
|
|
3111
|
+
})
|
|
3112
|
+
.then(function(res) {
|
|
3113
|
+
if (res.ok) return res.json();
|
|
3114
|
+
return res.json().then(function(err) { throw err; });
|
|
3115
|
+
})
|
|
3116
|
+
.then(function() {
|
|
3117
|
+
showToast('Project updated', 'success');
|
|
3118
|
+
loadWorkspace();
|
|
3119
|
+
})
|
|
3120
|
+
.catch(function(err) {
|
|
3121
|
+
var msg = (err && err.error) || 'Save failed';
|
|
3122
|
+
document.getElementById('ws-save-errors').textContent = msg;
|
|
3123
|
+
showToast(msg, 'error');
|
|
3124
|
+
});
|
|
3125
|
+
});
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
// Check workspace on init to show/hide tab
|
|
3129
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
3130
|
+
fetch('/api/workspace')
|
|
3131
|
+
.then(function(res) {
|
|
3132
|
+
if (res.ok) {
|
|
3133
|
+
return res.json().then(function(data) {
|
|
3134
|
+
if (data && data.projects && data.projects.length > 0) {
|
|
3135
|
+
document.getElementById('workspace-tab').style.display = '';
|
|
3136
|
+
}
|
|
3137
|
+
});
|
|
3138
|
+
}
|
|
3139
|
+
})
|
|
3140
|
+
.catch(function() {});
|
|
3141
|
+
});
|
|
3142
|
+
// END task 17.5
|
|
3143
|
+
|
|
3144
|
+
// --- Task 18.1: Dependency graph SVG renderer ---
|
|
3145
|
+
var graphData = null;
|
|
3146
|
+
|
|
3147
|
+
function loadGraph() {
|
|
3148
|
+
var view = document.getElementById('graph-view');
|
|
3149
|
+
view.innerHTML = '<div class="dep-graph-container"><div class="empty-state">Loading graph...</div></div>';
|
|
3150
|
+
|
|
3151
|
+
fetch('/api/graph')
|
|
3152
|
+
.then(function(res) { return res.ok ? res.json() : { nodes: [], edges: [] }; })
|
|
3153
|
+
.then(function(data) {
|
|
3154
|
+
graphData = data;
|
|
3155
|
+
renderGraph(data);
|
|
3156
|
+
})
|
|
3157
|
+
.catch(function() {
|
|
3158
|
+
view.innerHTML = '<div class="dep-graph-container"><div class="empty-state">Failed to load graph</div></div>';
|
|
3159
|
+
});
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
var TYPE_COLORS = {
|
|
3163
|
+
skill: '#6366f1',
|
|
3164
|
+
power: '#10b981',
|
|
3165
|
+
rule: '#f59e0b',
|
|
3166
|
+
workflow: '#ec4899',
|
|
3167
|
+
prompt: '#8b5cf6',
|
|
3168
|
+
agent: '#06b6d4',
|
|
3169
|
+
template: '#84cc16',
|
|
3170
|
+
reference: '#64748b'
|
|
3171
|
+
};
|
|
3172
|
+
|
|
3173
|
+
function renderGraph(data) {
|
|
3174
|
+
var view = document.getElementById('graph-view');
|
|
3175
|
+
var nodes = data.nodes || [];
|
|
3176
|
+
var edges = data.edges || [];
|
|
3177
|
+
|
|
3178
|
+
if (edges.length === 0) {
|
|
3179
|
+
view.innerHTML = '<div class="dep-graph-container"><div class="empty-state">No dependency relationships found. Add <code>depends</code> or <code>enhances</code> fields to artifact frontmatter.</div></div>';
|
|
3180
|
+
return;
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
// Collect types for filter
|
|
3184
|
+
var types = {};
|
|
3185
|
+
nodes.forEach(function(n) { if (n.type) types[n.type] = true; });
|
|
3186
|
+
var typeList = Object.keys(types).sort();
|
|
3187
|
+
|
|
3188
|
+
var html = '<div class="dep-graph-container">' +
|
|
3189
|
+
'<div class="dep-graph-controls">' +
|
|
3190
|
+
'<select id="graph-type-filter"><option value="">All types</option>' +
|
|
3191
|
+
typeList.map(function(t) { return '<option value="' + escapeHtmlJs(t) + '">' + escapeHtmlJs(t) + '</option>'; }).join('') +
|
|
3192
|
+
'</select>' +
|
|
3193
|
+
'<span style="font-size:0.75rem;color:var(--color-text-muted)">' + nodes.length + ' nodes, ' + edges.length + ' edges</span>' +
|
|
3194
|
+
'</div>' +
|
|
3195
|
+
'<svg id="graph-svg" class="dep-graph-svg" width="100%" height="500">' +
|
|
3196
|
+
'<defs><marker id="arrow-depends" viewBox="0 0 10 10" refX="20" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#6366f1"/></marker>' +
|
|
3197
|
+
'<marker id="arrow-enhances" viewBox="0 0 10 10" refX="20" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#10b981"/></marker></defs>' +
|
|
3198
|
+
'<g id="graph-edges"></g><g id="graph-nodes"></g></svg></div>';
|
|
3199
|
+
view.innerHTML = html;
|
|
3200
|
+
|
|
3201
|
+
var svg = document.getElementById('graph-svg');
|
|
3202
|
+
var svgWidth = svg.clientWidth || 800;
|
|
3203
|
+
var svgHeight = 500;
|
|
3204
|
+
|
|
3205
|
+
// Scale SVG height based on node count for better spacing
|
|
3206
|
+
var nodeCount = nodes.length;
|
|
3207
|
+
svgHeight = Math.max(500, Math.min(900, 300 + nodeCount * 40));
|
|
3208
|
+
svg.setAttribute('height', svgHeight);
|
|
3209
|
+
|
|
3210
|
+
// Force-directed layout
|
|
3211
|
+
var nodeMap = {};
|
|
3212
|
+
// Spread nodes in a circle initially to avoid overlap from the start
|
|
3213
|
+
var angleStep = (2 * Math.PI) / Math.max(nodeCount, 1);
|
|
3214
|
+
var initRadius = Math.min(svgWidth, svgHeight) * 0.35;
|
|
3215
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
3216
|
+
var angle = angleStep * i;
|
|
3217
|
+
nodeMap[nodes[i].name] = {
|
|
3218
|
+
x: svgWidth / 2 + Math.cos(angle) * initRadius + (Math.random() - 0.5) * 20,
|
|
3219
|
+
y: svgHeight / 2 + Math.sin(angle) * initRadius + (Math.random() - 0.5) * 20,
|
|
3220
|
+
vx: 0, vy: 0,
|
|
3221
|
+
name: nodes[i].name,
|
|
3222
|
+
displayName: nodes[i].displayName || nodes[i].name,
|
|
3223
|
+
type: nodes[i].type || 'skill'
|
|
3224
|
+
};
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
// Force simulation with tuned parameters
|
|
3228
|
+
var idealEdgeLen = Math.max(150, Math.min(250, svgWidth / Math.max(nodeCount, 2)));
|
|
3229
|
+
var iterations = 200;
|
|
3230
|
+
for (var iter = 0; iter < iterations; iter++) {
|
|
3231
|
+
var alpha = 1 - iter / iterations; // cooling factor
|
|
3232
|
+
var nodeKeys = Object.keys(nodeMap);
|
|
3233
|
+
// Repulsion between all node pairs (Coulomb-like)
|
|
3234
|
+
for (var i = 0; i < nodeKeys.length; i++) {
|
|
3235
|
+
for (var j = i + 1; j < nodeKeys.length; j++) {
|
|
3236
|
+
var a = nodeMap[nodeKeys[i]], b = nodeMap[nodeKeys[j]];
|
|
3237
|
+
var dx = b.x - a.x, dy = b.y - a.y;
|
|
3238
|
+
var dist = Math.sqrt(dx * dx + dy * dy);
|
|
3239
|
+
if (dist < 1) { dx = (Math.random() - 0.5) * 2; dy = (Math.random() - 0.5) * 2; dist = 1; }
|
|
3240
|
+
var repulsion = 30000 / (dist * dist);
|
|
3241
|
+
var fx = (dx / dist) * repulsion * alpha;
|
|
3242
|
+
var fy = (dy / dist) * repulsion * alpha;
|
|
3243
|
+
a.vx -= fx; a.vy -= fy;
|
|
3244
|
+
b.vx += fx; b.vy += fy;
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
// Attraction along edges (Hooke-like spring)
|
|
3248
|
+
for (var i = 0; i < edges.length; i++) {
|
|
3249
|
+
var src = nodeMap[edges[i].source], tgt = nodeMap[edges[i].target];
|
|
3250
|
+
if (!src || !tgt) continue;
|
|
3251
|
+
var dx = tgt.x - src.x, dy = tgt.y - src.y;
|
|
3252
|
+
var dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
3253
|
+
var force = (dist - idealEdgeLen) * 0.005 * alpha;
|
|
3254
|
+
var fx = (dx / dist) * force, fy = (dy / dist) * force;
|
|
3255
|
+
src.vx += fx; src.vy += fy;
|
|
3256
|
+
tgt.vx -= fx; tgt.vy -= fy;
|
|
3257
|
+
}
|
|
3258
|
+
// Gentle center gravity
|
|
3259
|
+
for (var i = 0; i < nodeKeys.length; i++) {
|
|
3260
|
+
var n = nodeMap[nodeKeys[i]];
|
|
3261
|
+
n.vx += (svgWidth / 2 - n.x) * 0.002 * alpha;
|
|
3262
|
+
n.vy += (svgHeight / 2 - n.y) * 0.002 * alpha;
|
|
3263
|
+
// Velocity damping
|
|
3264
|
+
n.vx *= 0.7; n.vy *= 0.7;
|
|
3265
|
+
n.x += n.vx; n.y += n.vy;
|
|
3266
|
+
// Keep nodes within bounds with padding for labels
|
|
3267
|
+
n.x = Math.max(50, Math.min(svgWidth - 50, n.x));
|
|
3268
|
+
n.y = Math.max(40, Math.min(svgHeight - 40, n.y));
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
drawGraph(nodes, edges, nodeMap);
|
|
3273
|
+
|
|
3274
|
+
// Wire filter
|
|
3275
|
+
document.getElementById('graph-type-filter').addEventListener('change', function() {
|
|
3276
|
+
var filterType = this.value;
|
|
3277
|
+
var filteredNodes = filterType ? nodes.filter(function(n) { return n.type === filterType; }) : nodes;
|
|
3278
|
+
var filteredNames = {};
|
|
3279
|
+
filteredNodes.forEach(function(n) { filteredNames[n.name] = true; });
|
|
3280
|
+
var filteredEdges = edges.filter(function(e) { return filteredNames[e.source] || filteredNames[e.target]; });
|
|
3281
|
+
drawGraph(filteredNodes, filteredEdges, nodeMap);
|
|
3282
|
+
});
|
|
3283
|
+
|
|
3284
|
+
// Pan and zoom (task 18.2)
|
|
3285
|
+
var viewBox = { x: -20, y: -10, w: svgWidth + 40, h: svgHeight + 20 };
|
|
3286
|
+
var isPanning = false, panStart = { x: 0, y: 0 };
|
|
3287
|
+
|
|
3288
|
+
svg.setAttribute('viewBox', viewBox.x + ' ' + viewBox.y + ' ' + viewBox.w + ' ' + viewBox.h);
|
|
3289
|
+
|
|
3290
|
+
svg.addEventListener('mousedown', function(e) {
|
|
3291
|
+
if (e.target.tagName === 'circle' || e.target.tagName === 'text') return;
|
|
3292
|
+
isPanning = true;
|
|
3293
|
+
panStart = { x: e.clientX, y: e.clientY };
|
|
3294
|
+
});
|
|
3295
|
+
svg.addEventListener('mousemove', function(e) {
|
|
3296
|
+
if (!isPanning) return;
|
|
3297
|
+
var dx = (e.clientX - panStart.x) * (viewBox.w / svgWidth);
|
|
3298
|
+
var dy = (e.clientY - panStart.y) * (viewBox.h / svgHeight);
|
|
3299
|
+
viewBox.x -= dx; viewBox.y -= dy;
|
|
3300
|
+
panStart = { x: e.clientX, y: e.clientY };
|
|
3301
|
+
svg.setAttribute('viewBox', viewBox.x + ' ' + viewBox.y + ' ' + viewBox.w + ' ' + viewBox.h);
|
|
3302
|
+
});
|
|
3303
|
+
svg.addEventListener('mouseup', function() { isPanning = false; });
|
|
3304
|
+
svg.addEventListener('mouseleave', function() { isPanning = false; });
|
|
3305
|
+
svg.addEventListener('wheel', function(e) {
|
|
3306
|
+
e.preventDefault();
|
|
3307
|
+
var scale = e.deltaY > 0 ? 1.1 : 0.9;
|
|
3308
|
+
var mx = viewBox.x + viewBox.w * (e.offsetX / svgWidth);
|
|
3309
|
+
var my = viewBox.y + viewBox.h * (e.offsetY / svgHeight);
|
|
3310
|
+
viewBox.w *= scale; viewBox.h *= scale;
|
|
3311
|
+
viewBox.x = mx - viewBox.w * (e.offsetX / svgWidth);
|
|
3312
|
+
viewBox.y = my - viewBox.h * (e.offsetY / svgHeight);
|
|
3313
|
+
svg.setAttribute('viewBox', viewBox.x + ' ' + viewBox.y + ' ' + viewBox.w + ' ' + viewBox.h);
|
|
3314
|
+
});
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
function drawGraph(nodes, edges, nodeMap) {
|
|
3318
|
+
var edgesG = document.getElementById('graph-edges');
|
|
3319
|
+
var nodesG = document.getElementById('graph-nodes');
|
|
3320
|
+
edgesG.innerHTML = '';
|
|
3321
|
+
nodesG.innerHTML = '';
|
|
3322
|
+
|
|
3323
|
+
// Draw edges
|
|
3324
|
+
for (var i = 0; i < edges.length; i++) {
|
|
3325
|
+
var e = edges[i];
|
|
3326
|
+
var src = nodeMap[e.source], tgt = nodeMap[e.target];
|
|
3327
|
+
if (!src || !tgt) continue;
|
|
3328
|
+
var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
3329
|
+
line.setAttribute('x1', src.x); line.setAttribute('y1', src.y);
|
|
3330
|
+
line.setAttribute('x2', tgt.x); line.setAttribute('y2', tgt.y);
|
|
3331
|
+
line.setAttribute('data-source', e.source);
|
|
3332
|
+
line.setAttribute('data-target', e.target);
|
|
3333
|
+
if (e.type === 'enhances') {
|
|
3334
|
+
line.setAttribute('stroke', '#10b981');
|
|
3335
|
+
line.setAttribute('stroke-dasharray', '6,3');
|
|
3336
|
+
line.setAttribute('marker-end', 'url(#arrow-enhances)');
|
|
3337
|
+
} else {
|
|
3338
|
+
line.setAttribute('stroke', '#6366f1');
|
|
3339
|
+
line.setAttribute('marker-end', 'url(#arrow-depends)');
|
|
3340
|
+
}
|
|
3341
|
+
line.setAttribute('stroke-width', '1.5');
|
|
3342
|
+
edgesG.appendChild(line);
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
// Draw nodes
|
|
3346
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
3347
|
+
var n = nodeMap[nodes[i].name];
|
|
3348
|
+
if (!n) continue;
|
|
3349
|
+
var color = TYPE_COLORS[n.type] || '#64748b';
|
|
3350
|
+
var circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
3351
|
+
circle.setAttribute('cx', n.x); circle.setAttribute('cy', n.y);
|
|
3352
|
+
circle.setAttribute('r', '12');
|
|
3353
|
+
circle.setAttribute('fill', color);
|
|
3354
|
+
circle.setAttribute('stroke', '#fff');
|
|
3355
|
+
circle.setAttribute('stroke-width', '2');
|
|
3356
|
+
circle.setAttribute('data-name', n.name);
|
|
3357
|
+
// Click to navigate (task 18.2)
|
|
3358
|
+
circle.addEventListener('click', function() {
|
|
3359
|
+
var name = this.getAttribute('data-name');
|
|
3360
|
+
showView('artifacts');
|
|
3361
|
+
showDetailView(name);
|
|
3362
|
+
});
|
|
3363
|
+
// Hover to highlight (task 18.2)
|
|
3364
|
+
circle.addEventListener('mouseenter', function() {
|
|
3365
|
+
highlightNode(this.getAttribute('data-name'));
|
|
3366
|
+
});
|
|
3367
|
+
circle.addEventListener('mouseleave', function() {
|
|
3368
|
+
clearHighlight();
|
|
3369
|
+
});
|
|
3370
|
+
nodesG.appendChild(circle);
|
|
3371
|
+
|
|
3372
|
+
var text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
3373
|
+
text.setAttribute('x', n.x); text.setAttribute('y', n.y + 22);
|
|
3374
|
+
text.setAttribute('text-anchor', 'middle');
|
|
3375
|
+
text.textContent = n.displayName;
|
|
3376
|
+
nodesG.appendChild(text);
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
// --- Task 18.2: Dependency graph interactions ---
|
|
3381
|
+
function highlightNode(name) {
|
|
3382
|
+
var svg = document.getElementById('graph-svg');
|
|
3383
|
+
if (!svg) return;
|
|
3384
|
+
var connected = {};
|
|
3385
|
+
connected[name] = true;
|
|
3386
|
+
var lines = svg.querySelectorAll('line');
|
|
3387
|
+
for (var i = 0; i < lines.length; i++) {
|
|
3388
|
+
var src = lines[i].getAttribute('data-source');
|
|
3389
|
+
var tgt = lines[i].getAttribute('data-target');
|
|
3390
|
+
if (src === name || tgt === name) {
|
|
3391
|
+
connected[src] = true;
|
|
3392
|
+
connected[tgt] = true;
|
|
3393
|
+
lines[i].classList.add('highlighted');
|
|
3394
|
+
lines[i].classList.remove('dimmed');
|
|
3395
|
+
} else {
|
|
3396
|
+
lines[i].classList.add('dimmed');
|
|
3397
|
+
lines[i].classList.remove('highlighted');
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
var circles = svg.querySelectorAll('circle');
|
|
3401
|
+
for (var i = 0; i < circles.length; i++) {
|
|
3402
|
+
var n = circles[i].getAttribute('data-name');
|
|
3403
|
+
if (connected[n]) {
|
|
3404
|
+
circles[i].classList.add('highlighted');
|
|
3405
|
+
circles[i].classList.remove('dimmed');
|
|
3406
|
+
} else {
|
|
3407
|
+
circles[i].classList.add('dimmed');
|
|
3408
|
+
circles[i].classList.remove('highlighted');
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3413
|
+
function clearHighlight() {
|
|
3414
|
+
var svg = document.getElementById('graph-svg');
|
|
3415
|
+
if (!svg) return;
|
|
3416
|
+
var els = svg.querySelectorAll('.dimmed, .highlighted');
|
|
3417
|
+
for (var i = 0; i < els.length; i++) {
|
|
3418
|
+
els[i].classList.remove('dimmed', 'highlighted');
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
// END task 18.1, 18.2
|
|
3422
|
+
|
|
3423
|
+
// --- Task 18.3: Artifact-scoped dependency section ---
|
|
3424
|
+
function loadDependencySection(entry) {
|
|
3425
|
+
var section = document.getElementById('detail-dependencies-section');
|
|
3426
|
+
if (!section) return;
|
|
3427
|
+
|
|
3428
|
+
var depends = entry.depends || [];
|
|
3429
|
+
var enhances = entry.enhances || [];
|
|
3430
|
+
|
|
3431
|
+
// Find dependents (artifacts that depend on this one)
|
|
3432
|
+
var dependents = [];
|
|
3433
|
+
var enhancedBy = [];
|
|
3434
|
+
for (var i = 0; i < catalogData.length; i++) {
|
|
3435
|
+
var other = catalogData[i];
|
|
3436
|
+
if (other.name === entry.name) continue;
|
|
3437
|
+
if (other.depends && other.depends.indexOf(entry.name) !== -1) dependents.push(other.name);
|
|
3438
|
+
if (other.enhances && other.enhances.indexOf(entry.name) !== -1) enhancedBy.push(other.name);
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
if (depends.length === 0 && enhances.length === 0 && dependents.length === 0 && enhancedBy.length === 0) {
|
|
3442
|
+
section.innerHTML = '';
|
|
3443
|
+
return;
|
|
3444
|
+
}
|
|
3445
|
+
|
|
3446
|
+
var catalogNames = {};
|
|
3447
|
+
for (var i = 0; i < catalogData.length; i++) catalogNames[catalogData[i].name] = true;
|
|
3448
|
+
|
|
3449
|
+
var html = '<div class="detail-section-label">Dependencies</div><div style="margin-bottom:20px;font-size:0.825rem">';
|
|
3450
|
+
|
|
3451
|
+
if (depends.length > 0) {
|
|
3452
|
+
html += '<div style="margin-bottom:8px"><strong>Depends on:</strong> ';
|
|
3453
|
+
html += depends.map(function(d) {
|
|
3454
|
+
var missing = !catalogNames[d];
|
|
3455
|
+
var warn = missing ? ' <span style="color:var(--color-error);font-size:0.75rem" title="Not found in catalog">\u26A0</span>' : '';
|
|
3456
|
+
return '<a href="#" class="dep-link" data-name="' + escapeHtmlJs(d) + '" style="color:var(--color-info);text-decoration:none;font-family:var(--font-mono);font-size:0.8rem">' + escapeHtmlJs(d) + '</a>' + warn;
|
|
3457
|
+
}).join(', ');
|
|
3458
|
+
html += '</div>';
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
if (enhances.length > 0) {
|
|
3462
|
+
html += '<div style="margin-bottom:8px"><strong>Enhances:</strong> ';
|
|
3463
|
+
html += enhances.map(function(d) {
|
|
3464
|
+
var missing = !catalogNames[d];
|
|
3465
|
+
var warn = missing ? ' <span style="color:var(--color-error);font-size:0.75rem" title="Not found in catalog">\u26A0</span>' : '';
|
|
3466
|
+
return '<a href="#" class="dep-link" data-name="' + escapeHtmlJs(d) + '" style="color:var(--color-info);text-decoration:none;font-family:var(--font-mono);font-size:0.8rem">' + escapeHtmlJs(d) + '</a>' + warn;
|
|
3467
|
+
}).join(', ');
|
|
3468
|
+
html += '</div>';
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
if (dependents.length > 0) {
|
|
3472
|
+
html += '<div style="margin-bottom:8px"><strong>Depended on by:</strong> ';
|
|
3473
|
+
html += dependents.map(function(d) {
|
|
3474
|
+
return '<a href="#" class="dep-link" data-name="' + escapeHtmlJs(d) + '" style="color:var(--color-info);text-decoration:none;font-family:var(--font-mono);font-size:0.8rem">' + escapeHtmlJs(d) + '</a>';
|
|
3475
|
+
}).join(', ');
|
|
3476
|
+
html += '</div>';
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3479
|
+
if (enhancedBy.length > 0) {
|
|
3480
|
+
html += '<div style="margin-bottom:8px"><strong>Enhanced by:</strong> ';
|
|
3481
|
+
html += enhancedBy.map(function(d) {
|
|
3482
|
+
return '<a href="#" class="dep-link" data-name="' + escapeHtmlJs(d) + '" style="color:var(--color-info);text-decoration:none;font-family:var(--font-mono);font-size:0.8rem">' + escapeHtmlJs(d) + '</a>';
|
|
3483
|
+
}).join(', ');
|
|
3484
|
+
html += '</div>';
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
html += '</div>';
|
|
3488
|
+
section.innerHTML = html;
|
|
3489
|
+
|
|
3490
|
+
// Wire dependency links
|
|
3491
|
+
var links = section.querySelectorAll('.dep-link');
|
|
3492
|
+
for (var i = 0; i < links.length; i++) {
|
|
3493
|
+
links[i].addEventListener('click', function(e) {
|
|
3494
|
+
e.preventDefault();
|
|
3495
|
+
showDetailView(this.getAttribute('data-name'));
|
|
3496
|
+
});
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
// END task 18.3
|
|
3500
|
+
|
|
3501
|
+
// --- Task 19.1: Build dashboard panel ---
|
|
3502
|
+
var buildHistory = [];
|
|
3503
|
+
|
|
3504
|
+
function renderBuildPanel() {
|
|
3505
|
+
var view = document.getElementById('build-view');
|
|
3506
|
+
var savedConfig = loadBuildConfig();
|
|
3507
|
+
|
|
3508
|
+
var html = '<div class="build-panel">' +
|
|
3509
|
+
'<div class="build-config"><h3>Build Configuration</h3>' +
|
|
3510
|
+
'<div class="build-config-row"><div class="build-config-label">Harnesses</div>' +
|
|
3511
|
+
'<div class="form-checkbox-group">' + ALL_HARNESSES.map(function(h) {
|
|
3512
|
+
var checked = savedConfig.harnesses.indexOf(h) !== -1 ? ' checked' : '';
|
|
3513
|
+
return '<label><input type="checkbox" name="build-harness" value="' + escapeHtmlJs(h) + '"' + checked + '> ' + (HARNESS_ICONS[h] || '') + ' ' + escapeHtmlJs(h) + '</label>';
|
|
3514
|
+
}).join('') + '</div></div>' +
|
|
3515
|
+
'<div class="build-config-row"><div class="build-config-label">Artifacts</div>' +
|
|
3516
|
+
'<input class="form-input" id="build-artifacts" placeholder="All artifacts (or comma-separated names)" value="' + escapeHtmlJs((savedConfig.artifacts || []).join(', ')) + '" style="font-size:0.8rem"></div>' +
|
|
3517
|
+
'<div class="build-config-row"><div class="build-config-label">Options</div>' +
|
|
3518
|
+
'<label style="font-size:0.8rem;display:flex;align-items:center;gap:4px;cursor:pointer"><input type="checkbox" id="build-strict"' + (savedConfig.strict ? ' checked' : '') + '> Strict mode (fail on degradation)</label></div>' +
|
|
3519
|
+
'<div style="margin-top:16px"><button class="btn-submit" id="build-trigger-btn" onclick="triggerBuild()">Build</button></div></div>' +
|
|
3520
|
+
'<div id="build-result-container"></div>' +
|
|
3521
|
+
'<div id="build-history-container"></div></div>';
|
|
3522
|
+
view.innerHTML = html;
|
|
3523
|
+
|
|
3524
|
+
renderBuildHistory();
|
|
3525
|
+
loadBuildStatus();
|
|
3526
|
+
}
|
|
3527
|
+
|
|
3528
|
+
function triggerBuild() {
|
|
3529
|
+
var btn = document.getElementById('build-trigger-btn');
|
|
3530
|
+
btn.textContent = 'Building...';
|
|
3531
|
+
btn.disabled = true;
|
|
3532
|
+
|
|
3533
|
+
var hCbs = document.querySelectorAll('input[name="build-harness"]:checked');
|
|
3534
|
+
var harnesses = [];
|
|
3535
|
+
for (var i = 0; i < hCbs.length; i++) harnesses.push(hCbs[i].value);
|
|
3536
|
+
var artifactsStr = (document.getElementById('build-artifacts') || {}).value || '';
|
|
3537
|
+
var artifacts = parseCommaSeparated(artifactsStr);
|
|
3538
|
+
var strict = document.getElementById('build-strict').checked;
|
|
3539
|
+
|
|
3540
|
+
// Save config (task 19.3)
|
|
3541
|
+
saveBuildConfig({ harnesses: harnesses, artifacts: artifacts, strict: strict });
|
|
3542
|
+
|
|
3543
|
+
var body = {};
|
|
3544
|
+
if (harnesses.length > 0 && harnesses.length < ALL_HARNESSES.length) body.harness = harnesses;
|
|
3545
|
+
if (artifacts.length > 0) body.artifacts = artifacts;
|
|
3546
|
+
if (strict) body.strict = true;
|
|
3547
|
+
|
|
3548
|
+
fetch('/api/build', {
|
|
3549
|
+
method: 'POST',
|
|
3550
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3551
|
+
body: JSON.stringify(body)
|
|
3552
|
+
})
|
|
3553
|
+
.then(function(res) { return res.json(); })
|
|
3554
|
+
.then(function(result) {
|
|
3555
|
+
btn.textContent = 'Build';
|
|
3556
|
+
btn.disabled = false;
|
|
3557
|
+
displayBuildResult(result);
|
|
3558
|
+
buildHistory.unshift(result);
|
|
3559
|
+
if (buildHistory.length > 10) buildHistory = buildHistory.slice(0, 10);
|
|
3560
|
+
renderBuildHistory();
|
|
3561
|
+
updateBuildStatusIndicator(result);
|
|
3562
|
+
})
|
|
3563
|
+
.catch(function() {
|
|
3564
|
+
btn.textContent = 'Build';
|
|
3565
|
+
btn.disabled = false;
|
|
3566
|
+
showToast('Build failed', 'error');
|
|
3567
|
+
});
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
function displayBuildResult(result) {
|
|
3571
|
+
var container = document.getElementById('build-result-container');
|
|
3572
|
+
if (!container) return;
|
|
3573
|
+
var status = result.status || (result.errors && result.errors.length > 0 ? 'failure' : 'success');
|
|
3574
|
+
var bannerClass = status === 'success' ? 'success' : 'failure';
|
|
3575
|
+
var html = '<div class="build-result">' +
|
|
3576
|
+
'<div class="build-result-banner ' + bannerClass + '">' +
|
|
3577
|
+
(status === 'success' ? '\u2713 Build succeeded' : '\u2717 Build failed') +
|
|
3578
|
+
' \u2014 ' + (result.artifactsCompiled || 0) + ' artifact(s), ' + (result.filesWritten || 0) + ' file(s)' +
|
|
3579
|
+
'</div>';
|
|
3580
|
+
|
|
3581
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
3582
|
+
html += '<details class="build-warnings"><summary>\u26A0 ' + result.warnings.length + ' warning(s)</summary>';
|
|
3583
|
+
for (var i = 0; i < result.warnings.length; i++) {
|
|
3584
|
+
var w = result.warnings[i];
|
|
3585
|
+
html += '<div class="build-warning-item">';
|
|
3586
|
+
if (w.artifactName) html += '<strong>' + escapeHtmlJs(w.artifactName) + '</strong>';
|
|
3587
|
+
if (w.harnessName) html += ' \u2192 ' + escapeHtmlJs(w.harnessName);
|
|
3588
|
+
html += ': ' + escapeHtmlJs(w.message || '') + '</div>';
|
|
3589
|
+
}
|
|
3590
|
+
html += '</details>';
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
if (result.errors && result.errors.length > 0) {
|
|
3594
|
+
html += '<details class="build-errors" open><summary>\u2717 ' + result.errors.length + ' error(s)</summary>';
|
|
3595
|
+
for (var i = 0; i < result.errors.length; i++) {
|
|
3596
|
+
var e = result.errors[i];
|
|
3597
|
+
html += '<div class="build-error-item">';
|
|
3598
|
+
if (e.artifactName) html += '<strong>' + escapeHtmlJs(e.artifactName) + '</strong>';
|
|
3599
|
+
if (e.harnessName) html += ' \u2192 ' + escapeHtmlJs(e.harnessName);
|
|
3600
|
+
html += ': ' + escapeHtmlJs(e.message || '') + '</div>';
|
|
3601
|
+
}
|
|
3602
|
+
html += '</details>';
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3605
|
+
html += '</div>';
|
|
3606
|
+
container.innerHTML = html;
|
|
3607
|
+
}
|
|
3608
|
+
// END task 19.1
|
|
3609
|
+
|
|
3610
|
+
// --- Task 19.2: Build status indicator and history ---
|
|
3611
|
+
function loadBuildStatus() {
|
|
3612
|
+
fetch('/api/build/status')
|
|
3613
|
+
.then(function(res) { return res.ok ? res.json() : null; })
|
|
3614
|
+
.then(function(data) {
|
|
3615
|
+
if (data) {
|
|
3616
|
+
updateBuildStatusIndicator(data);
|
|
3617
|
+
if (data.history) buildHistory = data.history;
|
|
3618
|
+
renderBuildHistory();
|
|
3619
|
+
}
|
|
3620
|
+
})
|
|
3621
|
+
.catch(function() {});
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
function updateBuildStatusIndicator(result) {
|
|
3625
|
+
var indicator = document.getElementById('build-status-indicator');
|
|
3626
|
+
if (!indicator) return;
|
|
3627
|
+
var status = result.status || (result.errors && result.errors.length > 0 ? 'failure' : 'success');
|
|
3628
|
+
if (status === 'success') {
|
|
3629
|
+
indicator.innerHTML = '<span class="build-status-dot success"></span> Build OK';
|
|
3630
|
+
} else {
|
|
3631
|
+
indicator.innerHTML = '<span class="build-status-dot failure"></span> Build failed';
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
function renderBuildHistory() {
|
|
3636
|
+
var container = document.getElementById('build-history-container');
|
|
3637
|
+
if (!container) return;
|
|
3638
|
+
if (buildHistory.length === 0) {
|
|
3639
|
+
container.innerHTML = '';
|
|
3640
|
+
return;
|
|
3641
|
+
}
|
|
3642
|
+
var html = '<div class="build-history"><h3>Build History</h3>';
|
|
3643
|
+
for (var i = 0; i < buildHistory.length; i++) {
|
|
3644
|
+
var b = buildHistory[i];
|
|
3645
|
+
var status = b.status || (b.errors && b.errors.length > 0 ? 'failure' : 'success');
|
|
3646
|
+
var ts = b.timestamp ? new Date(b.timestamp).toLocaleString() : 'Unknown';
|
|
3647
|
+
var warnCount = (b.warnings || []).length;
|
|
3648
|
+
var errCount = (b.errors || []).length;
|
|
3649
|
+
html += '<div class="build-history-item" data-idx="' + i + '">' +
|
|
3650
|
+
'<span class="build-status-dot ' + status + '"></span>' +
|
|
3651
|
+
'<span>' + escapeHtmlJs(ts) + '</span>' +
|
|
3652
|
+
'<span>' + (b.artifactsCompiled || 0) + ' artifacts</span>' +
|
|
3653
|
+
(warnCount > 0 ? '<span style="color:var(--color-warning)">' + warnCount + ' warn</span>' : '') +
|
|
3654
|
+
(errCount > 0 ? '<span style="color:var(--color-error)">' + errCount + ' err</span>' : '') +
|
|
3655
|
+
'</div>';
|
|
3656
|
+
}
|
|
3657
|
+
html += '</div>';
|
|
3658
|
+
container.innerHTML = html;
|
|
3659
|
+
|
|
3660
|
+
var items = container.querySelectorAll('.build-history-item');
|
|
3661
|
+
for (var i = 0; i < items.length; i++) {
|
|
3662
|
+
items[i].addEventListener('click', function() {
|
|
3663
|
+
var idx = parseInt(this.getAttribute('data-idx'), 10);
|
|
3664
|
+
if (buildHistory[idx]) displayBuildResult(buildHistory[idx]);
|
|
3665
|
+
});
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
// Load build status on init
|
|
3670
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
3671
|
+
fetch('/api/build/status')
|
|
3672
|
+
.then(function(res) { return res.ok ? res.json() : null; })
|
|
3673
|
+
.then(function(data) {
|
|
3674
|
+
if (data) updateBuildStatusIndicator(data);
|
|
3675
|
+
})
|
|
3676
|
+
.catch(function() {});
|
|
3677
|
+
});
|
|
3678
|
+
// END task 19.2
|
|
3679
|
+
|
|
3680
|
+
// --- Task 19.3: Build config localStorage persistence ---
|
|
3681
|
+
function saveBuildConfig(config) {
|
|
3682
|
+
try {
|
|
3683
|
+
localStorage.setItem('forge-build-config', JSON.stringify(config));
|
|
3684
|
+
} catch(e) {}
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
function loadBuildConfig() {
|
|
3688
|
+
try {
|
|
3689
|
+
var stored = localStorage.getItem('forge-build-config');
|
|
3690
|
+
if (stored) return JSON.parse(stored);
|
|
3691
|
+
} catch(e) {}
|
|
3692
|
+
return { harnesses: ALL_HARNESSES.slice(), artifacts: [], strict: false };
|
|
3693
|
+
}
|
|
3694
|
+
// END task 19.3
|
|
3695
|
+
|
|
3696
|
+
// --- Task 11.1: Tab navigation (wired in HTML above) ---
|
|
3697
|
+
// showView() already handles tab switching, filter visibility, and content rendering.
|
|
3698
|
+
// The tab nav HTML and new button are in the header.
|
|
3699
|
+
// END task 11.1
|
|
3700
|
+
|
|
3701
|
+
// END task 5.1
|
|
3702
|
+
</script>`;
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3705
|
+
export function generateHtmlPage(): string {
|
|
3706
|
+
return `<!DOCTYPE html>
|
|
3707
|
+
<html lang="en">
|
|
3708
|
+
<head>
|
|
3709
|
+
<meta charset="utf-8">
|
|
3710
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
3711
|
+
<title>Kanon Catalog</title>
|
|
3712
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
3713
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
3714
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
|
|
3715
|
+
${generateStyles()}
|
|
3716
|
+
</head>
|
|
3717
|
+
<body>
|
|
3718
|
+
${generateMarkup()}
|
|
3719
|
+
${generateClientScript()}
|
|
3720
|
+
</body>
|
|
3721
|
+
</html>`;
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
/**
|
|
3725
|
+
* Safely serializes a value to JSON for embedding in an HTML `<script>` block.
|
|
3726
|
+
* Replaces `</script>` and `<!--` sequences that would confuse the HTML parser.
|
|
3727
|
+
*/
|
|
3728
|
+
function safeJsonEmbed(data: unknown): string {
|
|
3729
|
+
return JSON.stringify(data)
|
|
3730
|
+
.replace(/<\/script>/gi, "<\\/script>")
|
|
3731
|
+
.replace(/<!--/g, "<\\!--");
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3734
|
+
/**
|
|
3735
|
+
* Generates a fully self-contained static HTML page for the catalog browser.
|
|
3736
|
+
*
|
|
3737
|
+
* Catalog entries and artifact `knowledge.md` content are embedded as inline
|
|
3738
|
+
* `window.__CATALOG_DATA__` and `window.__ARTIFACT_CONTENT__` variables so the
|
|
3739
|
+
* page works without a backend server (e.g. GitHub Pages). The client-side JS
|
|
3740
|
+
* in `generateHtmlPage()` checks for those globals before falling back to the
|
|
3741
|
+
* live `/api/*` endpoints, so local `kanon catalog browse` continues to work
|
|
3742
|
+
* from the same HTML template.
|
|
3743
|
+
*/
|
|
3744
|
+
export function generateStaticHtmlPage(
|
|
3745
|
+
entries: CatalogEntry[],
|
|
3746
|
+
contentMap: Record<string, string>,
|
|
3747
|
+
): string {
|
|
3748
|
+
const dataScript = `<script>window.__CATALOG_DATA__ = ${safeJsonEmbed(entries)};
|
|
3749
|
+
window.__ARTIFACT_CONTENT__ = ${safeJsonEmbed(contentMap)};
|
|
3750
|
+
window.__CAPABILITY_MATRIX__ = ${safeJsonEmbed(CAPABILITY_MATRIX)};</script>`;
|
|
3751
|
+
// Use a replacer function instead of a replacement string to avoid
|
|
3752
|
+
// String.replace interpreting $-sequences (e.g. $`, $') in the JSON data.
|
|
3753
|
+
return generateHtmlPage().replace("</head>", () => `${dataScript}\n</head>`);
|
|
3754
|
+
}
|