@jeganwrites/claudash 1.0.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/CONTRIBUTING.md +35 -0
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/analyzer.py +890 -0
- package/bin/claudash.js +121 -0
- package/claude_ai_tracker.py +358 -0
- package/cli.py +1034 -0
- package/config.py +100 -0
- package/db.py +1156 -0
- package/fix_tracker.py +539 -0
- package/insights.py +359 -0
- package/mcp_server.py +414 -0
- package/package.json +39 -0
- package/scanner.py +385 -0
- package/server.py +762 -0
- package/templates/accounts.html +936 -0
- package/templates/dashboard.html +1742 -0
- package/tools/get-derived-keys.py +112 -0
- package/tools/mac-sync.py +386 -0
- package/tools/oauth_sync.py +308 -0
- package/tools/setup-pm2.sh +53 -0
- package/waste_patterns.py +334 -0
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Accounts — Claudash</title>
|
|
7
|
+
<style>
|
|
8
|
+
@import url('https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=DM+Mono:wght@400;500&family=DM+Sans:wght@300;400;500;600&display=swap');
|
|
9
|
+
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #FAFAF8;
|
|
12
|
+
--surface: #FFFFFF;
|
|
13
|
+
--surface-warm: #F5F3EE;
|
|
14
|
+
--border: #E8E6E1;
|
|
15
|
+
--border-strong: #D4D0C8;
|
|
16
|
+
--text-primary: #1A1916;
|
|
17
|
+
--text-secondary: #6B6860;
|
|
18
|
+
--text-tertiary: #9C9890;
|
|
19
|
+
--accent: #1A1916;
|
|
20
|
+
--accent-green: #0D7A5F;
|
|
21
|
+
--accent-amber: #B45309;
|
|
22
|
+
--accent-red: #C0392B;
|
|
23
|
+
--accent-blue: #1E40AF;
|
|
24
|
+
--tag-bg: #F0EEE8;
|
|
25
|
+
|
|
26
|
+
--serif: 'DM Serif Display', Georgia, serif;
|
|
27
|
+
--sans: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
28
|
+
--mono: 'DM Mono', 'SF Mono', Menlo, Consolas, monospace;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
32
|
+
html, body {
|
|
33
|
+
background: var(--bg);
|
|
34
|
+
color: var(--text-primary);
|
|
35
|
+
font-family: var(--sans);
|
|
36
|
+
font-size: 14px;
|
|
37
|
+
line-height: 1.5;
|
|
38
|
+
-webkit-font-smoothing: antialiased;
|
|
39
|
+
}
|
|
40
|
+
a { color: inherit; text-decoration: none; }
|
|
41
|
+
button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
|
|
42
|
+
|
|
43
|
+
/* ── Header ─────────────────────────────────────────────────────── */
|
|
44
|
+
.header {
|
|
45
|
+
position: sticky; top: 0; z-index: 100;
|
|
46
|
+
height: 56px;
|
|
47
|
+
background: var(--bg);
|
|
48
|
+
border-bottom: 1px solid var(--border);
|
|
49
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
50
|
+
padding: 0 32px;
|
|
51
|
+
}
|
|
52
|
+
.brand { display: flex; align-items: baseline; gap: 10px; }
|
|
53
|
+
.brand .name {
|
|
54
|
+
font-family: var(--serif);
|
|
55
|
+
font-size: 22px;
|
|
56
|
+
letter-spacing: -0.01em;
|
|
57
|
+
}
|
|
58
|
+
.brand .version {
|
|
59
|
+
font-family: var(--mono);
|
|
60
|
+
font-size: 10px;
|
|
61
|
+
color: var(--text-tertiary);
|
|
62
|
+
padding: 2px 6px;
|
|
63
|
+
background: var(--tag-bg);
|
|
64
|
+
border-radius: 3px;
|
|
65
|
+
letter-spacing: 0.05em;
|
|
66
|
+
}
|
|
67
|
+
.header-right {
|
|
68
|
+
display: flex; align-items: center; gap: 20px;
|
|
69
|
+
font-family: var(--sans);
|
|
70
|
+
font-size: 13px;
|
|
71
|
+
color: var(--text-secondary);
|
|
72
|
+
}
|
|
73
|
+
.header-right a { color: var(--text-secondary); font-weight: 500; transition: color 0.15s; }
|
|
74
|
+
.header-right a:hover { color: var(--text-primary); }
|
|
75
|
+
.btn-primary {
|
|
76
|
+
background: var(--text-primary);
|
|
77
|
+
color: var(--bg);
|
|
78
|
+
padding: 7px 14px;
|
|
79
|
+
border-radius: 6px;
|
|
80
|
+
font-size: 13px;
|
|
81
|
+
font-weight: 500;
|
|
82
|
+
transition: opacity 0.15s;
|
|
83
|
+
}
|
|
84
|
+
.btn-primary:hover { opacity: 0.85; }
|
|
85
|
+
.btn-ghost {
|
|
86
|
+
padding: 6px 12px;
|
|
87
|
+
border-radius: 6px;
|
|
88
|
+
font-size: 13px;
|
|
89
|
+
font-weight: 500;
|
|
90
|
+
color: var(--text-secondary);
|
|
91
|
+
transition: all 0.15s;
|
|
92
|
+
}
|
|
93
|
+
.btn-ghost:hover { background: var(--surface-warm); color: var(--text-primary); }
|
|
94
|
+
.btn-danger {
|
|
95
|
+
padding: 6px 12px;
|
|
96
|
+
border-radius: 6px;
|
|
97
|
+
font-size: 13px;
|
|
98
|
+
font-weight: 500;
|
|
99
|
+
color: var(--accent-red);
|
|
100
|
+
transition: background 0.15s;
|
|
101
|
+
}
|
|
102
|
+
.btn-danger:hover { background: #FCE8E6; }
|
|
103
|
+
|
|
104
|
+
/* ── Content ────────────────────────────────────────────────────── */
|
|
105
|
+
.content {
|
|
106
|
+
max-width: 1000px;
|
|
107
|
+
margin: 0 auto;
|
|
108
|
+
padding: 40px 32px 80px;
|
|
109
|
+
}
|
|
110
|
+
.page-title {
|
|
111
|
+
font-family: var(--serif);
|
|
112
|
+
font-size: 28px;
|
|
113
|
+
margin-bottom: 8px;
|
|
114
|
+
}
|
|
115
|
+
.page-sub {
|
|
116
|
+
font-family: var(--sans);
|
|
117
|
+
font-size: 13px;
|
|
118
|
+
color: var(--text-secondary);
|
|
119
|
+
margin-bottom: 32px;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.msg-zone { min-height: 0; margin-bottom: 16px; }
|
|
123
|
+
.msg {
|
|
124
|
+
padding: 10px 14px;
|
|
125
|
+
border-radius: 6px;
|
|
126
|
+
font-size: 13px;
|
|
127
|
+
font-weight: 500;
|
|
128
|
+
animation: fadeIn 0.2s ease;
|
|
129
|
+
}
|
|
130
|
+
.msg.success { background: #E6F4EF; color: var(--accent-green); border: 1px solid #C5E4D8; }
|
|
131
|
+
.msg.error { background: #FCE8E6; color: var(--accent-red); border: 1px solid #F4C4C0; }
|
|
132
|
+
.msg.info { background: var(--surface-warm); color: var(--text-secondary); border: 1px solid var(--border); }
|
|
133
|
+
|
|
134
|
+
/* ── Accounts list ──────────────────────────────────────────────── */
|
|
135
|
+
.accounts-list { display: flex; flex-direction: column; gap: 16px; }
|
|
136
|
+
.account-card {
|
|
137
|
+
background: var(--surface);
|
|
138
|
+
border: 1px solid var(--border);
|
|
139
|
+
border-radius: 10px;
|
|
140
|
+
padding: 24px 28px;
|
|
141
|
+
animation: fadeUp 0.4s ease both;
|
|
142
|
+
}
|
|
143
|
+
.account-head {
|
|
144
|
+
display: flex; justify-content: space-between; align-items: flex-start;
|
|
145
|
+
margin-bottom: 16px;
|
|
146
|
+
gap: 20px;
|
|
147
|
+
}
|
|
148
|
+
.account-title { display: flex; align-items: center; gap: 12px; }
|
|
149
|
+
.color-dot {
|
|
150
|
+
width: 10px; height: 10px; border-radius: 50%;
|
|
151
|
+
}
|
|
152
|
+
.color-dot.teal { background: #0F766E; }
|
|
153
|
+
.color-dot.purple { background: #6D28D9; }
|
|
154
|
+
.color-dot.blue { background: #1E40AF; }
|
|
155
|
+
.color-dot.coral { background: #DD5D3D; }
|
|
156
|
+
.color-dot.amber { background: #B45309; }
|
|
157
|
+
.account-name {
|
|
158
|
+
font-family: var(--serif);
|
|
159
|
+
font-size: 20px;
|
|
160
|
+
color: var(--text-primary);
|
|
161
|
+
}
|
|
162
|
+
.plan-tag {
|
|
163
|
+
font-family: var(--mono);
|
|
164
|
+
font-size: 10px;
|
|
165
|
+
padding: 3px 8px;
|
|
166
|
+
border-radius: 10px;
|
|
167
|
+
background: var(--tag-bg);
|
|
168
|
+
color: var(--text-secondary);
|
|
169
|
+
letter-spacing: 0.05em;
|
|
170
|
+
text-transform: uppercase;
|
|
171
|
+
}
|
|
172
|
+
.account-actions { display: flex; gap: 4px; }
|
|
173
|
+
|
|
174
|
+
.meta-grid {
|
|
175
|
+
display: grid;
|
|
176
|
+
grid-template-columns: repeat(4, 1fr);
|
|
177
|
+
gap: 16px 24px;
|
|
178
|
+
padding: 16px 0;
|
|
179
|
+
border-top: 1px solid var(--border);
|
|
180
|
+
border-bottom: 1px solid var(--border);
|
|
181
|
+
margin-bottom: 16px;
|
|
182
|
+
}
|
|
183
|
+
.meta-cell .k {
|
|
184
|
+
font-family: var(--sans);
|
|
185
|
+
font-size: 10px;
|
|
186
|
+
font-weight: 500;
|
|
187
|
+
text-transform: uppercase;
|
|
188
|
+
letter-spacing: 0.06em;
|
|
189
|
+
color: var(--text-tertiary);
|
|
190
|
+
margin-bottom: 4px;
|
|
191
|
+
}
|
|
192
|
+
.meta-cell .v {
|
|
193
|
+
font-family: var(--mono);
|
|
194
|
+
font-size: 14px;
|
|
195
|
+
color: var(--text-primary);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.paths-list { margin-bottom: 12px; }
|
|
199
|
+
.paths-list .k {
|
|
200
|
+
font-family: var(--sans);
|
|
201
|
+
font-size: 10px;
|
|
202
|
+
font-weight: 500;
|
|
203
|
+
text-transform: uppercase;
|
|
204
|
+
letter-spacing: 0.06em;
|
|
205
|
+
color: var(--text-tertiary);
|
|
206
|
+
margin-bottom: 6px;
|
|
207
|
+
}
|
|
208
|
+
.path-row {
|
|
209
|
+
display: flex; align-items: center; gap: 10px;
|
|
210
|
+
padding: 4px 0;
|
|
211
|
+
font-family: var(--mono);
|
|
212
|
+
font-size: 12px;
|
|
213
|
+
color: var(--text-secondary);
|
|
214
|
+
}
|
|
215
|
+
.path-status {
|
|
216
|
+
width: 6px; height: 6px; border-radius: 50%;
|
|
217
|
+
}
|
|
218
|
+
.path-status.ok { background: var(--accent-green); }
|
|
219
|
+
.path-status.missing { background: var(--accent-red); }
|
|
220
|
+
.path-status.unknown { background: var(--text-tertiary); }
|
|
221
|
+
|
|
222
|
+
.proj-pills { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
|
|
223
|
+
.proj-pill {
|
|
224
|
+
font-family: var(--sans);
|
|
225
|
+
font-size: 11px;
|
|
226
|
+
font-weight: 500;
|
|
227
|
+
padding: 3px 10px;
|
|
228
|
+
border-radius: 10px;
|
|
229
|
+
background: var(--tag-bg);
|
|
230
|
+
color: var(--text-secondary);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* ── Inline form ────────────────────────────────────────────────── */
|
|
234
|
+
.form-card {
|
|
235
|
+
background: var(--surface);
|
|
236
|
+
border: 1px solid var(--border);
|
|
237
|
+
border-radius: 10px;
|
|
238
|
+
padding: 28px 32px;
|
|
239
|
+
margin-bottom: 20px;
|
|
240
|
+
display: none;
|
|
241
|
+
animation: fadeUp 0.3s ease;
|
|
242
|
+
}
|
|
243
|
+
.form-card.open { display: block; }
|
|
244
|
+
.form-card h3 {
|
|
245
|
+
font-family: var(--serif);
|
|
246
|
+
font-size: 22px;
|
|
247
|
+
margin-bottom: 20px;
|
|
248
|
+
}
|
|
249
|
+
.form-grid {
|
|
250
|
+
display: grid;
|
|
251
|
+
grid-template-columns: 1fr 1fr;
|
|
252
|
+
gap: 16px 20px;
|
|
253
|
+
margin-bottom: 20px;
|
|
254
|
+
}
|
|
255
|
+
.form-field.wide { grid-column: 1 / -1; }
|
|
256
|
+
.form-field label {
|
|
257
|
+
display: block;
|
|
258
|
+
font-family: var(--sans);
|
|
259
|
+
font-size: 10px;
|
|
260
|
+
font-weight: 500;
|
|
261
|
+
text-transform: uppercase;
|
|
262
|
+
letter-spacing: 0.06em;
|
|
263
|
+
color: var(--text-tertiary);
|
|
264
|
+
margin-bottom: 6px;
|
|
265
|
+
}
|
|
266
|
+
.form-field input, .form-field select {
|
|
267
|
+
width: 100%;
|
|
268
|
+
padding: 10px 12px;
|
|
269
|
+
font-family: var(--mono);
|
|
270
|
+
font-size: 13px;
|
|
271
|
+
color: var(--text-primary);
|
|
272
|
+
background: var(--bg);
|
|
273
|
+
border: 1px solid var(--border);
|
|
274
|
+
border-radius: 6px;
|
|
275
|
+
transition: border-color 0.15s;
|
|
276
|
+
}
|
|
277
|
+
.form-field input:focus, .form-field select:focus {
|
|
278
|
+
outline: none;
|
|
279
|
+
border-color: var(--text-primary);
|
|
280
|
+
}
|
|
281
|
+
.form-field input:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
282
|
+
.form-field .hint {
|
|
283
|
+
font-family: var(--sans);
|
|
284
|
+
font-size: 11px;
|
|
285
|
+
color: var(--text-tertiary);
|
|
286
|
+
margin-top: 4px;
|
|
287
|
+
}
|
|
288
|
+
.dynamic-list { display: flex; flex-direction: column; gap: 8px; }
|
|
289
|
+
.dynamic-row { display: flex; gap: 8px; align-items: center; }
|
|
290
|
+
.dynamic-row input { flex: 1; }
|
|
291
|
+
.remove-btn {
|
|
292
|
+
font-family: var(--mono);
|
|
293
|
+
font-size: 16px;
|
|
294
|
+
color: var(--text-tertiary);
|
|
295
|
+
padding: 4px 10px;
|
|
296
|
+
border-radius: 6px;
|
|
297
|
+
transition: all 0.15s;
|
|
298
|
+
}
|
|
299
|
+
.remove-btn:hover { background: #FCE8E6; color: var(--accent-red); }
|
|
300
|
+
.add-btn-inline {
|
|
301
|
+
font-family: var(--sans);
|
|
302
|
+
font-size: 12px;
|
|
303
|
+
font-weight: 500;
|
|
304
|
+
color: var(--text-secondary);
|
|
305
|
+
padding: 6px 0;
|
|
306
|
+
}
|
|
307
|
+
.add-btn-inline:hover { color: var(--text-primary); }
|
|
308
|
+
.form-actions {
|
|
309
|
+
display: flex; gap: 10px;
|
|
310
|
+
padding-top: 16px;
|
|
311
|
+
border-top: 1px solid var(--border);
|
|
312
|
+
}
|
|
313
|
+
.discover-suggestions {
|
|
314
|
+
background: var(--surface-warm);
|
|
315
|
+
border: 1px dashed var(--border);
|
|
316
|
+
border-radius: 6px;
|
|
317
|
+
padding: 12px 16px;
|
|
318
|
+
margin: 8px 0;
|
|
319
|
+
font-family: var(--mono);
|
|
320
|
+
font-size: 12px;
|
|
321
|
+
color: var(--text-secondary);
|
|
322
|
+
}
|
|
323
|
+
.discover-suggestions .title {
|
|
324
|
+
font-family: var(--sans);
|
|
325
|
+
font-size: 11px;
|
|
326
|
+
font-weight: 500;
|
|
327
|
+
text-transform: uppercase;
|
|
328
|
+
letter-spacing: 0.06em;
|
|
329
|
+
color: var(--text-tertiary);
|
|
330
|
+
margin-bottom: 6px;
|
|
331
|
+
}
|
|
332
|
+
.discover-item {
|
|
333
|
+
cursor: pointer;
|
|
334
|
+
padding: 4px 0;
|
|
335
|
+
transition: color 0.15s;
|
|
336
|
+
}
|
|
337
|
+
.discover-item:hover { color: var(--text-primary); }
|
|
338
|
+
|
|
339
|
+
/* ── Delete confirm ─────────────────────────────────────────────── */
|
|
340
|
+
.delete-confirm {
|
|
341
|
+
display: none;
|
|
342
|
+
margin-top: 16px;
|
|
343
|
+
padding: 14px 16px;
|
|
344
|
+
background: #FCE8E6;
|
|
345
|
+
border: 1px solid #F4C4C0;
|
|
346
|
+
border-radius: 6px;
|
|
347
|
+
font-size: 13px;
|
|
348
|
+
}
|
|
349
|
+
.delete-confirm.open { display: block; }
|
|
350
|
+
.delete-confirm p { color: var(--accent-red); margin-bottom: 10px; }
|
|
351
|
+
.delete-confirm .actions { display: flex; gap: 8px; }
|
|
352
|
+
|
|
353
|
+
/* ── Browser (claude.ai) card ───────────────────────────────────── */
|
|
354
|
+
.browser-card {
|
|
355
|
+
background: var(--surface-warm);
|
|
356
|
+
border: 1px solid var(--border);
|
|
357
|
+
border-radius: 8px;
|
|
358
|
+
padding: 16px 20px;
|
|
359
|
+
margin-top: 16px;
|
|
360
|
+
}
|
|
361
|
+
.browser-head {
|
|
362
|
+
display: flex; justify-content: space-between; align-items: baseline;
|
|
363
|
+
margin-bottom: 10px;
|
|
364
|
+
}
|
|
365
|
+
.browser-title {
|
|
366
|
+
font-family: var(--sans);
|
|
367
|
+
font-size: 12px;
|
|
368
|
+
font-weight: 600;
|
|
369
|
+
text-transform: uppercase;
|
|
370
|
+
letter-spacing: 0.06em;
|
|
371
|
+
color: var(--text-secondary);
|
|
372
|
+
}
|
|
373
|
+
.browser-status {
|
|
374
|
+
font-family: var(--mono);
|
|
375
|
+
font-size: 11px;
|
|
376
|
+
color: var(--text-tertiary);
|
|
377
|
+
}
|
|
378
|
+
.browser-row {
|
|
379
|
+
font-family: var(--mono);
|
|
380
|
+
font-size: 12px;
|
|
381
|
+
color: var(--text-primary);
|
|
382
|
+
margin-bottom: 8px;
|
|
383
|
+
}
|
|
384
|
+
.browser-row .val { font-weight: 500; }
|
|
385
|
+
.browser-actions {
|
|
386
|
+
display: flex; gap: 6px;
|
|
387
|
+
margin-top: 10px;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.setup-steps {
|
|
391
|
+
display: none;
|
|
392
|
+
margin-top: 14px;
|
|
393
|
+
padding: 14px 16px;
|
|
394
|
+
background: var(--bg);
|
|
395
|
+
border: 1px solid var(--border);
|
|
396
|
+
border-radius: 6px;
|
|
397
|
+
}
|
|
398
|
+
.setup-steps.open { display: block; }
|
|
399
|
+
.step {
|
|
400
|
+
display: flex; gap: 12px;
|
|
401
|
+
font-family: var(--sans);
|
|
402
|
+
font-size: 12px;
|
|
403
|
+
color: var(--text-secondary);
|
|
404
|
+
margin-bottom: 12px;
|
|
405
|
+
}
|
|
406
|
+
.step-dot {
|
|
407
|
+
width: 20px; height: 20px;
|
|
408
|
+
border-radius: 50%;
|
|
409
|
+
background: var(--surface-warm);
|
|
410
|
+
border: 1px solid var(--border);
|
|
411
|
+
display: flex; align-items: center; justify-content: center;
|
|
412
|
+
font-family: var(--mono);
|
|
413
|
+
font-size: 11px;
|
|
414
|
+
color: var(--text-secondary);
|
|
415
|
+
flex-shrink: 0;
|
|
416
|
+
}
|
|
417
|
+
.step.done .step-dot { background: var(--accent-green); border-color: var(--accent-green); color: white; }
|
|
418
|
+
.setup-input {
|
|
419
|
+
display: flex; gap: 8px;
|
|
420
|
+
margin-top: 10px;
|
|
421
|
+
}
|
|
422
|
+
.setup-input input {
|
|
423
|
+
flex: 1;
|
|
424
|
+
padding: 8px 12px;
|
|
425
|
+
font-family: var(--mono);
|
|
426
|
+
font-size: 12px;
|
|
427
|
+
background: var(--surface);
|
|
428
|
+
border: 1px solid var(--border);
|
|
429
|
+
border-radius: 6px;
|
|
430
|
+
}
|
|
431
|
+
.setup-input input:focus { outline: none; border-color: var(--text-primary); }
|
|
432
|
+
.setup-result {
|
|
433
|
+
margin-top: 10px;
|
|
434
|
+
padding: 8px 12px;
|
|
435
|
+
border-radius: 4px;
|
|
436
|
+
font-family: var(--mono);
|
|
437
|
+
font-size: 12px;
|
|
438
|
+
}
|
|
439
|
+
.setup-result.ok { background: #E6F4EF; color: var(--accent-green); }
|
|
440
|
+
.setup-result.fail { background: #FCE8E6; color: var(--accent-red); }
|
|
441
|
+
.security-note {
|
|
442
|
+
margin-top: 12px;
|
|
443
|
+
font-size: 11px;
|
|
444
|
+
color: var(--text-tertiary);
|
|
445
|
+
font-style: italic;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/* ── Empty state ────────────────────────────────────────────────── */
|
|
449
|
+
.empty {
|
|
450
|
+
text-align: center;
|
|
451
|
+
padding: 60px 20px;
|
|
452
|
+
color: var(--text-tertiary);
|
|
453
|
+
}
|
|
454
|
+
.empty .title {
|
|
455
|
+
font-family: var(--serif);
|
|
456
|
+
font-size: 20px;
|
|
457
|
+
color: var(--text-secondary);
|
|
458
|
+
margin-bottom: 8px;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/* ── Animations ─────────────────────────────────────────────────── */
|
|
462
|
+
@keyframes fadeUp {
|
|
463
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
464
|
+
to { opacity: 1; transform: translateY(0); }
|
|
465
|
+
}
|
|
466
|
+
@keyframes fadeIn {
|
|
467
|
+
from { opacity: 0; }
|
|
468
|
+
to { opacity: 1; }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
@media (max-width: 760px) {
|
|
472
|
+
.content { padding: 24px 18px 60px; }
|
|
473
|
+
.form-grid { grid-template-columns: 1fr; }
|
|
474
|
+
.meta-grid { grid-template-columns: repeat(2, 1fr); }
|
|
475
|
+
}
|
|
476
|
+
</style>
|
|
477
|
+
</head>
|
|
478
|
+
<body>
|
|
479
|
+
|
|
480
|
+
<header class="header">
|
|
481
|
+
<div class="brand">
|
|
482
|
+
<span class="name">Claudash</span>
|
|
483
|
+
<span class="version">v1.0</span>
|
|
484
|
+
</div>
|
|
485
|
+
<div class="header-right">
|
|
486
|
+
<a href="/">← Dashboard</a>
|
|
487
|
+
<button class="btn-primary" id="add-btn">+ Add account</button>
|
|
488
|
+
</div>
|
|
489
|
+
</header>
|
|
490
|
+
|
|
491
|
+
<main class="content">
|
|
492
|
+
<h1 class="page-title">Accounts</h1>
|
|
493
|
+
<p class="page-sub">Configure Claude subscriptions, data paths, projects, and claude.ai browser tracking.</p>
|
|
494
|
+
<p style="font-size:12px;color:var(--text-tertiary);margin:-4px 0 16px 0;font-family:var(--sans)">config.py sets initial account configuration on first run. After first run, accounts are managed here. Changes to config.py after first run have no effect.</p>
|
|
495
|
+
|
|
496
|
+
<div class="msg-zone" id="msg"></div>
|
|
497
|
+
|
|
498
|
+
<div class="form-card" id="form"></div>
|
|
499
|
+
|
|
500
|
+
<div class="accounts-list" id="accounts-list"></div>
|
|
501
|
+
</main>
|
|
502
|
+
|
|
503
|
+
<script>
|
|
504
|
+
// ── Dashboard-key auth wrapper ───────────────────────────────────────
|
|
505
|
+
const DASHBOARD_KEY_STORAGE = 'dashboard_key';
|
|
506
|
+
function authHeaders() {
|
|
507
|
+
return {
|
|
508
|
+
'Content-Type': 'application/json',
|
|
509
|
+
'X-Dashboard-Key': localStorage.getItem(DASHBOARD_KEY_STORAGE) || ''
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
(function installAuthFetch() {
|
|
513
|
+
const _origFetch = window.fetch.bind(window);
|
|
514
|
+
window.fetch = function(url, opts) {
|
|
515
|
+
opts = opts || {};
|
|
516
|
+
const method = (opts.method || 'GET').toUpperCase();
|
|
517
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
518
|
+
opts.headers = Object.assign(
|
|
519
|
+
{'X-Dashboard-Key': localStorage.getItem(DASHBOARD_KEY_STORAGE) || ''},
|
|
520
|
+
opts.headers || {}
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
return _origFetch(url, opts).then(function(resp) {
|
|
524
|
+
if (resp.status === 401) {
|
|
525
|
+
const key = prompt(
|
|
526
|
+
'Dashboard key required.\n\n' +
|
|
527
|
+
'Run on the server:\n' +
|
|
528
|
+
' python3 cli.py keys\n\n' +
|
|
529
|
+
'Copy the dashboard_key value and paste it here:'
|
|
530
|
+
);
|
|
531
|
+
if (key) {
|
|
532
|
+
localStorage.setItem(DASHBOARD_KEY_STORAGE, key.trim());
|
|
533
|
+
location.reload();
|
|
534
|
+
}
|
|
535
|
+
throw new Error('unauthorized');
|
|
536
|
+
}
|
|
537
|
+
return resp;
|
|
538
|
+
});
|
|
539
|
+
};
|
|
540
|
+
})();
|
|
541
|
+
|
|
542
|
+
// ── App ─────────────────────────────────────────────────────────
|
|
543
|
+
(function() {
|
|
544
|
+
let accounts = [];
|
|
545
|
+
let browserAccounts = {};
|
|
546
|
+
let editingId = null;
|
|
547
|
+
|
|
548
|
+
const $ = (id) => document.getElementById(id);
|
|
549
|
+
function esc(s) {
|
|
550
|
+
return String(s == null ? '' : s)
|
|
551
|
+
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
|
552
|
+
.replace(/"/g,'"').replace(/'/g,''');
|
|
553
|
+
}
|
|
554
|
+
function fmtNum(n) { return (n || 0).toLocaleString('en-US'); }
|
|
555
|
+
function fmtMoney(n) { return '$' + (n || 0).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2}); }
|
|
556
|
+
function timeAgo(epoch) {
|
|
557
|
+
if (!epoch) return 'never';
|
|
558
|
+
const diff = Math.floor(Date.now()/1000) - epoch;
|
|
559
|
+
if (diff < 60) return diff + 's ago';
|
|
560
|
+
if (diff < 3600) return Math.floor(diff/60) + 'm ago';
|
|
561
|
+
if (diff < 86400) return Math.floor(diff/3600) + 'h ago';
|
|
562
|
+
return Math.floor(diff/86400) + 'd ago';
|
|
563
|
+
}
|
|
564
|
+
function showMsg(text, type) {
|
|
565
|
+
$('msg').innerHTML = '<div class="msg ' + type + '">' + esc(text) + '</div>';
|
|
566
|
+
setTimeout(() => { $('msg').innerHTML = ''; }, 5000);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function load() {
|
|
570
|
+
fetch('/api/accounts').then(r => r.json()).then(data => {
|
|
571
|
+
accounts = data || [];
|
|
572
|
+
return fetch('/api/claude-ai/accounts').then(r => r.json());
|
|
573
|
+
}).then(ba => {
|
|
574
|
+
browserAccounts = {};
|
|
575
|
+
(ba || []).forEach(a => { browserAccounts[a.account_id] = a; });
|
|
576
|
+
renderList();
|
|
577
|
+
}).catch(() => { renderList(); });
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function renderList() {
|
|
581
|
+
const el = $('accounts-list');
|
|
582
|
+
if (!accounts.length) {
|
|
583
|
+
el.innerHTML =
|
|
584
|
+
'<div class="empty">' +
|
|
585
|
+
'<div class="title">No accounts yet</div>' +
|
|
586
|
+
'<div>Click "+ Add account" to get started.</div>' +
|
|
587
|
+
'</div>';
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
el.innerHTML = accounts.map(a => renderCard(a)).join('');
|
|
591
|
+
wireCardEvents();
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function renderCard(a) {
|
|
595
|
+
const paths = a.data_paths || [];
|
|
596
|
+
const projs = a.projects || [];
|
|
597
|
+
const plan = (a.plan || 'max').toUpperCase();
|
|
598
|
+
return (
|
|
599
|
+
'<div class="account-card" data-id="' + esc(a.account_id) + '">' +
|
|
600
|
+
'<div class="account-head">' +
|
|
601
|
+
'<div class="account-title">' +
|
|
602
|
+
'<div class="color-dot ' + esc(a.color || 'teal') + '"></div>' +
|
|
603
|
+
'<div class="account-name">' + esc(a.label) + '</div>' +
|
|
604
|
+
'<div class="plan-tag">' + esc(plan) + '</div>' +
|
|
605
|
+
'</div>' +
|
|
606
|
+
'<div class="account-actions">' +
|
|
607
|
+
'<button class="btn-ghost" data-edit="' + esc(a.account_id) + '">Edit</button>' +
|
|
608
|
+
'<button class="btn-ghost" data-scan="' + esc(a.account_id) + '">Scan</button>' +
|
|
609
|
+
'<button class="btn-danger" data-delete="' + esc(a.account_id) + '">Delete</button>' +
|
|
610
|
+
'</div>' +
|
|
611
|
+
'</div>' +
|
|
612
|
+
|
|
613
|
+
'<div class="meta-grid">' +
|
|
614
|
+
'<div class="meta-cell"><div class="k">Plan cost</div><div class="v">' + fmtMoney(a.monthly_cost_usd || 0) + '/mo</div></div>' +
|
|
615
|
+
'<div class="meta-cell"><div class="k">Window limit</div><div class="v">' + fmtNum(a.window_token_limit || 0) + '</div></div>' +
|
|
616
|
+
'<div class="meta-cell"><div class="k">Daily budget</div><div class="v">' + (a.daily_budget_usd > 0 ? fmtMoney(a.daily_budget_usd) : 'off') + '</div></div>' +
|
|
617
|
+
'<div class="meta-cell"><div class="k">30d spend</div><div class="v">' + (a.cost_30d !== undefined ? fmtMoney(a.cost_30d) : '—') + '</div></div>' +
|
|
618
|
+
'<div class="meta-cell"><div class="k">Sessions (30d)</div><div class="v">' + (a.sessions_30d !== undefined ? fmtNum(a.sessions_30d) : '—') + '</div></div>' +
|
|
619
|
+
'</div>' +
|
|
620
|
+
|
|
621
|
+
'<div class="paths-list">' +
|
|
622
|
+
'<div class="k">Data paths</div>' +
|
|
623
|
+
paths.map(p =>
|
|
624
|
+
'<div class="path-row"><span class="path-status unknown" data-pathcheck="' + esc(a.account_id) + '::' + esc(p) + '"></span><span>' + esc(p) + '</span></div>'
|
|
625
|
+
).join('') +
|
|
626
|
+
'</div>' +
|
|
627
|
+
|
|
628
|
+
(projs.length ?
|
|
629
|
+
'<div class="proj-pills">' +
|
|
630
|
+
projs.map(p => '<span class="proj-pill">' + esc(p.project_name) + '</span>').join('') +
|
|
631
|
+
'</div>' : '') +
|
|
632
|
+
|
|
633
|
+
renderBrowser(a.account_id) +
|
|
634
|
+
|
|
635
|
+
'<div class="delete-confirm" id="del-' + esc(a.account_id) + '">' +
|
|
636
|
+
'<p>This hides the account from the dashboard but keeps all session data.</p>' +
|
|
637
|
+
'<div class="actions">' +
|
|
638
|
+
'<button class="btn-danger" data-confirm-delete="' + esc(a.account_id) + '">Confirm delete</button>' +
|
|
639
|
+
'<button class="btn-ghost" data-cancel-delete="' + esc(a.account_id) + '">Cancel</button>' +
|
|
640
|
+
'</div>' +
|
|
641
|
+
'</div>' +
|
|
642
|
+
'</div>'
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function renderBrowser(accountId) {
|
|
647
|
+
const ba = browserAccounts[accountId];
|
|
648
|
+
const status = ba ? ba.status : 'unconfigured';
|
|
649
|
+
const snap = ba ? ba.latest_snapshot : null;
|
|
650
|
+
const lastPolled = ba ? ba.last_polled : null;
|
|
651
|
+
const plan = ba ? ba.plan : 'max';
|
|
652
|
+
const pollAgo = lastPolled ? timeAgo(lastPolled) : 'never';
|
|
653
|
+
|
|
654
|
+
let body = '';
|
|
655
|
+
if (status === 'unconfigured') {
|
|
656
|
+
body =
|
|
657
|
+
'<div class="browser-row">Not connected — add a session key to track claude.ai window burn.</div>' +
|
|
658
|
+
'<div class="browser-actions">' +
|
|
659
|
+
'<button class="btn-primary" data-setup="' + esc(accountId) + '">Connect claude.ai</button>' +
|
|
660
|
+
'</div>';
|
|
661
|
+
} else if (status === 'expired') {
|
|
662
|
+
body =
|
|
663
|
+
'<div class="browser-row">Session expired · last polled ' + esc(pollAgo) + '</div>' +
|
|
664
|
+
'<div class="browser-actions">' +
|
|
665
|
+
'<button class="btn-primary" data-setup="' + esc(accountId) + '">Reconnect</button>' +
|
|
666
|
+
'<button class="btn-danger" data-clear-session="' + esc(accountId) + '">Remove</button>' +
|
|
667
|
+
'</div>';
|
|
668
|
+
} else if (status === 'active' && snap) {
|
|
669
|
+
const usage = plan === 'pro' && snap.messages_limit
|
|
670
|
+
? snap.messages_used + '/' + snap.messages_limit + ' messages'
|
|
671
|
+
: (snap.pct_used || 0).toFixed(1) + '% window used';
|
|
672
|
+
body =
|
|
673
|
+
'<div class="browser-row"><span class="val">' + esc(usage) + '</span> · last polled ' + esc(pollAgo) + '</div>' +
|
|
674
|
+
'<div class="browser-actions">' +
|
|
675
|
+
'<button class="btn-ghost" data-refresh-browser="' + esc(accountId) + '">Refresh</button>' +
|
|
676
|
+
'<button class="btn-danger" data-clear-session="' + esc(accountId) + '">Remove session</button>' +
|
|
677
|
+
'</div>';
|
|
678
|
+
} else {
|
|
679
|
+
body = '<div class="browser-row">' + esc(status) + '</div>';
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return (
|
|
683
|
+
'<div class="browser-card">' +
|
|
684
|
+
'<div class="browser-head">' +
|
|
685
|
+
'<span class="browser-title">claude.ai browser</span>' +
|
|
686
|
+
'<span class="browser-status">' + esc(status) + '</span>' +
|
|
687
|
+
'</div>' +
|
|
688
|
+
body +
|
|
689
|
+
'<div class="setup-steps" id="setup-' + esc(accountId) + '">' +
|
|
690
|
+
'<div class="step done"><div class="step-dot">✓</div><div>Log into claude.ai in Chrome or Vivaldi on your Mac</div></div>' +
|
|
691
|
+
'<div class="step"><div class="step-dot">1</div><div>Open DevTools → Application → Cookies → https://claude.ai, copy the <b>sessionKey</b> value</div></div>' +
|
|
692
|
+
'<div class="step"><div class="step-dot">2</div><div>Paste it below and click Verify. Your key is stored only on this server.</div></div>' +
|
|
693
|
+
'<div class="setup-input">' +
|
|
694
|
+
'<input type="password" id="sk-' + esc(accountId) + '" placeholder="sk-ant-sid01-..." autocomplete="off">' +
|
|
695
|
+
'<button class="btn-primary" data-verify-setup="' + esc(accountId) + '">Verify</button>' +
|
|
696
|
+
'<button class="btn-ghost" data-cancel-setup="' + esc(accountId) + '">Cancel</button>' +
|
|
697
|
+
'</div>' +
|
|
698
|
+
'<div id="setup-result-' + esc(accountId) + '"></div>' +
|
|
699
|
+
'<div class="security-note">Session keys are plaintext in the SQLite DB. Clear them with Remove session when you\'re done.</div>' +
|
|
700
|
+
'</div>' +
|
|
701
|
+
'</div>'
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function wireCardEvents() {
|
|
706
|
+
// Path existence check
|
|
707
|
+
document.querySelectorAll('[data-pathcheck]').forEach(el => {
|
|
708
|
+
const [aid, p] = el.dataset.pathcheck.split('::');
|
|
709
|
+
fetch('/api/accounts/' + aid + '/preview').then(r => r.json()).then(data => {
|
|
710
|
+
const match = (data.paths || []).find(x => x.path === p);
|
|
711
|
+
if (match && match.exists) el.className = 'path-status ok';
|
|
712
|
+
else if (match) el.className = 'path-status missing';
|
|
713
|
+
else el.className = 'path-status unknown';
|
|
714
|
+
}).catch(() => {});
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// Buttons
|
|
718
|
+
document.querySelectorAll('[data-edit]').forEach(b => b.addEventListener('click', () => {
|
|
719
|
+
const a = accounts.find(x => x.account_id === b.dataset.edit); if (a) showForm(a);
|
|
720
|
+
}));
|
|
721
|
+
document.querySelectorAll('[data-scan]').forEach(b => b.addEventListener('click', (e) => {
|
|
722
|
+
e.stopPropagation();
|
|
723
|
+
const id = b.dataset.scan; const prev = b.textContent;
|
|
724
|
+
b.textContent = 'Scanning…'; b.disabled = true;
|
|
725
|
+
fetch('/api/accounts/' + id + '/scan', {method: 'POST', headers: authHeaders()})
|
|
726
|
+
.then(r => r.json())
|
|
727
|
+
.then(d => { b.textContent = prev; b.disabled = false; showMsg('Scanned ' + id + ': ' + (d.rows_added || 0) + ' new rows', 'success'); load(); })
|
|
728
|
+
.catch(() => { b.textContent = prev; b.disabled = false; });
|
|
729
|
+
}));
|
|
730
|
+
document.querySelectorAll('[data-delete]').forEach(b => b.addEventListener('click', () => {
|
|
731
|
+
const el = $('del-' + b.dataset.delete);
|
|
732
|
+
if (el) el.classList.add('open');
|
|
733
|
+
}));
|
|
734
|
+
document.querySelectorAll('[data-confirm-delete]').forEach(b => b.addEventListener('click', () => {
|
|
735
|
+
const id = b.dataset.confirmDelete;
|
|
736
|
+
fetch('/api/accounts/' + id, {method: 'DELETE', headers: authHeaders()})
|
|
737
|
+
.then(r => r.json())
|
|
738
|
+
.then(resp => { if (resp.success) { showMsg('Deleted ' + id, 'success'); load(); } else showMsg(resp.error || 'Delete failed', 'error'); });
|
|
739
|
+
}));
|
|
740
|
+
document.querySelectorAll('[data-cancel-delete]').forEach(b => b.addEventListener('click', () => {
|
|
741
|
+
const el = $('del-' + b.dataset.cancelDelete);
|
|
742
|
+
if (el) el.classList.remove('open');
|
|
743
|
+
}));
|
|
744
|
+
document.querySelectorAll('[data-setup]').forEach(b => b.addEventListener('click', () => {
|
|
745
|
+
const el = $('setup-' + b.dataset.setup);
|
|
746
|
+
if (el) el.classList.add('open');
|
|
747
|
+
}));
|
|
748
|
+
document.querySelectorAll('[data-cancel-setup]').forEach(b => b.addEventListener('click', () => {
|
|
749
|
+
const el = $('setup-' + b.dataset.cancelSetup);
|
|
750
|
+
if (el) el.classList.remove('open');
|
|
751
|
+
}));
|
|
752
|
+
document.querySelectorAll('[data-verify-setup]').forEach(b => b.addEventListener('click', () => {
|
|
753
|
+
const id = b.dataset.verifySetup;
|
|
754
|
+
const sk = $('sk-' + id).value.trim();
|
|
755
|
+
const res = $('setup-result-' + id);
|
|
756
|
+
if (!sk) { res.innerHTML = '<div class="setup-result fail">Paste your session key first.</div>'; return; }
|
|
757
|
+
b.textContent = 'Verifying…'; b.disabled = true;
|
|
758
|
+
fetch('/api/claude-ai/accounts/' + id + '/setup', {
|
|
759
|
+
method: 'POST', headers: authHeaders(),
|
|
760
|
+
body: JSON.stringify({session_key: sk})
|
|
761
|
+
}).then(r => r.json()).then(resp => {
|
|
762
|
+
b.textContent = 'Verify'; b.disabled = false;
|
|
763
|
+
if (resp.success) {
|
|
764
|
+
res.innerHTML = '<div class="setup-result ok">Connected — ' + esc(resp.label) + ', ' + (resp.pct_used || 0).toFixed(1) + '% window used</div>';
|
|
765
|
+
setTimeout(load, 1500);
|
|
766
|
+
} else {
|
|
767
|
+
res.innerHTML = '<div class="setup-result fail">' + esc(resp.error || 'Verification failed') + '</div>';
|
|
768
|
+
}
|
|
769
|
+
}).catch(() => {
|
|
770
|
+
b.textContent = 'Verify'; b.disabled = false;
|
|
771
|
+
res.innerHTML = '<div class="setup-result fail">Network error</div>';
|
|
772
|
+
});
|
|
773
|
+
}));
|
|
774
|
+
document.querySelectorAll('[data-refresh-browser]').forEach(b => b.addEventListener('click', () => {
|
|
775
|
+
const id = b.dataset.refreshBrowser;
|
|
776
|
+
b.textContent = 'Refreshing…'; b.disabled = true;
|
|
777
|
+
fetch('/api/claude-ai/accounts/' + id + '/refresh', {method: 'POST', headers: authHeaders()})
|
|
778
|
+
.then(r => r.json())
|
|
779
|
+
.then(resp => { b.textContent = 'Refresh'; b.disabled = false; showMsg(resp.success ? 'Refreshed' : 'Refresh failed', resp.success ? 'success' : 'error'); load(); });
|
|
780
|
+
}));
|
|
781
|
+
document.querySelectorAll('[data-clear-session]').forEach(b => b.addEventListener('click', () => {
|
|
782
|
+
const id = b.dataset.clearSession;
|
|
783
|
+
if (!confirm('Remove the session key for ' + id + '?')) return;
|
|
784
|
+
fetch('/api/claude-ai/accounts/' + id + '/session', {method: 'DELETE', headers: authHeaders()})
|
|
785
|
+
.then(r => r.json())
|
|
786
|
+
.then(() => { showMsg('Session cleared for ' + id, 'success'); load(); });
|
|
787
|
+
}));
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// ── Form ───────────────────────────────────────────────────────
|
|
791
|
+
function showForm(a) {
|
|
792
|
+
editingId = a ? a.account_id : null;
|
|
793
|
+
const f = $('form');
|
|
794
|
+
f.classList.add('open');
|
|
795
|
+
f.innerHTML =
|
|
796
|
+
'<h3>' + (a ? 'Edit ' + esc(a.label) : 'Add account') + '</h3>' +
|
|
797
|
+
'<div class="form-grid">' +
|
|
798
|
+
'<div class="form-field"><label>Account ID</label><input id="f-id" value="' + esc(a ? a.account_id : '') + '" placeholder="e.g. work_max"' + (a ? ' disabled' : '') + '><div class="hint">Lowercase slug, underscores OK</div></div>' +
|
|
799
|
+
'<div class="form-field"><label>Display label</label><input id="f-label" value="' + esc(a ? a.label : '') + '" placeholder="e.g. Work (Max)"></div>' +
|
|
800
|
+
'<div class="form-field"><label>Plan</label><select id="f-plan"><option value="max">max</option><option value="pro">pro</option><option value="api">api</option></select></div>' +
|
|
801
|
+
'<div class="form-field"><label>Color</label><select id="f-color"><option value="teal">teal</option><option value="purple">purple</option><option value="blue">blue</option><option value="coral">coral</option><option value="amber">amber</option></select></div>' +
|
|
802
|
+
'<div class="form-field"><label>Monthly cost (USD)</label><input id="f-cost" type="number" value="' + (a ? a.monthly_cost_usd : 100) + '"></div>' +
|
|
803
|
+
'<div class="form-field"><label>Window token limit</label><input id="f-limit" type="number" value="' + (a ? a.window_token_limit : 1000000) + '"></div>' +
|
|
804
|
+
'<div class="form-field"><label>Daily budget (USD)</label><input id="f-budget" type="number" step="0.5" value="' + (a && a.daily_budget_usd ? a.daily_budget_usd : 0) + '"><div class="hint">API-equivalent. 0 disables budget alerts.</div></div>' +
|
|
805
|
+
'<div class="form-field wide"><label>Data paths</label><div class="dynamic-list" id="f-paths"></div><button class="add-btn-inline" id="add-path-btn">+ Add another path</button><div id="discover-suggestions"></div></div>' +
|
|
806
|
+
'<div class="form-field wide"><label>Projects (keyword → project name)</label><div class="dynamic-list" id="f-projects"></div><button class="add-btn-inline" id="add-proj-btn">+ Add another project</button></div>' +
|
|
807
|
+
'</div>' +
|
|
808
|
+
'<div class="form-actions">' +
|
|
809
|
+
'<button class="btn-primary" id="save-btn">Save</button>' +
|
|
810
|
+
'<button class="btn-ghost" id="cancel-btn">Cancel</button>' +
|
|
811
|
+
'</div>';
|
|
812
|
+
|
|
813
|
+
// Seed paths
|
|
814
|
+
const pathsList = $('f-paths');
|
|
815
|
+
const initialPaths = (a && a.data_paths && a.data_paths.length) ? a.data_paths : ['~/.claude/projects/'];
|
|
816
|
+
initialPaths.forEach(p => pathsList.appendChild(makePathRow(p)));
|
|
817
|
+
|
|
818
|
+
// Seed projects
|
|
819
|
+
const projList = $('f-projects');
|
|
820
|
+
const initialProjs = a && a.projects ? a.projects : [];
|
|
821
|
+
initialProjs.forEach(p => projList.appendChild(makeProjRow(p.project_name, (p.keywords || []).join(', '))));
|
|
822
|
+
|
|
823
|
+
// Select current values
|
|
824
|
+
if (a) {
|
|
825
|
+
$('f-plan').value = a.plan || 'max';
|
|
826
|
+
$('f-color').value = a.color || 'teal';
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
$('add-path-btn').addEventListener('click', () => pathsList.appendChild(makePathRow('')));
|
|
830
|
+
$('add-proj-btn').addEventListener('click', () => projList.appendChild(makeProjRow('', '')));
|
|
831
|
+
$('save-btn').addEventListener('click', save);
|
|
832
|
+
$('cancel-btn').addEventListener('click', hideForm);
|
|
833
|
+
|
|
834
|
+
// Discover
|
|
835
|
+
fetch('/api/accounts/discover', {method: 'POST', headers: authHeaders(), body: '{}'})
|
|
836
|
+
.then(r => r.json())
|
|
837
|
+
.then(d => {
|
|
838
|
+
const paths = d.discovered_paths || [];
|
|
839
|
+
if (!paths.length) return;
|
|
840
|
+
const sug = $('discover-suggestions');
|
|
841
|
+
sug.className = 'discover-suggestions';
|
|
842
|
+
sug.innerHTML =
|
|
843
|
+
'<div class="title">Found on this machine</div>' +
|
|
844
|
+
paths.map(p => '<div class="discover-item" data-discover="' + esc(p.path) + '">' + esc(p.path) + ' — ' + (p.estimated_records || 0) + ' files</div>').join('');
|
|
845
|
+
sug.querySelectorAll('[data-discover]').forEach(el => el.addEventListener('click', () => {
|
|
846
|
+
// Fill the first empty input, or add a new row
|
|
847
|
+
const inputs = pathsList.querySelectorAll('input');
|
|
848
|
+
let filled = false;
|
|
849
|
+
for (const inp of inputs) { if (!inp.value.trim()) { inp.value = el.dataset.discover; filled = true; break; } }
|
|
850
|
+
if (!filled) pathsList.appendChild(makePathRow(el.dataset.discover));
|
|
851
|
+
}));
|
|
852
|
+
}).catch(() => {});
|
|
853
|
+
|
|
854
|
+
f.scrollIntoView({behavior: 'smooth'});
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function makePathRow(val) {
|
|
858
|
+
const row = document.createElement('div');
|
|
859
|
+
row.className = 'dynamic-row';
|
|
860
|
+
row.innerHTML = '<input type="text" value="' + esc(val) + '" placeholder="~/.claude/projects/"><button class="remove-btn">×</button>';
|
|
861
|
+
row.querySelector('.remove-btn').addEventListener('click', () => {
|
|
862
|
+
if ($('f-paths').children.length > 1) row.remove();
|
|
863
|
+
});
|
|
864
|
+
return row;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function makeProjRow(name, keywords) {
|
|
868
|
+
const row = document.createElement('div');
|
|
869
|
+
row.className = 'dynamic-row';
|
|
870
|
+
row.innerHTML =
|
|
871
|
+
'<input type="text" value="' + esc(name) + '" placeholder="Project name">' +
|
|
872
|
+
'<input type="text" value="' + esc(keywords) + '" placeholder="keyword1, keyword2">' +
|
|
873
|
+
'<button class="remove-btn">×</button>';
|
|
874
|
+
row.querySelector('.remove-btn').addEventListener('click', () => row.remove());
|
|
875
|
+
return row;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function hideForm() { $('form').classList.remove('open'); editingId = null; }
|
|
879
|
+
|
|
880
|
+
function save() {
|
|
881
|
+
const account_id = $('f-id').value.trim();
|
|
882
|
+
const label = $('f-label').value.trim();
|
|
883
|
+
const plan = $('f-plan').value;
|
|
884
|
+
const color = $('f-color').value;
|
|
885
|
+
const monthly_cost_usd = parseFloat($('f-cost').value) || 0;
|
|
886
|
+
const window_token_limit = parseInt($('f-limit').value) || 0;
|
|
887
|
+
const daily_budget_usd = parseFloat($('f-budget').value) || 0;
|
|
888
|
+
const data_paths = Array.from($('f-paths').querySelectorAll('input')).map(i => i.value.trim()).filter(Boolean);
|
|
889
|
+
const projects = Array.from($('f-projects').querySelectorAll('.dynamic-row')).map(row => {
|
|
890
|
+
const inputs = row.querySelectorAll('input');
|
|
891
|
+
return {project_name: inputs[0].value.trim(), keywords: inputs[1].value.split(',').map(k => k.trim().toLowerCase()).filter(Boolean)};
|
|
892
|
+
}).filter(p => p.project_name);
|
|
893
|
+
|
|
894
|
+
if (!label) { showMsg('Display label is required', 'error'); return; }
|
|
895
|
+
if (!data_paths.length) { showMsg('At least one data path is required', 'error'); return; }
|
|
896
|
+
|
|
897
|
+
const isEdit = !!editingId;
|
|
898
|
+
const url = isEdit ? '/api/accounts/' + editingId : '/api/accounts';
|
|
899
|
+
const method = isEdit ? 'PUT' : 'POST';
|
|
900
|
+
const payload = {account_id, label, plan, color, monthly_cost_usd, window_token_limit, daily_budget_usd, data_paths};
|
|
901
|
+
|
|
902
|
+
fetch(url, {method, headers: authHeaders(), body: JSON.stringify(payload)})
|
|
903
|
+
.then(r => r.json())
|
|
904
|
+
.then(resp => {
|
|
905
|
+
if (!resp.success) { showMsg(resp.error || 'Save failed', 'error'); return; }
|
|
906
|
+
// Projects: diff existing against new
|
|
907
|
+
const aid = isEdit ? editingId : account_id;
|
|
908
|
+
return fetch('/api/accounts/' + aid + '/projects').then(r => r.json()).then(existing => {
|
|
909
|
+
const existingNames = new Set(existing.map(p => p.project_name));
|
|
910
|
+
const newNames = new Set(projects.map(p => p.project_name));
|
|
911
|
+
const ops = [];
|
|
912
|
+
for (const p of projects) {
|
|
913
|
+
if (!existingNames.has(p.project_name)) {
|
|
914
|
+
ops.push(fetch('/api/accounts/' + aid + '/projects', {method: 'POST', headers: authHeaders(), body: JSON.stringify(p)}));
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
for (const p of existing) {
|
|
918
|
+
if (!newNames.has(p.project_name)) {
|
|
919
|
+
ops.push(fetch('/api/accounts/' + aid + '/projects/' + encodeURIComponent(p.project_name), {method: 'DELETE', headers: authHeaders()}));
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
return Promise.all(ops);
|
|
923
|
+
}).then(() => {
|
|
924
|
+
showMsg((isEdit ? 'Updated ' : 'Created ') + label, 'success');
|
|
925
|
+
hideForm();
|
|
926
|
+
load();
|
|
927
|
+
});
|
|
928
|
+
}).catch(e => showMsg('Error: ' + e.message, 'error'));
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
$('add-btn').addEventListener('click', () => showForm(null));
|
|
932
|
+
load();
|
|
933
|
+
})();
|
|
934
|
+
</script>
|
|
935
|
+
</body>
|
|
936
|
+
</html>
|