remote-coder 0.4.1__py3-none-any.whl

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 (78) hide show
  1. app/__init__.py +3 -0
  2. app/admin/__init__.py +0 -0
  3. app/admin/advanced_settings.py +88 -0
  4. app/admin/database_browser.py +301 -0
  5. app/admin/router.py +528 -0
  6. app/admin/static/i18n.js +401 -0
  7. app/admin/static/icons/advanced.svg +8 -0
  8. app/admin/static/icons/database.svg +5 -0
  9. app/admin/static/icons/download.svg +3 -0
  10. app/admin/static/icons/home.svg +4 -0
  11. app/admin/static/icons/logs.svg +3 -0
  12. app/admin/static/icons/projects.svg +5 -0
  13. app/admin/static/summary.js +73 -0
  14. app/admin/templates/admin.html +511 -0
  15. app/admin/templates/advanced.html +635 -0
  16. app/admin/templates/database.html +880 -0
  17. app/admin/templates/logs.html +686 -0
  18. app/admin/templates/projects.html +878 -0
  19. app/ai/__init__.py +0 -0
  20. app/ai/base.py +129 -0
  21. app/ai/claude.py +20 -0
  22. app/ai/codex.py +34 -0
  23. app/ai/factory.py +27 -0
  24. app/ai/gemini.py +20 -0
  25. app/ai/model_catalog.py +47 -0
  26. app/ai/usage.py +134 -0
  27. app/cli.py +238 -0
  28. app/config.py +130 -0
  29. app/git/__init__.py +0 -0
  30. app/git/ai_commit.py +88 -0
  31. app/git/branch_naming.py +21 -0
  32. app/git/commit_message.py +279 -0
  33. app/git/service.py +669 -0
  34. app/jobs/__init__.py +0 -0
  35. app/jobs/manager.py +770 -0
  36. app/jobs/schemas.py +116 -0
  37. app/jobs/store.py +334 -0
  38. app/main.py +265 -0
  39. app/models.py +20 -0
  40. app/monitoring/__init__.py +10 -0
  41. app/monitoring/code.py +161 -0
  42. app/monitoring/events.py +33 -0
  43. app/monitoring/git.py +103 -0
  44. app/monitoring/log_buffer.py +245 -0
  45. app/monitoring/memory.py +19 -0
  46. app/monitoring/model.py +598 -0
  47. app/projects/__init__.py +19 -0
  48. app/projects/registry.py +384 -0
  49. app/security/__init__.py +0 -0
  50. app/security/auth.py +19 -0
  51. app/system_startup.py +34 -0
  52. app/telegram/__init__.py +0 -0
  53. app/telegram/bot_instances.py +67 -0
  54. app/telegram/commands/__init__.py +64 -0
  55. app/telegram/commands/base.py +222 -0
  56. app/telegram/commands/branch.py +366 -0
  57. app/telegram/commands/clear_stop.py +221 -0
  58. app/telegram/commands/fix.py +219 -0
  59. app/telegram/commands/model.py +93 -0
  60. app/telegram/commands/monitor.py +185 -0
  61. app/telegram/commands/registry.py +110 -0
  62. app/telegram/commands/status.py +243 -0
  63. app/telegram/commands/system.py +201 -0
  64. app/telegram/confirmations.py +36 -0
  65. app/telegram/conversation.py +789 -0
  66. app/telegram/i18n.py +742 -0
  67. app/telegram/model_preferences.py +53 -0
  68. app/telegram/notifier.py +387 -0
  69. app/telegram/parser.py +267 -0
  70. app/telegram/webhook.py +988 -0
  71. app/telegram/webhook_registration.py +172 -0
  72. app/tunnel.py +104 -0
  73. remote_coder-0.4.1.dist-info/METADATA +520 -0
  74. remote_coder-0.4.1.dist-info/RECORD +78 -0
  75. remote_coder-0.4.1.dist-info/WHEEL +5 -0
  76. remote_coder-0.4.1.dist-info/entry_points.txt +2 -0
  77. remote_coder-0.4.1.dist-info/licenses/LICENSE +201 -0
  78. remote_coder-0.4.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,880 @@
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" />
6
+ <title data-i18n="title.database">Remote AI Coder - Data Browser</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0f1419;
10
+ --bg-elevated: #1a2332;
11
+ --surface: #243044;
12
+ --border: rgba(255, 255, 255, 0.08);
13
+ --text: #e8edf4;
14
+ --text-muted: #8b9cb3;
15
+ --accent: #3d9cf5;
16
+ --accent-dim: rgba(61, 156, 245, 0.15);
17
+ --success: #34c759;
18
+ --success-bg: rgba(52, 199, 89, 0.12);
19
+ --danger: #ff453a;
20
+ --danger-bg: rgba(255, 69, 58, 0.12);
21
+ --warning: #ffcc00;
22
+ --radius: 12px;
23
+ --radius-sm: 8px;
24
+ --shadow: 0 4px 24px rgba(0, 0, 0, 0.35);
25
+ --font: "Segoe UI", system-ui, -apple-system, sans-serif;
26
+ --focus: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent);
27
+ }
28
+
29
+ *, *::before, *::after { box-sizing: border-box; }
30
+
31
+ body {
32
+ margin: 0;
33
+ min-height: 100vh;
34
+ font-family: var(--font);
35
+ font-size: 15px;
36
+ line-height: 1.5;
37
+ color: var(--text);
38
+ background: radial-gradient(ellipse 120% 80% at 50% -20%, #1e3a5f 0%, var(--bg) 55%);
39
+ padding: 1.25rem 1rem 2.5rem;
40
+ }
41
+
42
+ .wrap { max-width: 72rem; margin: 0 auto; }
43
+
44
+ .app-header {
45
+ margin-bottom: 1.5rem;
46
+ padding: 1.25rem 1.35rem;
47
+ background: linear-gradient(135deg, var(--bg-elevated) 0%, var(--surface) 100%);
48
+ border: 1px solid var(--border);
49
+ border-radius: var(--radius);
50
+ box-shadow: var(--shadow);
51
+ }
52
+
53
+ .app-header h1 {
54
+ margin: 0 0 0.35rem;
55
+ font-size: 1.5rem;
56
+ font-weight: 700;
57
+ letter-spacing: -0.02em;
58
+ }
59
+
60
+ .app-header .tagline {
61
+ margin: 0;
62
+ color: var(--text-muted);
63
+ font-size: 0.9rem;
64
+ }
65
+
66
+ .localhost-pill {
67
+ display: inline-flex;
68
+ align-items: center;
69
+ gap: 0.35rem;
70
+ margin-top: 0.75rem;
71
+ padding: 0.35rem 0.65rem;
72
+ font-size: 0.8rem;
73
+ font-weight: 600;
74
+ color: var(--warning);
75
+ background: rgba(255, 204, 0, 0.1);
76
+ border: 1px solid rgba(255, 204, 0, 0.25);
77
+ border-radius: 999px;
78
+ }
79
+
80
+ .header-row {
81
+ display: flex;
82
+ flex-wrap: wrap;
83
+ align-items: flex-start;
84
+ justify-content: space-between;
85
+ gap: 1rem;
86
+ }
87
+
88
+ .header-icon-nav {
89
+ display: flex;
90
+ flex-shrink: 0;
91
+ gap: 0.35rem;
92
+ align-items: center;
93
+ }
94
+
95
+ .header-icon-link {
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: center;
99
+ width: 2.5rem;
100
+ height: 2.5rem;
101
+ border-radius: var(--radius-sm);
102
+ border: 1px solid var(--border);
103
+ background: rgba(0, 0, 0, 0.15);
104
+ transition: background 0.15s, border-color 0.15s;
105
+ }
106
+
107
+ .header-icon-link:hover {
108
+ background: #2d3d54;
109
+ border-color: rgba(255, 255, 255, 0.12);
110
+ }
111
+
112
+ .header-icon-link:focus-visible {
113
+ outline: none;
114
+ box-shadow: var(--focus);
115
+ }
116
+
117
+ .header-icon-link.is-current {
118
+ border-color: rgba(61, 156, 245, 0.45);
119
+ background: var(--accent-dim);
120
+ }
121
+
122
+ .header-icon-link img {
123
+ display: block;
124
+ width: 1.35rem;
125
+ height: 1.35rem;
126
+ }
127
+
128
+ #global-msg {
129
+ margin-bottom: 1rem;
130
+ padding: 0.65rem 1rem;
131
+ border-radius: var(--radius-sm);
132
+ font-size: 0.9rem;
133
+ display: none;
134
+ }
135
+
136
+ #global-msg.is-visible { display: block; }
137
+
138
+ #global-msg.msg-err {
139
+ color: #ffb4b0;
140
+ background: var(--danger-bg);
141
+ border: 1px solid rgba(255, 69, 58, 0.35);
142
+ }
143
+
144
+ section.card {
145
+ margin-bottom: 1.25rem;
146
+ padding: 1.25rem 1.35rem;
147
+ background: var(--bg-elevated);
148
+ border: 1px solid var(--border);
149
+ border-radius: var(--radius);
150
+ box-shadow: var(--shadow);
151
+ }
152
+
153
+ section.card h2 {
154
+ margin: 0 0 0.5rem;
155
+ font-size: 1.05rem;
156
+ font-weight: 700;
157
+ }
158
+
159
+ section.card > .lead {
160
+ margin: 0 0 1rem;
161
+ color: var(--text-muted);
162
+ font-size: 0.88rem;
163
+ }
164
+
165
+ .toolbar {
166
+ display: flex;
167
+ flex-wrap: wrap;
168
+ gap: 0.65rem 0.75rem;
169
+ align-items: flex-end;
170
+ margin-bottom: 0.85rem;
171
+ }
172
+
173
+ .field-inline {
174
+ display: flex;
175
+ flex-direction: column;
176
+ gap: 0.25rem;
177
+ min-width: 0;
178
+ }
179
+
180
+ .field-inline label {
181
+ font-size: 0.72rem;
182
+ font-weight: 600;
183
+ text-transform: uppercase;
184
+ letter-spacing: 0.04em;
185
+ color: var(--text-muted);
186
+ }
187
+
188
+ .field-inline input,
189
+ .field-inline select {
190
+ font-family: inherit;
191
+ font-size: 0.88rem;
192
+ padding: 0.45rem 0.55rem;
193
+ color: var(--text);
194
+ background: var(--bg);
195
+ border: 1px solid var(--border);
196
+ border-radius: var(--radius-sm);
197
+ min-width: 0;
198
+ }
199
+
200
+ .field-inline input:focus,
201
+ .field-inline select:focus {
202
+ outline: none;
203
+ border-color: var(--accent);
204
+ box-shadow: 0 0 0 3px var(--accent-dim);
205
+ }
206
+
207
+ .field-inline.narrow { width: 8rem; }
208
+ .field-inline.mid { width: 11rem; }
209
+
210
+ .field-inline.role-wrap.hidden { display: none; }
211
+
212
+ .btn-row-actions {
213
+ display: flex;
214
+ flex-wrap: wrap;
215
+ align-items: flex-end;
216
+ gap: 0.5rem;
217
+ }
218
+
219
+ .btn-icon {
220
+ display: inline-flex;
221
+ align-items: center;
222
+ justify-content: center;
223
+ padding: 0.45rem 0.6rem;
224
+ min-width: 2.5rem;
225
+ min-height: 2.35rem;
226
+ border-radius: var(--radius-sm);
227
+ border: 1px solid var(--border);
228
+ background: var(--surface);
229
+ cursor: pointer;
230
+ transition: background 0.15s, border-color 0.15s;
231
+ }
232
+
233
+ .btn-icon:hover {
234
+ background: #2d3d54;
235
+ border-color: rgba(255, 255, 255, 0.12);
236
+ }
237
+
238
+ .btn-icon:focus-visible {
239
+ outline: none;
240
+ box-shadow: var(--focus);
241
+ }
242
+
243
+ .btn-icon img {
244
+ display: block;
245
+ width: 1.15rem;
246
+ height: 1.15rem;
247
+ }
248
+
249
+ .btn-detail {
250
+ font-family: inherit;
251
+ font-size: 0.82rem;
252
+ font-weight: 600;
253
+ padding: 0.35rem 0.6rem;
254
+ border-radius: var(--radius-sm);
255
+ border: 1px solid var(--accent);
256
+ background: var(--accent-dim);
257
+ color: var(--accent);
258
+ cursor: pointer;
259
+ }
260
+
261
+ .btn-detail:hover {
262
+ background: rgba(61, 156, 245, 0.25);
263
+ }
264
+
265
+ dialog.text-detail-dlg {
266
+ border: 1px solid var(--border);
267
+ border-radius: var(--radius);
268
+ background: var(--bg-elevated);
269
+ color: var(--text);
270
+ max-width: min(52rem, 94vw);
271
+ width: 100%;
272
+ padding: 0;
273
+ box-shadow: var(--shadow);
274
+ }
275
+
276
+ dialog.text-detail-dlg::backdrop {
277
+ background: rgba(0, 0, 0, 0.55);
278
+ }
279
+
280
+ .text-detail-head {
281
+ display: flex;
282
+ flex-wrap: wrap;
283
+ align-items: center;
284
+ justify-content: space-between;
285
+ gap: 0.75rem;
286
+ padding: 0.85rem 1.1rem;
287
+ border-bottom: 1px solid var(--border);
288
+ }
289
+
290
+ .text-detail-head h2 {
291
+ margin: 0;
292
+ font-size: 1rem;
293
+ font-weight: 700;
294
+ }
295
+
296
+ .text-detail-body-wrap {
297
+ padding: 1rem 1.1rem;
298
+ max-height: min(70vh, 36rem);
299
+ overflow: auto;
300
+ }
301
+
302
+ #text-detail-body {
303
+ margin: 0;
304
+ white-space: pre-wrap;
305
+ word-break: break-word;
306
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
307
+ font-size: 0.85rem;
308
+ line-height: 1.45;
309
+ color: var(--text);
310
+ }
311
+
312
+ button {
313
+ font-family: inherit;
314
+ font-size: 0.82rem;
315
+ font-weight: 600;
316
+ padding: 0.45rem 0.75rem;
317
+ border-radius: var(--radius-sm);
318
+ border: 1px solid var(--border);
319
+ background: var(--surface);
320
+ color: var(--text);
321
+ cursor: pointer;
322
+ transition: background 0.15s, border-color 0.15s, opacity 0.15s;
323
+ }
324
+
325
+ button:hover:not(:disabled) {
326
+ background: #2d3d54;
327
+ border-color: rgba(255, 255, 255, 0.12);
328
+ }
329
+
330
+ button:focus-visible {
331
+ outline: none;
332
+ box-shadow: var(--focus);
333
+ }
334
+
335
+ button.btn-primary {
336
+ background: var(--accent);
337
+ border-color: transparent;
338
+ color: #0a1628;
339
+ }
340
+
341
+ button.btn-primary:hover:not(:disabled) {
342
+ background: #5aadf7;
343
+ }
344
+
345
+ button:disabled {
346
+ opacity: 0.45;
347
+ cursor: not-allowed;
348
+ }
349
+
350
+ .table-wrap {
351
+ overflow-x: auto;
352
+ margin-top: 0.75rem;
353
+ border-radius: var(--radius-sm);
354
+ border: 1px solid var(--border);
355
+ }
356
+
357
+ table.data {
358
+ width: 100%;
359
+ border-collapse: collapse;
360
+ font-size: 0.82rem;
361
+ }
362
+
363
+ table.data th,
364
+ table.data td {
365
+ padding: 0.5rem 0.6rem;
366
+ text-align: left;
367
+ vertical-align: top;
368
+ border-bottom: 1px solid var(--border);
369
+ }
370
+
371
+ table.data th {
372
+ font-weight: 600;
373
+ font-size: 0.7rem;
374
+ text-transform: uppercase;
375
+ letter-spacing: 0.04em;
376
+ color: var(--text-muted);
377
+ background: rgba(0, 0, 0, 0.25);
378
+ white-space: nowrap;
379
+ }
380
+
381
+ table.data tbody tr:hover td {
382
+ background: rgba(61, 156, 245, 0.04);
383
+ }
384
+
385
+ .pager {
386
+ display: flex;
387
+ flex-wrap: wrap;
388
+ align-items: center;
389
+ gap: 0.5rem 0.75rem;
390
+ margin-top: 0.85rem;
391
+ font-size: 0.88rem;
392
+ color: var(--text-muted);
393
+ }
394
+
395
+ .db-path {
396
+ font-size: 0.78rem;
397
+ color: var(--text-muted);
398
+ word-break: break-all;
399
+ font-family: ui-monospace, monospace;
400
+ }
401
+
402
+ .summary-grid {
403
+ display: grid;
404
+ grid-template-columns: repeat(auto-fill, minmax(10.5rem, 1fr));
405
+ gap: 0.75rem;
406
+ margin-bottom: 1.25rem;
407
+ }
408
+
409
+ .stat-card {
410
+ padding: 1rem 1.1rem;
411
+ background: var(--bg-elevated);
412
+ border: 1px solid var(--border);
413
+ border-radius: var(--radius-sm);
414
+ }
415
+
416
+ .stat-card .label {
417
+ margin: 0;
418
+ font-size: 0.72rem;
419
+ font-weight: 600;
420
+ text-transform: uppercase;
421
+ letter-spacing: 0.06em;
422
+ color: var(--text-muted);
423
+ }
424
+
425
+ .stat-card .value {
426
+ margin: 0.25rem 0 0;
427
+ font-size: 1.35rem;
428
+ font-weight: 700;
429
+ color: var(--text);
430
+ }
431
+
432
+ .stat-card .sub {
433
+ margin: 0.2rem 0 0;
434
+ font-size: 0.78rem;
435
+ color: var(--text-muted);
436
+ word-break: break-all;
437
+ }
438
+
439
+ @media (max-width: 640px) {
440
+ .app-header h1 { font-size: 1.25rem; }
441
+ }
442
+ </style>
443
+ </head>
444
+ <body>
445
+ <div class="wrap">
446
+ <header class="app-header">
447
+ <div class="header-row">
448
+ <div>
449
+ <h1 data-i18n="database.h1">Data browser</h1>
450
+ <p class="tagline" data-i18n="database.tagline">Read-only view of the conversation memory SQLite. Arbitrary SQL is never run.</p>
451
+ </div>
452
+ <nav class="header-icon-nav" aria-label="Page navigation" data-i18n-aria-label="nav.aria">
453
+ <a href="/" class="header-icon-link" title="Admin home" aria-label="Admin home" data-i18n-title="nav.home" data-i18n-aria-label="nav.home">
454
+ <img src="/admin-static/icons/home.svg" width="22" height="22" alt="" />
455
+ </a>
456
+ <a href="/projects" class="header-icon-link" title="Projects" aria-label="Projects" data-i18n-title="nav.projects" data-i18n-aria-label="nav.projects">
457
+ <img src="/admin-static/icons/projects.svg" width="22" height="22" alt="" />
458
+ </a>
459
+ <a href="/advanced" class="header-icon-link" title="Advanced settings" aria-label="Advanced settings" data-i18n-title="nav.advanced" data-i18n-aria-label="nav.advanced">
460
+ <img src="/admin-static/icons/advanced.svg" width="22" height="22" alt="" />
461
+ </a>
462
+ <a href="/logs" class="header-icon-link" title="Server logs" aria-label="Server logs" data-i18n-title="nav.logs" data-i18n-aria-label="nav.logs">
463
+ <img src="/admin-static/icons/logs.svg" width="22" height="22" alt="" />
464
+ </a>
465
+ <a href="/database" class="header-icon-link is-current" title="Data browser" aria-label="Data browser" data-i18n-title="nav.database" data-i18n-aria-label="nav.database">
466
+ <img src="/admin-static/icons/database.svg" width="22" height="22" alt="" />
467
+ </a>
468
+ </nav>
469
+ </div>
470
+ <span class="localhost-pill" aria-hidden="true" data-i18n="common.localOnly">Local only</span>
471
+ <p class="tagline" style="margin-top:0.65rem" data-i18n-html="common.localhostNote">This page is only available from <strong>127.0.0.1</strong>.</p>
472
+ </header>
473
+
474
+ <div id="global-msg" role="status" aria-live="polite"></div>
475
+
476
+ <div class="summary-grid" id="summary-grid" aria-label="Summary" data-i18n-aria-label="common.summaryAria"></div>
477
+
478
+ <section class="card" aria-labelledby="db-heading">
479
+ <h2 id="db-heading" data-i18n="database.tablesHeading">Tables</h2>
480
+ <p class="lead" data-i18n="database.lead">Tables and sort columns are restricted to a server whitelist.</p>
481
+ <p class="db-path" id="db-path-line"></p>
482
+
483
+ <div class="toolbar">
484
+ <div class="field-inline mid">
485
+ <label for="f-table" data-i18n="database.table">Table</label>
486
+ <select id="f-table" aria-label="Table" data-i18n-aria-label="database.tableAria"></select>
487
+ </div>
488
+ <div class="field-inline narrow">
489
+ <label for="f-sort" data-i18n="database.sort">Sort</label>
490
+ <select id="f-sort" aria-label="Sort column" data-i18n-aria-label="database.sortAria"></select>
491
+ </div>
492
+ <div class="field-inline narrow">
493
+ <label for="f-order" data-i18n="database.order">Order</label>
494
+ <select id="f-order" aria-label="Sort order" data-i18n-aria-label="database.orderAria">
495
+ <option value="desc" data-i18n="database.desc">Descending</option>
496
+ <option value="asc" data-i18n="database.asc">Ascending</option>
497
+ </select>
498
+ </div>
499
+ <div class="field-inline narrow">
500
+ <label for="f-limit" data-i18n="database.pageSize">Page size</label>
501
+ <select id="f-limit" aria-label="Page size" data-i18n-aria-label="database.pageSizeAria">
502
+ <option value="25">25</option>
503
+ <option value="50" selected>50</option>
504
+ <option value="100">100</option>
505
+ <option value="200">200</option>
506
+ </select>
507
+ </div>
508
+ <div class="field-inline mid">
509
+ <label for="f-project">project</label>
510
+ <select id="f-project" aria-label="Project filter" data-i18n-aria-label="database.projectAria"></select>
511
+ </div>
512
+ <div class="field-inline mid role-wrap" id="role-wrap">
513
+ <label for="f-role">role</label>
514
+ <select id="f-role" aria-label="Role filter" data-i18n-aria-label="database.roleAria"></select>
515
+ </div>
516
+ <div class="field-inline mid">
517
+ <label for="f-q" data-i18n="database.searchPartial">Search (partial match)</label>
518
+ <input id="f-q" type="text" autocomplete="off" />
519
+ </div>
520
+ <div class="btn-row-actions">
521
+ <button type="button" class="btn-primary" id="btn-load" data-i18n="database.btnLoad">Load</button>
522
+ <button type="button" class="btn-icon" id="btn-csv" title="Download CSV (current filters/sort, up to 50,000 rows)" aria-label="Download CSV" data-i18n-title="database.csvTitle" data-i18n-aria-label="database.csvAria">
523
+ <img src="/admin-static/icons/download.svg" width="20" height="20" alt="" />
524
+ </button>
525
+ </div>
526
+ </div>
527
+
528
+ <div class="pager">
529
+ <span id="pager-summary"></span>
530
+ <button type="button" id="btn-prev" disabled data-i18n="common.prev">Prev</button>
531
+ <button type="button" id="btn-next" disabled data-i18n="common.next">Next</button>
532
+ </div>
533
+
534
+ <div class="table-wrap">
535
+ <table class="data" id="data-table">
536
+ <thead id="data-thead"></thead>
537
+ <tbody id="data-tbody"></tbody>
538
+ </table>
539
+ </div>
540
+ </section>
541
+ </div>
542
+
543
+ <dialog class="text-detail-dlg" id="text-detail-modal" aria-labelledby="text-detail-title">
544
+ <div class="text-detail-head">
545
+ <h2 id="text-detail-title" data-i18n="database.textContent">Text content</h2>
546
+ <button type="button" id="text-detail-close" data-i18n="common.close">Close</button>
547
+ </div>
548
+ <div class="text-detail-body-wrap">
549
+ <pre id="text-detail-body"></pre>
550
+ </div>
551
+ </dialog>
552
+
553
+ <script src="/admin-static/i18n.js"></script>
554
+ <script src="/admin-static/summary.js"></script>
555
+ <script>
556
+ const $ = (s) => document.querySelector(s);
557
+ const globalMsg = $("#global-msg");
558
+ let tablesMeta = [];
559
+ let currentOffset = 0;
560
+ let lastTotal = 0;
561
+ let lastLimit = 50;
562
+ let lastRows = [];
563
+
564
+ function hiddenDisplayColumns(tableName, allCols) {
565
+ const hide = new Set(["created_at"]);
566
+ if (tableName === "conversation_entries") {
567
+ hide.add("message_id");
568
+ hide.add("reply_to_message_id");
569
+ } else if (tableName === "message_branch_links") {
570
+ hide.add("message_id");
571
+ }
572
+ return (allCols || []).filter((c) => !hide.has(c));
573
+ }
574
+
575
+ function filteredSortable(meta) {
576
+ if (!meta) return [];
577
+ const hide = new Set();
578
+ if (meta.name === "conversation_entries") {
579
+ hide.add("message_id");
580
+ hide.add("reply_to_message_id");
581
+ } else if (meta.name === "message_branch_links") {
582
+ hide.add("message_id");
583
+ }
584
+ return (meta.sortable || []).filter((c) => !hide.has(c));
585
+ }
586
+
587
+ function setGlobalMsg(kind, text) {
588
+ if (!text) {
589
+ globalMsg.textContent = "";
590
+ globalMsg.className = "";
591
+ return;
592
+ }
593
+ globalMsg.textContent = text;
594
+ globalMsg.className = "is-visible " + (kind === "err" ? "msg-err" : "");
595
+ }
596
+
597
+ function escapeHtml(s) {
598
+ return String(s).replace(/[&<>"']/g, (c) => ({
599
+ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;"
600
+ }[c]));
601
+ }
602
+
603
+ function syncRoleVisibility() {
604
+ const t = $("#f-table").value;
605
+ $("#role-wrap").classList.toggle("hidden", t !== "conversation_entries");
606
+ }
607
+
608
+ function fillSelectOptions(selectEl, values, placeholder) {
609
+ const cur = selectEl.value;
610
+ selectEl.innerHTML = "";
611
+ const allOpt = document.createElement("option");
612
+ allOpt.value = "";
613
+ allOpt.textContent = placeholder;
614
+ selectEl.appendChild(allOpt);
615
+ (values || []).forEach((v) => {
616
+ const o = document.createElement("option");
617
+ o.value = v;
618
+ o.textContent = v;
619
+ selectEl.appendChild(o);
620
+ });
621
+ const list = values || [];
622
+ if (cur && list.includes(cur)) selectEl.value = cur;
623
+ else selectEl.value = "";
624
+ }
625
+
626
+ async function loadFilterOptions() {
627
+ const table = $("#f-table").value;
628
+ const r = await fetch("/api/database/filter-options?table=" + encodeURIComponent(table));
629
+ const raw = await r.text();
630
+ if (!r.ok) {
631
+ let detail = raw || "Request failed (" + r.status + ")";
632
+ try {
633
+ const j = JSON.parse(raw);
634
+ if (j && typeof j.detail === "string") detail = j.detail;
635
+ } catch (_) { /* ignore */ }
636
+ throw new Error(detail);
637
+ }
638
+ const d = JSON.parse(raw);
639
+ fillSelectOptions($("#f-project"), d.projects || [], i18n.t("common.all"));
640
+ if ($("#f-table").value === "conversation_entries") {
641
+ fillSelectOptions($("#f-role"), d.roles || [], i18n.t("common.all"));
642
+ } else {
643
+ $("#f-role").innerHTML = "";
644
+ }
645
+ }
646
+
647
+ function fillSortOptions() {
648
+ const name = $("#f-table").value;
649
+ const meta = tablesMeta.find((x) => x.name === name);
650
+ const sel = $("#f-sort");
651
+ if (!meta) return;
652
+ const prev = sel.value;
653
+ sel.innerHTML = "";
654
+ const sortable = filteredSortable(meta);
655
+ sortable.forEach((col) => {
656
+ const o = document.createElement("option");
657
+ o.value = col;
658
+ o.textContent = col;
659
+ sel.appendChild(o);
660
+ });
661
+ const pick = sortable.includes(prev) ? prev : meta.default_sort;
662
+ sel.value = sortable.includes(pick) ? pick : sortable[0] || meta.default_sort;
663
+ }
664
+
665
+ async function loadTablesMeta() {
666
+ const r = await fetch("/api/database/tables");
667
+ const raw = await r.text();
668
+ if (!r.ok) {
669
+ let detail = raw || "Request failed (" + r.status + ")";
670
+ try {
671
+ const j = JSON.parse(raw);
672
+ if (j && typeof j.detail === "string") detail = j.detail;
673
+ } catch (_) { /* ignore */ }
674
+ throw new Error(detail);
675
+ }
676
+ const d = JSON.parse(raw);
677
+ tablesMeta = d.tables || [];
678
+ $("#db-path-line").textContent = d.db_exists
679
+ ? i18n.t("database.dbPrefix", { path: d.db_path })
680
+ : i18n.t("database.dbMissing", { path: d.db_path });
681
+ const sel = $("#f-table");
682
+ sel.innerHTML = "";
683
+ tablesMeta.forEach((t) => {
684
+ const o = document.createElement("option");
685
+ o.value = t.name;
686
+ o.textContent = i18n.tv(t.label) + " (" + t.name + ")";
687
+ sel.appendChild(o);
688
+ });
689
+ if (!tablesMeta.length) {
690
+ setGlobalMsg("err", i18n.t("database.noTableMeta"));
691
+ return;
692
+ }
693
+ syncRoleVisibility();
694
+ fillSortOptions();
695
+ await loadFilterOptions();
696
+ }
697
+
698
+ function buildRowsQuery() {
699
+ const params = new URLSearchParams();
700
+ params.set("table", $("#f-table").value);
701
+ const p = $("#f-project").value.trim();
702
+ if (p) params.set("project", p);
703
+ if ($("#f-table").value === "conversation_entries") {
704
+ const role = $("#f-role").value.trim();
705
+ if (role) params.set("role", role);
706
+ }
707
+ const q = $("#f-q").value.trim();
708
+ if (q) params.set("q", q);
709
+ params.set("sort", $("#f-sort").value);
710
+ params.set("order", $("#f-order").value);
711
+ lastLimit = Number($("#f-limit").value) || 50;
712
+ params.set("limit", String(lastLimit));
713
+ params.set("offset", String(currentOffset));
714
+ return params.toString();
715
+ }
716
+
717
+ function buildExportQuery() {
718
+ const params = new URLSearchParams();
719
+ params.set("table", $("#f-table").value);
720
+ const p = $("#f-project").value.trim();
721
+ if (p) params.set("project", p);
722
+ if ($("#f-table").value === "conversation_entries") {
723
+ const role = $("#f-role").value.trim();
724
+ if (role) params.set("role", role);
725
+ }
726
+ const q = $("#f-q").value.trim();
727
+ if (q) params.set("q", q);
728
+ params.set("sort", $("#f-sort").value);
729
+ params.set("order", $("#f-order").value);
730
+ params.set("max_rows", "50000");
731
+ return params.toString();
732
+ }
733
+
734
+ function openTextDetail(text) {
735
+ const dlg = $("#text-detail-modal");
736
+ $("#text-detail-body").textContent = text;
737
+ if (dlg.showModal) dlg.showModal();
738
+ }
739
+
740
+ function renderTable(d) {
741
+ lastRows = d.rows || [];
742
+ const tableName = d.table || $("#f-table").value;
743
+ const allCols = d.columns || [];
744
+ const cols = hiddenDisplayColumns(tableName, allCols);
745
+ const thead = $("#data-thead");
746
+ const tbody = $("#data-tbody");
747
+ thead.innerHTML = "<tr>" + cols.map((c) => "<th>" + escapeHtml(c) + "</th>").join("") + "</tr>";
748
+ if (!lastRows.length) {
749
+ tbody.innerHTML =
750
+ '<tr><td colspan="' +
751
+ Math.max(cols.length, 1) +
752
+ '" style="color:var(--text-muted);text-align:center;padding:1.5rem">' + escapeHtml(i18n.t("database.noRows")) + "</td></tr>";
753
+ return;
754
+ }
755
+ tbody.innerHTML = lastRows
756
+ .map((row, rowIdx) => {
757
+ return (
758
+ "<tr>" +
759
+ cols
760
+ .map((c) => {
761
+ if (c === "text") {
762
+ return (
763
+ '<td><button type="button" class="btn-detail" data-row="' +
764
+ rowIdx +
765
+ '">' + escapeHtml(i18n.t("database.viewDetail")) + "</button></td>"
766
+ );
767
+ }
768
+ const v = row[c];
769
+ const s = v === null || v === undefined ? "" : String(v);
770
+ return "<td>" + escapeHtml(s) + "</td>";
771
+ })
772
+ .join("") +
773
+ "</tr>"
774
+ );
775
+ })
776
+ .join("");
777
+ tbody.querySelectorAll("button.btn-detail").forEach((btn) => {
778
+ btn.addEventListener("click", () => {
779
+ const idx = Number(btn.getAttribute("data-row"));
780
+ const raw = lastRows[idx];
781
+ const t = raw && raw.text !== undefined && raw.text !== null ? String(raw.text) : "";
782
+ openTextDetail(t);
783
+ });
784
+ });
785
+ }
786
+
787
+ function updatePager(d) {
788
+ lastTotal = Number(d.total) || 0;
789
+ const from = lastTotal === 0 ? 0 : currentOffset + 1;
790
+ const to = Math.min(currentOffset + (d.rows || []).length, lastTotal);
791
+ $("#pager-summary").textContent = i18n.t("database.pagerSummary", {
792
+ total: lastTotal,
793
+ from: from,
794
+ to: to,
795
+ limit: lastLimit,
796
+ offset: currentOffset,
797
+ });
798
+ $("#btn-prev").disabled = currentOffset <= 0;
799
+ $("#btn-next").disabled = currentOffset + lastLimit >= lastTotal;
800
+ }
801
+
802
+ async function loadRows() {
803
+ setGlobalMsg("", "");
804
+ const r = await fetch("/api/database/rows?" + buildRowsQuery());
805
+ const raw = await r.text();
806
+ if (!r.ok) {
807
+ let detail = raw || "Request failed (" + r.status + ")";
808
+ try {
809
+ const j = JSON.parse(raw);
810
+ if (j && typeof j.detail === "string") detail = j.detail;
811
+ } catch (_) { /* ignore */ }
812
+ throw new Error(detail);
813
+ }
814
+ const d = JSON.parse(raw);
815
+ renderTable(d);
816
+ updatePager(d);
817
+ }
818
+
819
+ $("#f-table").addEventListener("change", async () => {
820
+ syncRoleVisibility();
821
+ fillSortOptions();
822
+ currentOffset = 0;
823
+ try {
824
+ await loadFilterOptions();
825
+ } catch (e) {
826
+ setGlobalMsg("err", String(e));
827
+ }
828
+ });
829
+
830
+ $("#text-detail-close").addEventListener("click", () => {
831
+ $("#text-detail-modal").close();
832
+ });
833
+
834
+ $("#text-detail-modal").addEventListener("click", (e) => {
835
+ if (e.target === $("#text-detail-modal")) $("#text-detail-modal").close();
836
+ });
837
+
838
+ $("#btn-csv").addEventListener("click", () => {
839
+ window.location.assign("/api/database/export.csv?" + buildExportQuery());
840
+ });
841
+
842
+ $("#btn-load").addEventListener("click", async () => {
843
+ currentOffset = 0;
844
+ try {
845
+ await loadRows();
846
+ } catch (e) {
847
+ setGlobalMsg("err", String(e));
848
+ }
849
+ });
850
+
851
+ $("#btn-prev").addEventListener("click", async () => {
852
+ currentOffset = Math.max(0, currentOffset - lastLimit);
853
+ try {
854
+ await loadRows();
855
+ } catch (e) {
856
+ setGlobalMsg("err", String(e));
857
+ }
858
+ });
859
+
860
+ $("#btn-next").addEventListener("click", async () => {
861
+ currentOffset = currentOffset + lastLimit;
862
+ try {
863
+ await loadRows();
864
+ } catch (e) {
865
+ setGlobalMsg("err", String(e));
866
+ }
867
+ });
868
+
869
+ (async function init() {
870
+ adminSummary.loadSummaryGrid();
871
+ try {
872
+ await loadTablesMeta();
873
+ await loadRows();
874
+ } catch (e) {
875
+ setGlobalMsg("err", String(e));
876
+ }
877
+ })();
878
+ </script>
879
+ </body>
880
+ </html>