superintent 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +226 -0
  3. package/bin/superintent.js +2 -0
  4. package/dist/commands/extract.d.ts +2 -0
  5. package/dist/commands/extract.js +66 -0
  6. package/dist/commands/init.d.ts +2 -0
  7. package/dist/commands/init.js +56 -0
  8. package/dist/commands/knowledge.d.ts +2 -0
  9. package/dist/commands/knowledge.js +647 -0
  10. package/dist/commands/search.d.ts +2 -0
  11. package/dist/commands/search.js +153 -0
  12. package/dist/commands/spec.d.ts +2 -0
  13. package/dist/commands/spec.js +283 -0
  14. package/dist/commands/status.d.ts +2 -0
  15. package/dist/commands/status.js +43 -0
  16. package/dist/commands/ticket.d.ts +4 -0
  17. package/dist/commands/ticket.js +942 -0
  18. package/dist/commands/ui.d.ts +2 -0
  19. package/dist/commands/ui.js +954 -0
  20. package/dist/db/client.d.ts +4 -0
  21. package/dist/db/client.js +26 -0
  22. package/dist/db/init-schema.d.ts +2 -0
  23. package/dist/db/init-schema.js +28 -0
  24. package/dist/db/parsers.d.ts +24 -0
  25. package/dist/db/parsers.js +79 -0
  26. package/dist/db/schema.d.ts +7 -0
  27. package/dist/db/schema.js +64 -0
  28. package/dist/db/usage.d.ts +8 -0
  29. package/dist/db/usage.js +24 -0
  30. package/dist/embed/model.d.ts +5 -0
  31. package/dist/embed/model.js +34 -0
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +31 -0
  34. package/dist/types.d.ts +120 -0
  35. package/dist/types.js +1 -0
  36. package/dist/ui/components/index.d.ts +6 -0
  37. package/dist/ui/components/index.js +13 -0
  38. package/dist/ui/components/knowledge.d.ts +33 -0
  39. package/dist/ui/components/knowledge.js +238 -0
  40. package/dist/ui/components/layout.d.ts +1 -0
  41. package/dist/ui/components/layout.js +323 -0
  42. package/dist/ui/components/search.d.ts +15 -0
  43. package/dist/ui/components/search.js +114 -0
  44. package/dist/ui/components/spec.d.ts +11 -0
  45. package/dist/ui/components/spec.js +253 -0
  46. package/dist/ui/components/ticket.d.ts +90 -0
  47. package/dist/ui/components/ticket.js +604 -0
  48. package/dist/ui/components/utils.d.ts +26 -0
  49. package/dist/ui/components/utils.js +34 -0
  50. package/dist/ui/styles.css +2 -0
  51. package/dist/utils/cli.d.ts +21 -0
  52. package/dist/utils/cli.js +31 -0
  53. package/dist/utils/config.d.ts +12 -0
  54. package/dist/utils/config.js +116 -0
  55. package/dist/utils/id.d.ts +6 -0
  56. package/dist/utils/id.js +13 -0
  57. package/dist/utils/io.d.ts +8 -0
  58. package/dist/utils/io.js +15 -0
  59. package/package.json +60 -0
@@ -0,0 +1,238 @@
1
+ // Knowledge-related UI components
2
+ import { escapeHtml } from './utils.js';
3
+ // Helper to render knowledge view
4
+ export function renderKnowledgeView() {
5
+ return `
6
+ <div class="flex flex-col lg:flex-row gap-4 lg:gap-6">
7
+ <aside class="w-full lg:w-64 shrink-0 lg:sticky lg:top-4 lg:self-start">
8
+ <h2 class="text-2xl font-bold text-gray-800 mb-4">Filters</h2>
9
+ <div class="grid grid-cols-2 lg:grid-cols-1 gap-2 lg:gap-4 lg:space-y-0 bg-white rounded-lg shadow-card p-3 lg:p-4">
10
+ <div>
11
+ <label class="block text-sm font-medium text-gray-700 mb-1">Status</label>
12
+ <select name="k-status" class="w-full border rounded-lg px-3 py-2 text-sm bg-white"
13
+ hx-get="/partials/knowledge-list"
14
+ hx-trigger="change"
15
+ hx-target="#knowledge-list"
16
+ hx-include="[name='k-category'],[name='k-namespace'],[name='k-scope'],[name='k-origin']">
17
+ <option value="active">Active</option>
18
+ <option value="inactive">Inactive</option>
19
+ <option value="all">All</option>
20
+ </select>
21
+ </div>
22
+
23
+ <div>
24
+ <label class="block text-sm font-medium text-gray-700 mb-1">Category</label>
25
+ <select name="k-category" class="w-full border rounded-lg px-3 py-2 text-sm bg-white"
26
+ hx-get="/partials/knowledge-list"
27
+ hx-trigger="change"
28
+ hx-target="#knowledge-list"
29
+ hx-include="[name='k-status'],[name='k-namespace'],[name='k-scope'],[name='k-origin']">
30
+ <option value="">All</option>
31
+ <option value="pattern">Pattern</option>
32
+ <option value="truth">Truth</option>
33
+ <option value="principle">Principle</option>
34
+ <option value="architecture">Architecture</option>
35
+ <option value="gotcha">Gotcha</option>
36
+ </select>
37
+ </div>
38
+
39
+ <div>
40
+ <label class="block text-sm font-medium text-gray-700 mb-1">Scope</label>
41
+ <select name="k-scope" class="w-full border rounded-lg px-3 py-2 text-sm bg-white"
42
+ hx-get="/partials/knowledge-list"
43
+ hx-trigger="change"
44
+ hx-target="#knowledge-list"
45
+ hx-include="[name='k-status'],[name='k-category'],[name='k-namespace'],[name='k-origin']">
46
+ <option value="">All</option>
47
+ <option value="global">Global</option>
48
+ <option value="new-only">New Only</option>
49
+ <option value="backward-compatible">Backward Compatible</option>
50
+ <option value="legacy-frozen">Legacy Frozen</option>
51
+ </select>
52
+ </div>
53
+
54
+ <div>
55
+ <label class="block text-sm font-medium text-gray-700 mb-1">Source</label>
56
+ <select name="k-origin" class="w-full border rounded-lg px-3 py-2 text-sm bg-white"
57
+ hx-get="/partials/knowledge-list"
58
+ hx-trigger="change"
59
+ hx-target="#knowledge-list"
60
+ hx-include="[name='k-status'],[name='k-category'],[name='k-namespace'],[name='k-scope']">
61
+ <option value="">All</option>
62
+ <option value="ticket">Ticket</option>
63
+ <option value="discovery">Discovery</option>
64
+ <option value="manual">Manual</option>
65
+ </select>
66
+ </div>
67
+ </div>
68
+ </aside>
69
+
70
+ <main class="flex-1">
71
+ <h1 class="text-2xl font-bold text-gray-800 mb-4">Knowledge Base</h1>
72
+ <div id="knowledge-list" hx-get="/partials/knowledge-list" hx-trigger="load, poll-refresh" hx-swap="innerHTML">
73
+ </div>
74
+ </main>
75
+ </div>
76
+ `;
77
+ }
78
+ // Helper to render knowledge list
79
+ export function renderKnowledgeList(items) {
80
+ if (items.length === 0) {
81
+ return '<p class="text-gray-500 text-center py-8">No knowledge entries found</p>';
82
+ }
83
+ const categoryColors = {
84
+ pattern: 'purple',
85
+ truth: 'green',
86
+ principle: 'orange',
87
+ architecture: 'blue',
88
+ gotcha: 'red',
89
+ };
90
+ return `
91
+ <div class="space-y-3">
92
+ ${items.map(k => {
93
+ const color = categoryColors[k.category || ''] || 'gray';
94
+ const inactiveClass = !k.active ? 'opacity-60 border-dashed' : '';
95
+ return `
96
+ <div class="bg-white rounded-lg shadow-card p-4 hover:shadow-card-hover transition-shadow cursor-pointer ${inactiveClass}"
97
+ hx-get="/partials/knowledge-modal/${encodeURIComponent(k.id)}"
98
+ hx-target="#modal-content"
99
+ hx-trigger="click"
100
+ onclick="showModal()">
101
+ <div class="text-xs font-mono text-gray-400 mb-1">${escapeHtml(k.id)}</div>
102
+ <div class="flex items-start justify-between">
103
+ <div class="flex-1">
104
+ <div class="flex items-center gap-2">
105
+ <div class="text-sm font-medium text-gray-800">${escapeHtml(k.title)}</div>
106
+ ${!k.active ? '<span class="px-2 py-0.5 text-xs rounded bg-gray-200 text-gray-600">Inactive</span>' : ''}
107
+ </div>
108
+ <p class="text-sm text-gray-600 mt-1 line-clamp-2">${escapeHtml(k.content.slice(0, 200))}${k.content.length > 200 ? '...' : ''}</p>
109
+ </div>
110
+ <div class="text-right flex-shrink-0 ml-4">
111
+ <div class="text-sm font-medium text-gray-600">${Math.round(k.confidence * 100)}%</div>
112
+ <div class="text-xs text-gray-400">confidence</div>
113
+ </div>
114
+ </div>
115
+ <div class="flex items-center mt-3 text-xs text-gray-500 gap-3">
116
+ <span><span class="text-gray-400">Namespace:</span> ${escapeHtml(k.namespace)}</span>
117
+ <span><span class="text-gray-400">Source:</span> ${k.source || 'manual'}${k.source === 'ticket' && k.origin_ticket_type ? ` (${k.origin_ticket_type})` : ''}</span>
118
+ ${k.category ? `<span><span class="text-gray-400">Category:</span> <span class="text-${color}-600 font-medium">${k.category}</span></span>` : ''}
119
+ <span><span class="text-gray-400">Scope:</span> ${k.decision_scope}</span>
120
+ </div>
121
+ </div>
122
+ `;
123
+ }).join('')}
124
+ </div>
125
+ `;
126
+ }
127
+ // Helper to render knowledge modal
128
+ export function renderKnowledgeModal(knowledge) {
129
+ const categoryColors = {
130
+ pattern: 'purple',
131
+ truth: 'green',
132
+ principle: 'orange',
133
+ architecture: 'blue',
134
+ gotcha: 'red',
135
+ };
136
+ const color = categoryColors[knowledge.category || ''] || 'gray';
137
+ return `
138
+ <div class="p-6">
139
+ <div class="flex items-start justify-between mb-4">
140
+ <div>
141
+ <div class="flex items-center gap-2 mb-1">
142
+ <span class="text-xs font-mono text-gray-400">${escapeHtml(knowledge.id)}</span>
143
+ <button type="button"
144
+ class="p-0.5 text-gray-400 hover:text-blue-600 rounded transition-colors"
145
+ title="Copy knowledge ID"
146
+ onclick="navigator.clipboard.writeText('${escapeHtml(knowledge.id)}').then(() => { const svg = this.querySelector('svg'); const originalPath = svg.innerHTML; svg.innerHTML = '<path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M5 13l4 4L19 7&quot;></path>'; this.classList.add('text-green-600'); setTimeout(() => { svg.innerHTML = originalPath; this.classList.remove('text-green-600'); }, 1500); })">
147
+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
148
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
149
+ </svg>
150
+ </button>
151
+ <span class="px-2 py-0.5 text-xs font-medium rounded ${knowledge.active ? 'bg-green-100 text-green-700' : 'bg-gray-200 text-gray-600'}">${knowledge.active ? 'Active' : 'Inactive'}</span>
152
+ ${knowledge.category ? `<span class="px-2 py-0.5 text-xs font-medium rounded bg-${color}-100 text-${color}-700">${knowledge.category}</span>` : ''}
153
+ </div>
154
+ <h2 class="text-xl font-bold text-gray-800">${escapeHtml(knowledge.title)}</h2>
155
+ </div>
156
+ <button onclick="hideModal()" class="text-gray-400 hover:text-gray-600 p-1">
157
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
158
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
159
+ </svg>
160
+ </button>
161
+ </div>
162
+
163
+ <!-- Confidence & Usage Stats -->
164
+ <div class="mb-4 grid grid-cols-2 gap-4">
165
+ <div class="bg-gray-50 rounded-lg p-3">
166
+ <div class="text-xs text-gray-500 mb-1">Confidence</div>
167
+ <div class="text-xl font-semibold text-${color}-600 mb-2">${Math.round(knowledge.confidence * 100)}%</div>
168
+ <div class="w-full bg-gray-200 rounded-full h-1.5">
169
+ <div class="bg-${color}-500 h-1.5 rounded-full transition-all" style="width: ${Math.round(knowledge.confidence * 100)}%"></div>
170
+ </div>
171
+ </div>
172
+ <div class="bg-gray-50 rounded-lg p-3">
173
+ <div class="text-xs text-gray-500 mb-1">Usage</div>
174
+ <div class="text-xl font-semibold text-gray-700">${knowledge.usage_count || 0} <span class="text-sm font-normal text-gray-500">times</span></div>
175
+ <div class="text-xs text-gray-400 mt-1">${knowledge.last_used_at ? 'Last used ' + knowledge.last_used_at.split('T')[0] : 'Never used'}</div>
176
+ </div>
177
+ </div>
178
+
179
+ <!-- Content -->
180
+ <div class="mb-4">
181
+ <h3 class="text-sm font-semibold text-gray-700 mb-2">Content</h3>
182
+ <div class="text-sm text-gray-700 bg-gray-50 rounded-lg p-4 leading-relaxed markdown-content" data-markdown>${escapeHtml(knowledge.content)}</div>
183
+ </div>
184
+
185
+ <!-- Metadata -->
186
+ <div class="mb-4">
187
+ <h3 class="text-sm font-semibold text-gray-700 mb-2">Metadata</h3>
188
+ <div class="text-sm text-gray-600 space-y-1">
189
+ <div><span class="text-gray-400">Namespace:</span> ${escapeHtml(knowledge.namespace)}</div>
190
+ <div><span class="text-gray-400">Scope:</span> ${knowledge.decision_scope}</div>
191
+ <div><span class="text-gray-400">Source:</span> ${knowledge.source}${knowledge.source === 'ticket' && knowledge.origin_ticket_type ? ` (${knowledge.origin_ticket_type})` : ''}</div>
192
+ </div>
193
+ </div>
194
+
195
+ ${knowledge.origin_ticket_id ? `
196
+ <div class="mb-4">
197
+ <h3 class="text-sm font-semibold text-gray-700 mb-2">Origin Ticket</h3>
198
+ <span class="inline-flex items-center px-2 py-1 text-xs rounded-lg bg-yellow-50 text-yellow-700 hover:bg-yellow-100 cursor-pointer font-mono font-medium"
199
+ hx-get="/partials/ticket-modal/${encodeURIComponent(knowledge.origin_ticket_id)}"
200
+ hx-target="#modal-content"
201
+ hx-trigger="click">
202
+ <svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
203
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
204
+ </svg>
205
+ ${escapeHtml(knowledge.origin_ticket_id)}
206
+ </span>
207
+ </div>
208
+ ` : ''}
209
+
210
+ <!-- Tags -->
211
+ ${knowledge.tags?.length ? `
212
+ <div class="mb-4">
213
+ <h3 class="text-sm font-semibold text-gray-700 mb-2">Tags</h3>
214
+ <div class="flex flex-wrap gap-2">
215
+ ${knowledge.tags.map(t => `<span class="px-3 py-1 text-sm rounded-full bg-gray-100 text-gray-700">${escapeHtml(t)}</span>`).join('')}
216
+ </div>
217
+ </div>
218
+ ` : ''}
219
+
220
+ <!-- Actions & Metadata Footer -->
221
+ <div class="mt-6 pt-4 border-t flex items-center justify-between">
222
+ <div class="text-xs text-gray-400">
223
+ <span>Created: ${knowledge.created_at || 'N/A'}</span>
224
+ <span class="ml-4">Updated: ${knowledge.updated_at || 'N/A'}</span>
225
+ </div>
226
+ <button type="button"
227
+ class="px-3 py-1.5 text-xs font-medium ${knowledge.active ? 'text-gray-700 bg-gray-100 hover:bg-gray-200' : 'text-green-700 bg-green-50 hover:bg-green-100'} rounded transition-colors"
228
+ hx-patch="/api/knowledge/${knowledge.id}/active"
229
+ hx-vals='{"active": ${!knowledge.active}}'
230
+ hx-target="#modal-content"
231
+ hx-swap="innerHTML"
232
+ hx-on::after-request="htmx.trigger('#knowledge-list', 'poll-refresh')">
233
+ ${knowledge.active ? 'Deactivate' : 'Activate'}
234
+ </button>
235
+ </div>
236
+ </div>
237
+ `;
238
+ }
@@ -0,0 +1 @@
1
+ export declare function getHtml(namespace: string): string;
@@ -0,0 +1,323 @@
1
+ // Main layout component for Superintent Web UI
2
+ import { escapeHtml } from './utils.js';
3
+ // Main HTML shell with navigation, tabs, and JavaScript
4
+ export function getHtml(namespace) {
5
+ return `<!DOCTYPE html>
6
+ <html lang="en">
7
+ <head>
8
+ <meta charset="UTF-8">
9
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
10
+ <title>Superintent</title>
11
+ <link rel="stylesheet" href="/styles.css">
12
+ <script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
13
+ <script src="https://cdn.jsdelivr.net/npm/marked@17.0.1/lib/marked.umd.js"></script>
14
+ <script src="https://cdn.jsdelivr.net/npm/dompurify@3.3.1/dist/purify.min.js"></script>
15
+ <style>
16
+ .drag-over { outline: 2px dashed var(--color-blue-500); background: var(--color-blue-50); }
17
+ .dragging { opacity: 0.5; }
18
+ .htmx-indicator { display: none; }
19
+ .htmx-request .htmx-indicator { display: inline-block; }
20
+ .htmx-request.htmx-indicator { display: inline-block; }
21
+ .tab-active { color: var(--color-blue-700); }
22
+ .score-bar { height: 4px; border-radius: 2px; }
23
+ .modal-content { max-height: 85vh; min-height: 200px; }
24
+ #modal { opacity: 0; transition: opacity 150ms ease-out; }
25
+ #modal.show { opacity: 1; }
26
+ #modal-content { transform: scale(0.95); transition: transform 150ms ease-out; }
27
+ #modal.show #modal-content { transform: scale(1); }
28
+ /* Markdown prose styles */
29
+ .markdown-content h1, .markdown-content h2, .markdown-content h3 { font-weight: 600; margin-top: 1em; margin-bottom: 0.5em; }
30
+ .markdown-content h1 { font-size: 1.25em; }
31
+ .markdown-content h2 { font-size: 1.1em; }
32
+ .markdown-content h3 { font-size: 1em; }
33
+ .markdown-content p { margin-bottom: 0.75em; }
34
+ .markdown-content ul, .markdown-content ol { margin-left: 1.5em; margin-bottom: 0.75em; }
35
+ .markdown-content ul { list-style-type: disc; }
36
+ .markdown-content ol { list-style-type: decimal; }
37
+ .markdown-content li { margin-bottom: 0.25em; }
38
+ .markdown-content code { background: var(--color-gray-200); padding: 0.125em 0.25em; border-radius: 0.25em; font-size: 0.9em; }
39
+ .markdown-content pre { background: var(--color-gray-800); color: var(--color-gray-100); padding: 0.75em; border-radius: 0.375em; overflow-x: auto; margin-bottom: 0.75em; }
40
+ .markdown-content pre code { background: none; padding: 0; color: inherit; }
41
+ .markdown-content strong { font-weight: 600; }
42
+ .markdown-content a { color: var(--color-blue-600); text-decoration: underline; }
43
+ .markdown-content blockquote { border-left: 3px solid var(--color-gray-300); padding-left: 1em; color: var(--color-gray-500); margin-bottom: 0.75em; }
44
+ .markdown-content li:has(> input[type="checkbox"]) { list-style: none; display: flex; align-items: baseline; gap: 0.4em; margin-left: -1.5em; }
45
+ .markdown-content li > input[type="checkbox"] { margin: 0; pointer-events: none; }
46
+ </style>
47
+ </head>
48
+ <body class="bg-gray-100 min-h-screen">
49
+ <!-- Navigation -->
50
+ <nav class="bg-white shadow-sm">
51
+ <div class="max-w-7xl mx-auto px-4">
52
+ <div class="flex items-center justify-between h-14">
53
+ <span class="text-sm sm:text-xl font-bold text-gray-800">Superintent</span>
54
+ <div class="flex">
55
+ <button id="tab-ticket" onclick="switchTab('ticket')"
56
+ class="px-2 sm:px-4 py-4 text-xs sm:text-sm font-medium text-gray-600 hover:text-gray-900 cursor-pointer tab-active">
57
+ Ticket
58
+ </button>
59
+ <button id="tab-search" onclick="switchTab('search')"
60
+ class="px-2 sm:px-4 py-4 text-xs sm:text-sm font-medium text-gray-600 hover:text-gray-900 cursor-pointer">
61
+ Search
62
+ </button>
63
+ <button id="tab-knowledge" onclick="switchTab('knowledge')"
64
+ class="px-2 sm:px-4 py-4 text-xs sm:text-sm font-medium text-gray-600 hover:text-gray-900 cursor-pointer">
65
+ Knowledge
66
+ </button>
67
+ <button id="tab-spec" onclick="switchTab('spec')"
68
+ class="px-2 sm:px-4 py-4 text-xs sm:text-sm font-medium text-gray-600 hover:text-gray-900 cursor-pointer">
69
+ Spec
70
+ </button>
71
+ </div>
72
+ <div class="text-xs text-gray-400" id="status-indicator">
73
+ <span class="inline-block w-2 h-2 rounded-full bg-green-500 mr-1"></span>
74
+ <span class="hidden sm:inline">${escapeHtml(namespace)}</span>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </nav>
79
+
80
+ <!-- Main Content -->
81
+ <main id="main-content" class="p-2 sm:p-4">
82
+ <!-- Ticket View (default) - full width -->
83
+ <div id="view-ticket" hx-get="/partials/kanban-view" hx-trigger="load"></div>
84
+ <!-- Search View -->
85
+ <div id="view-search" class="hidden max-w-7xl mx-auto" hx-get="/partials/search-view" hx-trigger="revealed"></div>
86
+ <!-- Knowledge View -->
87
+ <div id="view-knowledge" class="hidden max-w-7xl mx-auto" hx-get="/partials/knowledge-view" hx-trigger="revealed"></div>
88
+ <!-- Spec View -->
89
+ <div id="view-spec" class="hidden max-w-7xl mx-auto" hx-get="/partials/spec-view" hx-trigger="revealed"></div>
90
+ </main>
91
+
92
+ <!-- Modal -->
93
+ <div id="modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50" onclick="if(event.target===this)hideModal()">
94
+ <div id="modal-content" class="modal-content bg-white rounded-lg shadow-2xl w-full max-w-2xl overflow-auto m-4">
95
+ <!-- Modal content loaded via HTMX -->
96
+ </div>
97
+ </div>
98
+
99
+ <script>
100
+ // Tab switching with URL hash persistence
101
+ function switchTab(tab, updateHash = true) {
102
+ ['ticket', 'search', 'knowledge', 'spec'].forEach(t => {
103
+ document.getElementById('view-' + t).classList.toggle('hidden', t !== tab);
104
+ document.getElementById('tab-' + t).classList.toggle('tab-active', t === tab);
105
+ });
106
+ // Update URL hash for refresh persistence
107
+ if (updateHash) {
108
+ history.replaceState(null, '', '#' + tab);
109
+ }
110
+ // Trigger HTMX load for lazy-loaded views
111
+ htmx.trigger('#view-' + tab, 'revealed');
112
+ }
113
+
114
+ // Restore tab from URL hash on page load
115
+ (function() {
116
+ const hash = window.location.hash.slice(1);
117
+ if (['ticket', 'search', 'knowledge', 'spec'].includes(hash)) {
118
+ switchTab(hash, false);
119
+ }
120
+ })();
121
+
122
+ // Markdown rendering with sanitization
123
+ function renderMarkdown(content) {
124
+ if (typeof marked !== 'undefined' && typeof DOMPurify !== 'undefined') {
125
+ const html = marked.parse(content);
126
+ return DOMPurify.sanitize(html, {
127
+ ADD_TAGS: ['input'],
128
+ ADD_ATTR: ['type', 'checked', 'disabled'],
129
+ });
130
+ }
131
+ // Fallback: escape HTML and preserve whitespace
132
+ return content.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
133
+ }
134
+
135
+ // Render markdown content in elements with data-markdown attribute
136
+ function processMarkdownElements() {
137
+ document.querySelectorAll('[data-markdown]').forEach(function(el) {
138
+ if (!el.dataset.rendered) {
139
+ el.innerHTML = renderMarkdown(el.textContent);
140
+ el.dataset.rendered = 'true';
141
+ }
142
+ });
143
+ }
144
+
145
+ // Markdown editor: toggle between write and preview tabs
146
+ function initMarkdownEditor(editorId) {
147
+ const editor = document.getElementById(editorId);
148
+ if (!editor) return;
149
+ const textarea = editor.querySelector('textarea');
150
+ const preview = editor.querySelector('[data-md-preview]');
151
+ const writeTab = editor.querySelector('[data-md-tab="write"]');
152
+ const previewTab = editor.querySelector('[data-md-tab="preview"]');
153
+
154
+ writeTab.addEventListener('click', function() {
155
+ textarea.classList.remove('hidden');
156
+ preview.classList.add('hidden');
157
+ writeTab.classList.add('border-blue-700', 'text-blue-700');
158
+ writeTab.classList.remove('border-transparent', 'text-gray-500');
159
+ previewTab.classList.remove('border-blue-700', 'text-blue-700');
160
+ previewTab.classList.add('border-transparent', 'text-gray-500');
161
+ });
162
+
163
+ previewTab.addEventListener('click', function() {
164
+ textarea.classList.add('hidden');
165
+ preview.classList.remove('hidden');
166
+ previewTab.classList.add('border-blue-700', 'text-blue-700');
167
+ previewTab.classList.remove('border-transparent', 'text-gray-500');
168
+ writeTab.classList.remove('border-blue-700', 'text-blue-700');
169
+ writeTab.classList.add('border-transparent', 'text-gray-500');
170
+ preview.innerHTML = textarea.value.trim()
171
+ ? renderMarkdown(textarea.value)
172
+ : '<p class="text-gray-400 italic">Nothing to preview</p>';
173
+ });
174
+ }
175
+
176
+ // Modal functions
177
+ function showModal() {
178
+ const modal = document.getElementById('modal');
179
+ // Clear previous content and show loading spinner
180
+ document.getElementById('modal-content').innerHTML = '<div class="flex justify-center items-center py-16"><div class="animate-spin h-8 w-8 border-4 border-blue-500 border-t-transparent rounded-full"></div></div>';
181
+ modal.classList.remove('hidden');
182
+ document.body.style.overflow = 'hidden';
183
+ // Trigger reflow for transition
184
+ void modal.offsetWidth;
185
+ modal.classList.add('show');
186
+ }
187
+ function hideModal() {
188
+ const modal = document.getElementById('modal');
189
+ modal.classList.remove('show');
190
+ document.body.style.overflow = '';
191
+ // Wait for transition to finish before hiding
192
+ setTimeout(() => modal.classList.add('hidden'), 150);
193
+ }
194
+ document.addEventListener('keydown', (e) => {
195
+ if (e.key === 'Escape') hideModal();
196
+ });
197
+
198
+ // Drag and drop
199
+ let draggedId = null;
200
+
201
+ function onDragStart(e, id) {
202
+ draggedId = id;
203
+ e.target.classList.add('dragging');
204
+ e.dataTransfer.effectAllowed = 'move';
205
+ }
206
+
207
+ function onDragEnd(e) {
208
+ e.target.classList.remove('dragging');
209
+ document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
210
+ }
211
+
212
+ function onDragOver(e) {
213
+ e.preventDefault();
214
+ e.currentTarget.classList.add('drag-over');
215
+ }
216
+
217
+ function onDragLeave(e) {
218
+ e.currentTarget.classList.remove('drag-over');
219
+ }
220
+
221
+ function onDrop(e, newStatus) {
222
+ e.preventDefault();
223
+ e.currentTarget.classList.remove('drag-over');
224
+ if (!draggedId) return;
225
+
226
+ fetch('/api/tickets/' + encodeURIComponent(draggedId) + '/status', {
227
+ method: 'PATCH',
228
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
229
+ body: 'status=' + encodeURIComponent(newStatus)
230
+ })
231
+ .then(r => r.json())
232
+ .then(data => {
233
+ if (data.success) {
234
+ // Refresh all columns
235
+ htmx.trigger('#kanban-columns', 'refresh');
236
+ }
237
+ });
238
+ draggedId = null;
239
+ }
240
+
241
+ // HTMX event handlers
242
+ document.body.addEventListener('htmx:afterRequest', function(e) {
243
+ if (e.detail.pathInfo.requestPath.includes('/status')) {
244
+ htmx.trigger('#kanban-columns', 'refresh');
245
+ }
246
+ });
247
+
248
+ // Process markdown elements after HTMX swaps content
249
+ document.body.addEventListener('htmx:afterSwap', function(e) {
250
+ processMarkdownElements();
251
+ });
252
+
253
+ // Auto-refresh polling for Tickets and Knowledge tabs
254
+ (function() {
255
+ const POLL_INTERVAL = 5000; // 5 seconds
256
+ let pollTimer = null;
257
+ let currentTab = 'ticket';
258
+ let isPageVisible = true;
259
+
260
+ // Start polling for the current tab
261
+ function startPolling() {
262
+ stopPolling();
263
+ if (!isPageVisible) return;
264
+
265
+ pollTimer = setInterval(function() {
266
+ if (!isPageVisible) return;
267
+
268
+ if (currentTab === 'ticket') {
269
+ htmx.trigger('#kanban-columns', 'refresh');
270
+ } else if (currentTab === 'knowledge') {
271
+ htmx.trigger('#knowledge-list', 'poll-refresh');
272
+ } else if (currentTab === 'spec') {
273
+ htmx.trigger('#spec-list', 'poll-refresh');
274
+ }
275
+ }, POLL_INTERVAL);
276
+ }
277
+
278
+ // Stop polling
279
+ function stopPolling() {
280
+ if (pollTimer) {
281
+ clearInterval(pollTimer);
282
+ pollTimer = null;
283
+ }
284
+ }
285
+
286
+ // Handle tab switching - update currentTab and restart polling
287
+ const originalSwitchTab = window.switchTab;
288
+ window.switchTab = function(tab, updateHash) {
289
+ currentTab = tab;
290
+ originalSwitchTab(tab, updateHash);
291
+ // Only poll for ticket and knowledge tabs
292
+ if (tab === 'ticket' || tab === 'knowledge' || tab === 'spec') {
293
+ startPolling();
294
+ } else {
295
+ stopPolling();
296
+ }
297
+ };
298
+
299
+ // Handle page visibility changes
300
+ document.addEventListener('visibilitychange', function() {
301
+ isPageVisible = !document.hidden;
302
+ if (isPageVisible && (currentTab === 'ticket' || currentTab === 'knowledge' || currentTab === 'spec')) {
303
+ // Immediate refresh when tab becomes visible
304
+ if (currentTab === 'ticket') {
305
+ htmx.trigger('#kanban-columns', 'refresh');
306
+ } else if (currentTab === 'knowledge') {
307
+ htmx.trigger('#knowledge-list', 'poll-refresh');
308
+ } else if (currentTab === 'spec') {
309
+ htmx.trigger('#spec-list', 'poll-refresh');
310
+ }
311
+ startPolling();
312
+ } else {
313
+ stopPolling();
314
+ }
315
+ });
316
+
317
+ // Initialize polling for default tab (ticket)
318
+ startPolling();
319
+ })();
320
+ </script>
321
+ </body>
322
+ </html>`;
323
+ }
@@ -0,0 +1,15 @@
1
+ export declare function renderSearchView(): string;
2
+ export declare function renderSearchResults(results: {
3
+ id: string;
4
+ title: string;
5
+ content: string;
6
+ category?: string;
7
+ namespace: string;
8
+ source?: string;
9
+ origin_ticket_type?: string;
10
+ decision_scope: string;
11
+ tags?: string[];
12
+ score: number;
13
+ confidence: number;
14
+ active: boolean;
15
+ }[]): string;