sqlseed-web 0.2.4__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 (87) hide show
  1. sqlseed_web/AGENTS.md +106 -0
  2. sqlseed_web/__init__.py +24 -0
  3. sqlseed_web/__main__.py +8 -0
  4. sqlseed_web/_application.py +205 -0
  5. sqlseed_web/ai_settings.py +290 -0
  6. sqlseed_web/api.py +1102 -0
  7. sqlseed_web/app.py +26 -0
  8. sqlseed_web/managed_worker.py +184 -0
  9. sqlseed_web/operation_errors.py +42 -0
  10. sqlseed_web/plugin_environment.py +231 -0
  11. sqlseed_web/plugin_management.py +322 -0
  12. sqlseed_web/plugin_process.py +131 -0
  13. sqlseed_web/runtime_lifecycle.py +130 -0
  14. sqlseed_web/runtime_session.py +97 -0
  15. sqlseed_web/settings_environment.py +368 -0
  16. sqlseed_web/sqlite_target.py +86 -0
  17. sqlseed_web/state.py +348 -0
  18. sqlseed_web/static/AGENTS.md +180 -0
  19. sqlseed_web/static/ai.css +57 -0
  20. sqlseed_web/static/configs.css +50 -0
  21. sqlseed_web/static/date-picker.css +230 -0
  22. sqlseed_web/static/disclosure.css +149 -0
  23. sqlseed_web/static/graph-clarity.css +103 -0
  24. sqlseed_web/static/index.html +29 -0
  25. sqlseed_web/static/js/api.js +228 -0
  26. sqlseed_web/static/js/app.js +97 -0
  27. sqlseed_web/static/js/dropdown.js +432 -0
  28. sqlseed_web/static/js/filepicker.js +203 -0
  29. sqlseed_web/static/js/genform.js +1066 -0
  30. sqlseed_web/static/js/labels.js +195 -0
  31. sqlseed_web/static/js/pages/browse.js +209 -0
  32. sqlseed_web/static/js/pages/configs.js +424 -0
  33. sqlseed_web/static/js/pages/connect.js +332 -0
  34. sqlseed_web/static/js/pages/heal.js +395 -0
  35. sqlseed_web/static/js/pages/meta.js +110 -0
  36. sqlseed_web/static/js/pages/runs.js +293 -0
  37. sqlseed_web/static/js/pages/settings.js +942 -0
  38. sqlseed_web/static/js/pages/wizard.js +751 -0
  39. sqlseed_web/static/js/pages/workbench.js +3123 -0
  40. sqlseed_web/static/js/tree.js +126 -0
  41. sqlseed_web/static/js/workbench/ai-eligibility.js +33 -0
  42. sqlseed_web/static/js/workbench/ai-handoff.js +31 -0
  43. sqlseed_web/static/js/workbench/ai-stream.js +116 -0
  44. sqlseed_web/static/js/workbench/ai.js +888 -0
  45. sqlseed_web/static/js/workbench/connection.js +508 -0
  46. sqlseed_web/static/js/workbench/date-picker.js +445 -0
  47. sqlseed_web/static/js/workbench/dependency-view.js +119 -0
  48. sqlseed_web/static/js/workbench/editor.js +1236 -0
  49. sqlseed_web/static/js/workbench/focus.js +11 -0
  50. sqlseed_web/static/js/workbench/graph-layout.js +332 -0
  51. sqlseed_web/static/js/workbench/graph.js +970 -0
  52. sqlseed_web/static/js/workbench/guidance.js +29 -0
  53. sqlseed_web/static/js/workbench/model.js +124 -0
  54. sqlseed_web/static/js/workbench/plugin-management.js +512 -0
  55. sqlseed_web/static/js/workbench/preview-scroll-layout.js +94 -0
  56. sqlseed_web/static/js/workbench/preview.js +572 -0
  57. sqlseed_web/static/js/workbench/provider-guide.js +33 -0
  58. sqlseed_web/static/js/workbench/recovery.js +28 -0
  59. sqlseed_web/static/js/workbench/scroll-lock.js +26 -0
  60. sqlseed_web/static/js/workbench/session.js +174 -0
  61. sqlseed_web/static/js/workbench/table-data.js +186 -0
  62. sqlseed_web/static/js/workbench/ui.js +262 -0
  63. sqlseed_web/static/navigation.css +92 -0
  64. sqlseed_web/static/preview.css +29 -0
  65. sqlseed_web/static/runs.css +53 -0
  66. sqlseed_web/static/scrollbars.css +42 -0
  67. sqlseed_web/static/settings.css +108 -0
  68. sqlseed_web/static/style.css +3382 -0
  69. sqlseed_web/static/table-data.css +27 -0
  70. sqlseed_web/static/workbench.css +509 -0
  71. sqlseed_web/supervised_plugins.py +173 -0
  72. sqlseed_web/supervisor.py +238 -0
  73. sqlseed_web/workbench.py +381 -0
  74. sqlseed_web/workbench_ai.py +887 -0
  75. sqlseed_web/workbench_ai_relations.py +285 -0
  76. sqlseed_web/workbench_ai_stream.py +172 -0
  77. sqlseed_web/workbench_data.py +163 -0
  78. sqlseed_web/workbench_execution.py +199 -0
  79. sqlseed_web/workbench_runtime.py +1218 -0
  80. sqlseed_web/workbench_schema.py +277 -0
  81. sqlseed_web/workbench_store.py +458 -0
  82. sqlseed_web/worker_control.py +192 -0
  83. sqlseed_web-0.2.4.dist-info/METADATA +105 -0
  84. sqlseed_web-0.2.4.dist-info/RECORD +87 -0
  85. sqlseed_web-0.2.4.dist-info/WHEEL +4 -0
  86. sqlseed_web-0.2.4.dist-info/entry_points.txt +2 -0
  87. sqlseed_web-0.2.4.dist-info/licenses/LICENSE +679 -0
@@ -0,0 +1,293 @@
1
+ import { h, get } from '../api.js';
2
+ import { download, valueText } from '../workbench/ui.js';
3
+ import { remainingRun } from '../workbench/recovery.js';
4
+ import { openTableData } from '../workbench/table-data.js';
5
+ const labels = {
6
+ queued: '等待执行',
7
+ running: '生成中',
8
+ done: '生成成功',
9
+ error: '生成失败',
10
+ interrupted: '服务中断',
11
+ not_run: '未执行'
12
+ };
13
+ let root,
14
+ workspace,
15
+ list,
16
+ detail,
17
+ notice,
18
+ version = 0,
19
+ requestSequence = 0,
20
+ timer = null,
21
+ selectedId = null;
22
+ let dataViewer = null;
23
+ const action = (label, handler, attrs = {}) => h('button', {
24
+ type: 'button',
25
+ class: 'btn',
26
+ onclick: handler,
27
+ ...attrs
28
+ }, label);
29
+ export function render() {
30
+ version++;
31
+ selectedId = new URLSearchParams(location.hash.split('?')[1] || '').get('id');
32
+ notice = h('p', {
33
+ class: 'run-notice wb-notice',
34
+ role: 'status',
35
+ 'aria-live': 'polite'
36
+ });
37
+ list = h('aside', {
38
+ class: 'run-list',
39
+ 'aria-label': '运行列表'
40
+ });
41
+ detail = h('section', {
42
+ class: 'run-detail wb-run-detail'
43
+ }, h('p', {
44
+ class: 'empty'
45
+ }, '选择一条运行记录查看结果。'));
46
+ workspace = h('div', {
47
+ class: 'runs-workspace'
48
+ }, list, detail);
49
+ root = h('div', {
50
+ class: 'page runs-page'
51
+ }, h('header', {
52
+ class: 'heading'
53
+ }, h('div', {}, h('div', {
54
+ class: 'crumb'
55
+ }, 'sqlseed'), h('h1', {}, '运行记录'), h('p', {
56
+ class: 'subtitle'
57
+ }, '查看已提交的配置版本与逐表结果。')), h('div', {
58
+ class: 'heading-actions'
59
+ }, h('a', {
60
+ href: '#/workbench',
61
+ class: 'btn'
62
+ }, '返回工作台'), action('刷新', () => refresh(version)))), notice, workspace);
63
+ return root;
64
+ }
65
+ export function mount() {
66
+ return refresh(version);
67
+ }
68
+ export function unmount() {
69
+ version++;
70
+ clearTimeout(timer);
71
+ timer = null;
72
+ dataViewer?.close();
73
+ dataViewer = null;
74
+ }
75
+ async function refresh(expected) {
76
+ const sequence = ++requestSequence;
77
+ const current = () => expected === version && sequence === requestSequence;
78
+ clearTimeout(timer);
79
+ timer = null;
80
+ try {
81
+ const response = await get('/api/workbench/runs');
82
+ if (!current()) return;
83
+ const runs = Array.isArray(response) ? response : response.runs || [];
84
+ if (!runs.length) {
85
+ selectedId = null;
86
+ workspace.replaceChildren(h('section', {
87
+ class: 'run-empty',
88
+ role: 'status'
89
+ }, h('h2', {}, '还没有运行记录'), h('p', {}, '生成后会在这里保留配置版本、执行结果与实际写入数量'), h('a', {
90
+ href: '#/workbench',
91
+ class: 'btn primary'
92
+ }, '返回工作台')));
93
+ notice.textContent = '';
94
+ return;
95
+ }
96
+ workspace.replaceChildren(list, detail);
97
+ if (!selectedId && runs.length) selectedId = runs[0].id;
98
+ list.replaceChildren(...runs.map(run => action('', () => {
99
+ dataViewer?.close();
100
+ dataViewer = null;
101
+ selectedId = run.id;
102
+ refresh(version);
103
+ }, {
104
+ class: `run-card wb-run-card${run.id === selectedId ? ' active' : ''}`,
105
+ 'aria-pressed': String(run.id === selectedId)
106
+ })));
107
+ [...list.querySelectorAll('.wb-run-card')].forEach((card, i) => card.append(h('strong', {}, runs[i].name || runs[i].id), status(runs[i].status), h('small', {}, runTime(runs[i].created_at ?? runs[i].started_at)), h('small', {}, runs[i].target_label)));
108
+ if (selectedId) {
109
+ const run = await get(`/api/workbench/runs/${encodeURIComponent(selectedId)}`);
110
+ if (!current()) return;
111
+ drawRun(run);
112
+ }
113
+ notice.textContent = '';
114
+ if (runs.some(run => ['queued', 'running'].includes(run.status))) timer = setTimeout(() => refresh(expected), 1200);
115
+ } catch (error) {
116
+ if (!current()) return;
117
+ notice.textContent = `暂时无法读取记录:${error.message}。这不代表任务已失败。`;
118
+ timer = setTimeout(() => refresh(expected), 5000);
119
+ }
120
+ }
121
+ function status(value) {
122
+ return h('span', {
123
+ class: `run-status ${value}`
124
+ }, labels[value] || value || '状态待确认');
125
+ }
126
+ function runTime(value) {
127
+ if (value === undefined || value === null || value === '') return h('span', {}, '时间未知');
128
+ const timestamp = typeof value === 'number' ? value * 1000 : value;
129
+ const date = new Date(timestamp);
130
+ if (!Number.isFinite(date.getTime())) return h('span', {}, '时间未知');
131
+ return h('time', {
132
+ datetime: date.toISOString()
133
+ }, date.toLocaleString('zh-CN', {
134
+ year: 'numeric',
135
+ month: '2-digit',
136
+ day: '2-digit',
137
+ hour: '2-digit',
138
+ minute: '2-digit',
139
+ hourCycle: 'h23'
140
+ }));
141
+ }
142
+ function errorMessages(...values) {
143
+ return [...new Set(values.flatMap(value => Array.isArray(value) ? value : [value]).filter(value => value !== undefined && value !== null && value !== '').map(valueText))];
144
+ }
145
+ function knownCount(value) {
146
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0;
147
+ }
148
+ function tableResult(table) {
149
+ const errors = errorMessages(table.errors, table.error);
150
+ if (errors.length) return errors.join(';');
151
+ const descriptions = {
152
+ done: '生成成功,数据已提交',
153
+ error: '生成失败,请查看本次运行的错误详情',
154
+ queued: '等待前序表完成后执行',
155
+ running: '正在生成,提交数量统计中',
156
+ not_run: '前序任务未完成,本表未执行',
157
+ interrupted: '执行被中断,提交情况待核对'
158
+ };
159
+ return descriptions[table.status] || '结果待确认';
160
+ }
161
+ function viewTable(run, table) {
162
+ if (selectedId !== run.id || !root?.isConnected) return;
163
+ const expected = version;
164
+ dataViewer?.close();
165
+ dataViewer = openTableData({
166
+ runId: run.id,
167
+ table: table.name,
168
+ targetKey: run.target_key,
169
+ targetLabel: run.target_label,
170
+ isCurrent: () => version === expected && selectedId === run.id && Boolean(root?.isConnected)
171
+ });
172
+ }
173
+ function drawRun(run) {
174
+ const recovery = remainingRun(run);
175
+ const count = run.rows_inserted;
176
+ const replacement = run.execution?.mode === 'replace_selected';
177
+ const exact = knownCount(count) && run.row_counts_exact !== false && run.count_complete !== false;
178
+ const errors = errorMessages(run.errors, run.error);
179
+ const tables = Array.isArray(run.tables) ? run.tables : [];
180
+ const configured = Array.isArray(run.document?.tables) ? run.document.tables : [];
181
+ const plannedCount = table => table.requested_count ?? table.count ?? configured.find(item => item.name === table.name)?.count;
182
+ const planned = tables.length && tables.every(table => knownCount(plannedCount(table))) ? tables.reduce((total, table) => total + plannedCount(table), 0) : null;
183
+ function executionDescription() {
184
+ if (replacement) {
185
+ return `清空所选表后生成 · 自增计数${run.execution.reset_identity ? '重置' : '保留'}`;
186
+ } else {
187
+ return '追加数据 · 保留已有记录';
188
+ }
189
+ }
190
+ function committedCountMetric() {
191
+ if (knownCount(count)) {
192
+ return h('div', {}, h('strong', {}, `${exact ? '' : '至少 '}${count.toLocaleString()}`), ['queued', 'running'].includes(run.status) ? ' 行已确认提交' : ' 行已提交');
193
+ } else {
194
+ return h('div', {
195
+ class: 'run-metric-unknown'
196
+ }, '提交数量待核对');
197
+ }
198
+ }
199
+ function tableCommittedCount(table) {
200
+ if (table.status === 'running') {
201
+ return '统计中';
202
+ } else if (knownCount(table.rows_inserted)) {
203
+ return table.rows_inserted;
204
+ } else {
205
+ return '待核对';
206
+ }
207
+ }
208
+ function runRecoveryCard() {
209
+ if (run.status === 'error') {
210
+ return h('section', {
211
+ class: 'run-recovery wb-source-card',
212
+ 'aria-label': '失败后的下一步'
213
+ }, h('h3', {}, '接下来怎么处理'), h('p', {}, recovery.ok ? '已提交的数据会保留。新配置只包含未完成表的剩余行数;先修正规则、预览并检查依赖,再确认追加。' : recovery.reason), ...(recovery.ok ? [h('p', {
214
+ class: 'muted'
215
+ }, '已完成的父表改为引用已有数据。剩余配置不保证延续上次随机序列,也不能自动修复业务关系。'), h('a', {
216
+ href: `#/workbench?run=${encodeURIComponent(run.id)}&recover=remaining`,
217
+ class: 'btn primary'
218
+ }, '修正并生成剩余数据')] : []));
219
+ } else {
220
+ return null;
221
+ }
222
+ }
223
+ const content = [h('div', {
224
+ class: 'run-heading'
225
+ }, h('h2', {}, run.name || '生成任务'), status(run.status)), h('p', {
226
+ class: 'mono run-target'
227
+ }, run.target_label), h('p', {
228
+ class: 'muted run-identity'
229
+ }, `配置 v${run.revision ?? '—'} · ${run.id}`), h('p', {
230
+ class: 'run-execution'
231
+ }, executionDescription()), run.result?.rolled_back ? h('p', {
232
+ class: 'run-rollback',
233
+ role: 'status'
234
+ }, '本次清空和生成已回滚,原有数据已保留;本次没有新增已提交记录。') : null, replacement && run.status === 'running' ? h('p', {
235
+ class: 'muted'
236
+ }, '正在同一事务中清空和生成,全部成功后才确认提交。') : null, h('div', {
237
+ class: 'run-metrics'
238
+ }, h('div', {
239
+ class: 'run-total run-planned'
240
+ }, h('span', {
241
+ class: 'run-metric-label'
242
+ }, '计划生成'), h('div', {}, h('strong', {}, planned === null ? '未记录' : planned.toLocaleString()), planned === null ? '' : ' 行')), h('div', {
243
+ class: 'run-total run-committed'
244
+ }, h('span', {
245
+ class: 'run-metric-label'
246
+ }, '实际已提交'), committedCountMetric())), h('p', {
247
+ class: 'muted run-count-explanation'
248
+ }, '计划行数来自本次配置;已提交是本次新增并确认写入的记录,不包含数据库中原有的数据。'), !exact ? h('p', {
249
+ class: 'run-warning'
250
+ }, '无法确认最后一批的提交状态。这里保留已知数量,请核对数据库后再决定是否重新生成。') : null, h('div', {
251
+ class: 'run-table-scroll'
252
+ }, h('table', {
253
+ class: 'run-table'
254
+ }, h('thead', {}, h('tr', {}, ...['表', '状态', '计划行数', '已提交', '结果'].map(text => h('th', {}, text)))), h('tbody', {}, ...tables.map(table => h('tr', {}, h('td', {
255
+ class: 'mono'
256
+ }, table.name), h('td', {}, status(table.status)), h('td', {}, plannedCount(table) ?? '未记录'), h('td', {}, tableCommittedCount(table)), h('td', {
257
+ class: table.status === 'error' ? 'run-error' : ''
258
+ }, h('div', {
259
+ class: 'run-result-content'
260
+ }, h('span', {}, tableResult(table)), action('查看当前数据', () => viewTable(run, table), {
261
+ class: 'btn run-view-data',
262
+ disabled: ['queued', 'running'].includes(run.status),
263
+ title: ['queued', 'running'].includes(run.status) ? '运行结束后可查看数据库当前数据' : '查看数据库当前记录,包含已有数据'
264
+ })))))))), errors.length ? h('div', {
265
+ class: 'run-error',
266
+ role: 'alert'
267
+ }, ...errors.map(error => h('p', {}, error))) : null, runRecoveryCard(), h('details', {
268
+ class: 'run-snapshot'
269
+ }, h('summary', {}, '本次配置快照'), h('p', {
270
+ class: 'muted'
271
+ }, '这是提交时的固定版本,后续编辑不会改变本次运行。'), h('pre', {}, JSON.stringify(run.document, null, 2)), action('导出快照 JSON', () => download(`sqlseed-run-${run.id}.json`, JSON.stringify({
272
+ target_key: run.target_key,
273
+ schema_hash: run.schema_hash,
274
+ document: run.document,
275
+ execution: run.execution || {
276
+ mode: 'append',
277
+ reset_identity: false
278
+ },
279
+ plan_hash: run.plan_hash
280
+ }, null, 2)))), h('div', {
281
+ class: 'run-actions'
282
+ }, h('a', {
283
+ href: `#/workbench?run=${encodeURIComponent(run.id)}`,
284
+ class: 'btn'
285
+ }, '从此快照新建配置')), run.status === 'error' ? h('p', {
286
+ class: 'muted'
287
+ }, '从完整快照新建会复用全部计划行数,可能再次生成已经提交的数据。') : null, replacement ? h('p', {
288
+ class: 'muted'
289
+ }, '从快照新建只复用生成规则,默认追加;清空需在生成前重新选择并确认。') : null, ['queued', 'running'].includes(run.status) ? h('p', {
290
+ class: 'muted'
291
+ }, '任务由服务端执行,离开此页面不影响生成。') : null];
292
+ detail.replaceChildren(...content.filter(element => element !== null));
293
+ }