@njeybe/nativ 2.2.0 → 2.3.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.
- package/CHANGELOG.md +24 -1
- package/README.md +15 -0
- package/dist/runner/agent-supervisor.d.ts +5 -3
- package/dist/runner/agent-supervisor.js +17 -12
- package/dist/server/studio-server.js +1 -130
- package/dist/server/studio-ui/document.d.ts +3 -0
- package/dist/server/studio-ui/document.js +22 -0
- package/dist/server/studio-ui/pages/benchmarks/index.d.ts +2 -0
- package/dist/server/studio-ui/pages/benchmarks/index.js +11 -0
- package/dist/server/studio-ui/pages/benchmarks/script.d.ts +1 -0
- package/dist/server/studio-ui/pages/benchmarks/script.js +104 -0
- package/dist/server/studio-ui/pages/benchmarks/styles.d.ts +1 -0
- package/dist/server/studio-ui/pages/benchmarks/styles.js +30 -0
- package/dist/server/studio-ui/pages/database/browser.d.ts +1 -0
- package/dist/server/studio-ui/pages/database/browser.js +326 -0
- package/dist/server/studio-ui/pages/database/connection.d.ts +1 -0
- package/dist/server/studio-ui/pages/database/connection.js +158 -0
- package/dist/server/studio-ui/pages/database/data-shell.d.ts +1 -0
- package/dist/server/studio-ui/pages/database/data-shell.js +165 -0
- package/dist/server/studio-ui/pages/database/drift.d.ts +1 -0
- package/dist/server/studio-ui/pages/database/drift.js +301 -0
- package/dist/server/studio-ui/pages/database/events.d.ts +1 -0
- package/dist/server/studio-ui/pages/database/events.js +231 -0
- package/dist/server/studio-ui/pages/database/explorer.d.ts +1 -0
- package/dist/server/studio-ui/pages/database/explorer.js +183 -0
- package/dist/server/studio-ui/pages/database/index.d.ts +2 -0
- package/dist/server/studio-ui/pages/database/index.js +28 -0
- package/dist/server/studio-ui/pages/database/markup.d.ts +1 -0
- package/dist/server/studio-ui/pages/database/markup.js +32 -0
- package/dist/server/studio-ui/pages/database/state.d.ts +2 -0
- package/dist/server/studio-ui/pages/database/state.js +57 -0
- package/dist/server/studio-ui/pages/database/styles.d.ts +1 -0
- package/dist/server/studio-ui/pages/database/styles.js +118 -0
- package/dist/server/studio-ui/pages/flow/index.d.ts +2 -0
- package/dist/server/studio-ui/pages/flow/index.js +11 -0
- package/dist/server/studio-ui/pages/flow/layout.d.ts +1 -0
- package/dist/server/studio-ui/pages/flow/layout.js +105 -0
- package/dist/server/studio-ui/pages/flow/plan-view.d.ts +1 -0
- package/dist/server/studio-ui/pages/flow/plan-view.js +67 -0
- package/dist/server/studio-ui/pages/flow/script.d.ts +1 -0
- package/dist/server/studio-ui/pages/flow/script.js +61 -0
- package/dist/server/studio-ui/pages/flow/styles.d.ts +1 -0
- package/dist/server/studio-ui/pages/flow/styles.js +59 -0
- package/dist/server/studio-ui/pages/flow/timeline-model.d.ts +1 -0
- package/dist/server/studio-ui/pages/flow/timeline-model.js +95 -0
- package/dist/server/studio-ui/pages/flow/timeline-view.d.ts +1 -0
- package/dist/server/studio-ui/pages/flow/timeline-view.js +92 -0
- package/dist/server/studio-ui/pages/home/index.d.ts +2 -0
- package/dist/server/studio-ui/pages/home/index.js +11 -0
- package/dist/server/studio-ui/pages/home/script.d.ts +1 -0
- package/dist/server/studio-ui/pages/home/script.js +160 -0
- package/dist/server/studio-ui/pages/home/styles.d.ts +1 -0
- package/dist/server/studio-ui/pages/home/styles.js +48 -0
- package/dist/server/studio-ui/pages/index.d.ts +2 -0
- package/dist/server/studio-ui/pages/index.js +17 -0
- package/dist/server/studio-ui/pages/tasks/index.d.ts +2 -0
- package/dist/server/studio-ui/pages/tasks/index.js +11 -0
- package/dist/server/studio-ui/pages/tasks/script.d.ts +1 -0
- package/dist/server/studio-ui/pages/tasks/script.js +96 -0
- package/dist/server/studio-ui/pages/tasks/styles.d.ts +1 -0
- package/dist/server/studio-ui/pages/tasks/styles.js +37 -0
- package/dist/server/studio-ui/pages/team/index.d.ts +2 -0
- package/dist/server/studio-ui/pages/team/index.js +11 -0
- package/dist/server/studio-ui/pages/team/script.d.ts +1 -0
- package/dist/server/studio-ui/pages/team/script.js +112 -0
- package/dist/server/studio-ui/pages/team/styles.d.ts +1 -0
- package/dist/server/studio-ui/pages/team/styles.js +22 -0
- package/dist/server/studio-ui/pages/types.d.ts +13 -0
- package/dist/server/studio-ui/pages/types.js +1 -0
- package/dist/server/studio-ui/pages/worktrees/index.d.ts +2 -0
- package/dist/server/studio-ui/pages/worktrees/index.js +11 -0
- package/dist/server/studio-ui/pages/worktrees/script.d.ts +1 -0
- package/dist/server/studio-ui/pages/worktrees/script.js +104 -0
- package/dist/server/studio-ui/pages/worktrees/styles.d.ts +1 -0
- package/dist/server/studio-ui/pages/worktrees/styles.js +15 -0
- package/dist/server/studio-ui/shell/actions.d.ts +1 -0
- package/dist/server/studio-ui/shell/actions.js +49 -0
- package/dist/server/studio-ui/shell/boot.d.ts +2 -0
- package/dist/server/studio-ui/shell/boot.js +13 -0
- package/dist/server/studio-ui/shell/console.d.ts +1 -0
- package/dist/server/studio-ui/shell/console.js +194 -0
- package/dist/server/studio-ui/shell/core.d.ts +1 -0
- package/dist/server/studio-ui/shell/core.js +50 -0
- package/dist/server/studio-ui/shell/detail-view.d.ts +1 -0
- package/dist/server/studio-ui/shell/detail-view.js +80 -0
- package/dist/server/studio-ui/shell/detail.d.ts +1 -0
- package/dist/server/studio-ui/shell/detail.js +83 -0
- package/dist/server/studio-ui/shell/dispatch.d.ts +1 -0
- package/dist/server/studio-ui/shell/dispatch.js +150 -0
- package/dist/server/studio-ui/shell/events-tools.d.ts +1 -0
- package/dist/server/studio-ui/shell/events-tools.js +94 -0
- package/dist/server/studio-ui/shell/format.d.ts +1 -0
- package/dist/server/studio-ui/shell/format.js +151 -0
- package/dist/server/studio-ui/shell/icons.d.ts +2 -0
- package/dist/server/studio-ui/shell/icons.js +20 -0
- package/dist/server/studio-ui/shell/markup-head.d.ts +1 -0
- package/dist/server/studio-ui/shell/markup-head.js +43 -0
- package/dist/server/studio-ui/shell/markup-panels.d.ts +2 -0
- package/dist/server/studio-ui/shell/markup-panels.js +7 -0
- package/dist/server/studio-ui/shell/markup-tools.d.ts +1 -0
- package/dist/server/studio-ui/shell/markup-tools.js +222 -0
- package/dist/server/studio-ui/shell/model.d.ts +1 -0
- package/dist/server/studio-ui/shell/model.js +192 -0
- package/dist/server/studio-ui/shell/paint.d.ts +1 -0
- package/dist/server/studio-ui/shell/paint.js +154 -0
- package/dist/server/studio-ui/shell/palette-view.d.ts +1 -0
- package/dist/server/studio-ui/shell/palette-view.js +60 -0
- package/dist/server/studio-ui/shell/palette.d.ts +1 -0
- package/dist/server/studio-ui/shell/palette.js +80 -0
- package/dist/server/studio-ui/shell/pipeline-state.d.ts +1 -0
- package/dist/server/studio-ui/shell/pipeline-state.js +152 -0
- package/dist/server/studio-ui/shell/proposals.d.ts +1 -0
- package/dist/server/studio-ui/shell/proposals.js +197 -0
- package/dist/server/studio-ui/shell/pure.d.ts +3 -0
- package/dist/server/studio-ui/shell/pure.js +9 -0
- package/dist/server/studio-ui/shell/router.d.ts +1 -0
- package/dist/server/studio-ui/shell/router.js +95 -0
- package/dist/server/studio-ui/shell/sidebar-view.d.ts +1 -0
- package/dist/server/studio-ui/shell/sidebar-view.js +38 -0
- package/dist/server/studio-ui/shell/sync.d.ts +1 -0
- package/dist/server/studio-ui/shell/sync.js +120 -0
- package/dist/server/studio-ui/shell/toasts-format.d.ts +1 -0
- package/dist/server/studio-ui/shell/toasts-format.js +89 -0
- package/dist/server/studio-ui/shell/triage.d.ts +1 -0
- package/dist/server/studio-ui/shell/triage.js +210 -0
- package/dist/server/studio-ui/styles/console.d.ts +1 -0
- package/dist/server/studio-ui/styles/console.js +83 -0
- package/dist/server/studio-ui/styles/dialogs.d.ts +1 -0
- package/dist/server/studio-ui/styles/dialogs.js +118 -0
- package/dist/server/studio-ui/styles/responsive.d.ts +1 -0
- package/dist/server/studio-ui/styles/responsive.js +28 -0
- package/dist/server/studio-ui/styles/shell.d.ts +1 -0
- package/dist/server/studio-ui/styles/shell.js +117 -0
- package/dist/server/studio-ui/styles/strategist.d.ts +1 -0
- package/dist/server/studio-ui/styles/strategist.js +77 -0
- package/dist/server/studio-ui/styles/tokens-base.d.ts +1 -0
- package/dist/server/studio-ui/styles/tokens-base.js +131 -0
- package/dist/server/studio-ui.d.ts +1 -10
- package/dist/server/studio-ui.js +63 -5314
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// Database Studio live data browser.
|
|
2
|
+
export const databaseBrowserScript = String.raw ` // ─── Database Studio: live data browser ────────────────────────────────────
|
|
3
|
+
function renderDataCell(val, colName) {
|
|
4
|
+
if (val === null || val === undefined) return '<td class="mono muted">NULL</td>';
|
|
5
|
+
if (typeof val === 'boolean') return '<td class="mono"><b>' + (val ? 'TRUE' : 'FALSE') + '</b></td>';
|
|
6
|
+
if (typeof val === 'string') {
|
|
7
|
+
var isImgUrl = /^https?:\/\//i.test(val) && /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)/i.test(val);
|
|
8
|
+
var isDataImg = /^data:image\//i.test(val);
|
|
9
|
+
if (isImgUrl || isDataImg) {
|
|
10
|
+
return '<td><button type="button" class="cell-preview-btn" data-action="preview-url" data-url="' + esc(val) + '">Preview Image</button> <span class="mono muted" style="font-size:11px">' + esc(val.length > 28 ? val.slice(0, 28) + '…' : val) + '</span></td>';
|
|
11
|
+
}
|
|
12
|
+
var displayStr = val.length > 50 ? val.slice(0, 50) + '…' : val;
|
|
13
|
+
return '<td class="mono" title="' + esc(val) + '">' + esc(displayStr) + '</td>';
|
|
14
|
+
}
|
|
15
|
+
if (typeof val === 'object') {
|
|
16
|
+
var json = JSON.stringify(val);
|
|
17
|
+
var display = json.length > 40 ? json.slice(0, 40) + '…' : json;
|
|
18
|
+
return '<td class="mono cell-expandable" title="Click to expand JSON" data-action="view-json" data-raw="' + esc(json) + '">' + esc(display) + '</td>';
|
|
19
|
+
}
|
|
20
|
+
return '<td class="mono">' + esc(String(val)) + '</td>';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function renderData() {
|
|
24
|
+
var activeEnv = state.envMode === 'prod' ? 'prod' : 'dev';
|
|
25
|
+
state.dataEnv = activeEnv;
|
|
26
|
+
var s = state.status && state.status[activeEnv];
|
|
27
|
+
if (!s || !s.connected) {
|
|
28
|
+
return emptyState((activeEnv === 'dev' ? 'Dev / Staging' : 'Production') + ' database not connected',
|
|
29
|
+
'Please connect the ' + (activeEnv === 'dev' ? 'Dev' : 'Prod') + ' database in Connection Settings to browse and edit live data.');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
var tables = state.schema[activeEnv + 'Tables'] || [];
|
|
33
|
+
if (!tables.length) {
|
|
34
|
+
return '<div class="empty card"><p>No ' + entityNoun(s, true).toLowerCase() + ' found in this database.</p></div>';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!state.dataEntity || !tables.some(function (t) { return t.name === state.dataEntity; })) {
|
|
38
|
+
state.dataEntity = tables[0].name;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
var currentTable = tables.find(function (t) { return t.name === state.dataEntity; }) || tables[0];
|
|
42
|
+
var isColl = isCollection(currentTable);
|
|
43
|
+
|
|
44
|
+
var entityOptions = tables.map(function (t) {
|
|
45
|
+
var sel = t.name === state.dataEntity ? ' selected' : '';
|
|
46
|
+
return '<option value="' + esc(t.name) + '"' + sel + '>' + esc(t.name) + (isCollection(t) ? ' (collection)' : '') + '</option>';
|
|
47
|
+
}).join('');
|
|
48
|
+
|
|
49
|
+
var limitOptions = [25, 50, 100].map(function (n) {
|
|
50
|
+
var sel = state.dataLimit === n ? ' selected' : '';
|
|
51
|
+
return '<option value="' + n + '"' + sel + '>' + n + ' / page</option>';
|
|
52
|
+
}).join('');
|
|
53
|
+
|
|
54
|
+
var prodBannerHtml = activeEnv === 'prod'
|
|
55
|
+
? '<div class="prod-banner"><span><b>Production Safeguard Active:</b> Operating on live production data. All mutations require strict challenge phrase confirmation and are recorded to .nativ/prod_audit.log.</span><span class="badge badge-prod">PROD</span></div>'
|
|
56
|
+
: '';
|
|
57
|
+
|
|
58
|
+
var toolbar = '<div class="data-toolbar">' +
|
|
59
|
+
'<div class="data-controls">' +
|
|
60
|
+
'<select class="input" id="data-entity-select" aria-label="Table or collection" style="min-width:180px;font-weight:600">' + entityOptions + '</select>' +
|
|
61
|
+
'<input class="input" id="data-search" type="search" aria-label="Search records" placeholder="Search records in ' + esc(state.dataEntity) + '…" value="' + esc(state.dataSearch) + '" style="min-width:200px">' +
|
|
62
|
+
'<select class="input" id="data-limit-select" aria-label="Rows per page">' + limitOptions + '</select>' +
|
|
63
|
+
'<button class="btn" type="button" id="btn-data-refresh">Refresh</button>' +
|
|
64
|
+
'</div>' +
|
|
65
|
+
'<button class="btn primary" type="button" id="btn-insert-record">+ Insert ' + (isColl ? 'Document' : 'Record') + '</button>' +
|
|
66
|
+
'</div>';
|
|
67
|
+
|
|
68
|
+
if (state.dataLoading) {
|
|
69
|
+
return prodBannerHtml + toolbar + '<div class="card" style="padding:48px 24px;text-align:center"><div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div></div>';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (state.dataError) {
|
|
73
|
+
return prodBannerHtml + toolbar + '<div class="alert-danger" style="margin-top:12px"><strong>Query error:</strong> ' + esc(state.dataError) + '</div>';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
var res = state.dataResult;
|
|
77
|
+
if (!res || !res.rows || res.entity !== state.dataEntity) {
|
|
78
|
+
setTimeout(function () { fetchDataRecords(); }, 10);
|
|
79
|
+
return prodBannerHtml + toolbar + '<div class="card" style="padding:48px 24px;text-align:center"><p class="muted">Loading records for ' + esc(state.dataEntity) + '…</p></div>';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
var pk = res.primaryKey || 'id';
|
|
83
|
+
var schemaCols = currentTable.columns.map(function (c) { return c.name; });
|
|
84
|
+
var allKeys = new Set(schemaCols);
|
|
85
|
+
res.rows.forEach(function (r) { Object.keys(r).forEach(function (k) { allKeys.add(k); }); });
|
|
86
|
+
var displayCols = [pk];
|
|
87
|
+
allKeys.forEach(function (k) { if (k !== pk) displayCols.push(k); });
|
|
88
|
+
|
|
89
|
+
var thead = '<tr><th class="th-actions" style="width:115px">Actions</th>' + displayCols.map(function (col) {
|
|
90
|
+
var isSorted = state.dataSort === col;
|
|
91
|
+
var sortIcon = isSorted ? (state.dataOrder === 'desc' ? ' ▼' : ' ▲') : '';
|
|
92
|
+
return '<th class="sortable" data-sort-col="' + esc(col) + '">' + esc(col) +
|
|
93
|
+
(col === pk ? ' <span class="badge badge-pk">PK</span>' : '') +
|
|
94
|
+
'<span class="th-sort-icon">' + sortIcon + '</span></th>';
|
|
95
|
+
}).join('') + '</tr>';
|
|
96
|
+
|
|
97
|
+
var tbody = '';
|
|
98
|
+
if (res.rows.length === 0) {
|
|
99
|
+
tbody = '<tr><td colspan="' + (displayCols.length + 1) + '" style="text-align:center;padding:32px;color:var(--muted)">No records found' +
|
|
100
|
+
(state.dataSearch ? ' matching "' + esc(state.dataSearch) + '"' : '') + '.</td></tr>';
|
|
101
|
+
} else {
|
|
102
|
+
tbody = res.rows.map(function (row, rowIdx) {
|
|
103
|
+
var actions = '<td class="td-actions">' +
|
|
104
|
+
'<button class="btn small" type="button" data-action="edit-record" data-row="' + rowIdx + '">Edit</button>' +
|
|
105
|
+
'<button class="btn small btn-danger" type="button" data-action="delete-record" data-row="' + rowIdx + '">Delete</button>' +
|
|
106
|
+
'</td>';
|
|
107
|
+
|
|
108
|
+
var cells = displayCols.map(function (c) {
|
|
109
|
+
return renderDataCell(row[c], c);
|
|
110
|
+
}).join('');
|
|
111
|
+
|
|
112
|
+
return '<tr>' + actions + cells + '</tr>';
|
|
113
|
+
}).join('');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
var start = res.totalCount === 0 ? 0 : (res.page - 1) * res.limit + 1;
|
|
117
|
+
var end = Math.min(res.page * res.limit, res.totalCount);
|
|
118
|
+
var pagination = '<div class="pagination-bar">' +
|
|
119
|
+
'<div>Showing ' + start + '–' + end + ' of ' + res.totalCount + ' ' + (isColl ? 'documents' : 'records') + '</div>' +
|
|
120
|
+
'<div class="pagination-ctrls">' +
|
|
121
|
+
'<button class="btn small" type="button" id="btn-page-prev"' + (res.page <= 1 ? ' disabled' : '') + '>Previous</button>' +
|
|
122
|
+
'<span>Page ' + res.page + ' of ' + Math.max(1, res.totalPages) + '</span>' +
|
|
123
|
+
'<button class="btn small" type="button" id="btn-page-next"' + (res.page >= res.totalPages ? ' disabled' : '') + '>Next</button>' +
|
|
124
|
+
'</div>' +
|
|
125
|
+
'</div>';
|
|
126
|
+
|
|
127
|
+
return prodBannerHtml + toolbar + '<div class="data-grid-wrap"><table class="data-table"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>' + pagination;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function fetchDataRecords() {
|
|
131
|
+
if (!state.dataEntity) return;
|
|
132
|
+
state.dataLoading = true;
|
|
133
|
+
state.dataError = null;
|
|
134
|
+
renderDb();
|
|
135
|
+
|
|
136
|
+
var params = [
|
|
137
|
+
'env=' + encodeURIComponent(state.dataEnv),
|
|
138
|
+
'entity=' + encodeURIComponent(state.dataEntity),
|
|
139
|
+
'page=' + state.dataPage,
|
|
140
|
+
'limit=' + state.dataLimit
|
|
141
|
+
];
|
|
142
|
+
if (state.dataSort) params.push('sort=' + encodeURIComponent(state.dataSort));
|
|
143
|
+
if (state.dataOrder) params.push('order=' + encodeURIComponent(state.dataOrder));
|
|
144
|
+
if (state.dataSearch) params.push('search=' + encodeURIComponent(state.dataSearch));
|
|
145
|
+
|
|
146
|
+
api('/api/data?' + params.join('&')).then(function (res) {
|
|
147
|
+
state.dataResult = res;
|
|
148
|
+
state.dataLoading = false;
|
|
149
|
+
renderDb();
|
|
150
|
+
}, function (err) {
|
|
151
|
+
state.dataError = err.message;
|
|
152
|
+
state.dataLoading = false;
|
|
153
|
+
renderDb();
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function openInsertRecordModal() {
|
|
158
|
+
var tables = state.schema[state.dataEnv + 'Tables'] || [];
|
|
159
|
+
var table = tables.find(function (t) { return t.name === state.dataEntity; }) || { columns: [] };
|
|
160
|
+
var isColl = isCollection(table);
|
|
161
|
+
|
|
162
|
+
$('record-title').textContent = 'Insert ' + (isColl ? 'Document' : 'Record') + ' into ' + state.dataEntity;
|
|
163
|
+
$('record-lead').textContent = 'Target Environment: ' + state.dataEnv.toUpperCase() + '. Specify column values below:';
|
|
164
|
+
$('btn-save-record').textContent = 'Insert ' + (isColl ? 'Document' : 'Record');
|
|
165
|
+
state.editingPk = null;
|
|
166
|
+
|
|
167
|
+
var html = '';
|
|
168
|
+
table.columns.forEach(function (col) {
|
|
169
|
+
var isPk = col.primaryKey;
|
|
170
|
+
var fieldLabel = esc(col.name) + (isPk ? ' (Primary Key - Optional/Auto)' : '');
|
|
171
|
+
var placeholder = col.type + (col.default ? ' · default: ' + col.default : '');
|
|
172
|
+
var isLong = /json|map|array|text/i.test(col.type);
|
|
173
|
+
if (isLong) {
|
|
174
|
+
html += '<div class="field field-full"><label><b>' + fieldLabel + '</b></label><textarea class="input" name="' + esc(col.name) + '" placeholder="' + esc(placeholder) + '"></textarea></div>';
|
|
175
|
+
} else {
|
|
176
|
+
html += '<div class="field"><label><b>' + fieldLabel + '</b></label><input class="input" type="text" name="' + esc(col.name) + '" placeholder="' + esc(placeholder) + '"></div>';
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
$('record-fields').innerHTML = html || '<p class="muted">No schema columns defined.</p>';
|
|
180
|
+
openDialog($('record-dialog'));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function openEditRecordModal(rowIndex) {
|
|
184
|
+
if (!state.dataResult || !state.dataResult.rows[rowIndex]) return;
|
|
185
|
+
var row = state.dataResult.rows[rowIndex];
|
|
186
|
+
var pkCol = state.dataResult.primaryKey || 'id';
|
|
187
|
+
var pkVal = row[pkCol];
|
|
188
|
+
state.editingPk = pkVal;
|
|
189
|
+
|
|
190
|
+
var tables = state.schema[state.dataEnv + 'Tables'] || [];
|
|
191
|
+
var table = tables.find(function (t) { return t.name === state.dataEntity; }) || { columns: [] };
|
|
192
|
+
var isColl = isCollection(table);
|
|
193
|
+
|
|
194
|
+
$('record-title').textContent = 'Edit ' + (isColl ? 'Document' : 'Record') + ' (' + pkCol + ' = ' + pkVal + ')';
|
|
195
|
+
$('record-lead').textContent = 'Target Environment: ' + state.dataEnv.toUpperCase() + '. Primary key is locked for single-record isolation:';
|
|
196
|
+
$('btn-save-record').textContent = 'Save Changes';
|
|
197
|
+
|
|
198
|
+
var html = '';
|
|
199
|
+
html += '<div class="field field-full"><label><b>' + esc(pkCol) + ' (Primary Key - Locked)</b></label><input class="input" type="text" name="' + esc(pkCol) + '" value="' + esc(pkVal) + '" disabled style="opacity:.65"></div>';
|
|
200
|
+
|
|
201
|
+
var displayedCols = new Set([pkCol]);
|
|
202
|
+
table.columns.forEach(function (col) {
|
|
203
|
+
if (col.name === pkCol) return;
|
|
204
|
+
displayedCols.add(col.name);
|
|
205
|
+
var val = row[col.name];
|
|
206
|
+
var valStr = val == null ? '' : typeof val === 'object' ? JSON.stringify(val) : String(val);
|
|
207
|
+
var isLong = /json|map|array|text/i.test(col.type) || valStr.length > 50;
|
|
208
|
+
if (isLong) {
|
|
209
|
+
html += '<div class="field field-full"><label><b>' + esc(col.name) + ' (' + esc(col.type) + ')</b></label><textarea class="input" name="' + esc(col.name) + '">' + esc(valStr) + '</textarea></div>';
|
|
210
|
+
} else {
|
|
211
|
+
html += '<div class="field"><label><b>' + esc(col.name) + ' (' + esc(col.type) + ')</b></label><input class="input" type="text" name="' + esc(col.name) + '" value="' + esc(valStr) + '"></div>';
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
Object.keys(row).forEach(function (k) {
|
|
216
|
+
if (displayedCols.has(k)) return;
|
|
217
|
+
var val = row[k];
|
|
218
|
+
var valStr = val == null ? '' : typeof val === 'object' ? JSON.stringify(val) : String(val);
|
|
219
|
+
html += '<div class="field"><label><b>' + esc(k) + '</b></label><input class="input" type="text" name="' + esc(k) + '" value="' + esc(valStr) + '"></div>';
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
$('record-fields').innerHTML = html;
|
|
223
|
+
openDialog($('record-dialog'));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function parseInputValue(raw) {
|
|
227
|
+
if (raw === '') return null;
|
|
228
|
+
if (raw === 'true') return true;
|
|
229
|
+
if (raw === 'false') return false;
|
|
230
|
+
if (/^-?\d+$/.test(raw)) return parseInt(raw, 10);
|
|
231
|
+
if (/^-?\d+\.\d+$/.test(raw)) return parseFloat(raw);
|
|
232
|
+
if ((raw.startsWith('{') && raw.endsWith('}')) || (raw.startsWith('[') && raw.endsWith(']'))) {
|
|
233
|
+
try { return JSON.parse(raw); } catch (e) { return raw; }
|
|
234
|
+
}
|
|
235
|
+
return raw;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function handleSaveRecordSubmit() {
|
|
239
|
+
var form = $('record-form');
|
|
240
|
+
var inputs = form.querySelectorAll('input, textarea');
|
|
241
|
+
var data = {};
|
|
242
|
+
inputs.forEach(function (inp) {
|
|
243
|
+
if (inp.name && !inp.disabled) {
|
|
244
|
+
var v = inp.value.trim();
|
|
245
|
+
if (v !== '') data[inp.name] = parseInputValue(v);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (state.editingPk !== null) {
|
|
250
|
+
dispatchMutation('UPDATE', state.dataEntity, state.editingPk, { updates: data });
|
|
251
|
+
} else {
|
|
252
|
+
dispatchMutation('INSERT', state.dataEntity, null, { record: data });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function handleDeleteRecord(rowIndex) {
|
|
257
|
+
if (!state.dataResult || !state.dataResult.rows[rowIndex]) return;
|
|
258
|
+
var row = state.dataResult.rows[rowIndex];
|
|
259
|
+
var pkCol = state.dataResult.primaryKey || 'id';
|
|
260
|
+
var pkVal = row[pkCol];
|
|
261
|
+
if (pkVal === undefined || pkVal === null) return;
|
|
262
|
+
|
|
263
|
+
if (state.dataEnv === 'prod') {
|
|
264
|
+
dispatchMutation('DELETE', state.dataEntity, pkVal, {});
|
|
265
|
+
} else {
|
|
266
|
+
if (window.confirm('Delete record ' + pkVal + ' from ' + state.dataEntity + '?')) {
|
|
267
|
+
api('/api/data', {
|
|
268
|
+
method: 'DELETE',
|
|
269
|
+
headers: { 'Content-Type': 'application/json' },
|
|
270
|
+
body: JSON.stringify({ env: state.dataEnv, entity: state.dataEntity, primaryKey: pkVal })
|
|
271
|
+
}).then(function () {
|
|
272
|
+
toast('Record deleted successfully', 'ok');
|
|
273
|
+
fetchDataRecords();
|
|
274
|
+
}, function (err) {
|
|
275
|
+
toast('Delete failed: ' + err.message);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function dispatchMutation(action, entity, pk, payload) {
|
|
282
|
+
if (state.dataEnv === 'prod') {
|
|
283
|
+
state.pendingMutation = { action: action, entity: entity, pk: pk, payload: payload };
|
|
284
|
+
$('challenge-desc').textContent = 'Action: ' + action + ' on entity "' + entity + '"' + (pk != null ? ' (PK: ' + pk + ')' : '') + '.';
|
|
285
|
+
$('challenge-target-name').textContent = entity;
|
|
286
|
+
var input = $('challenge-input');
|
|
287
|
+
input.value = '';
|
|
288
|
+
$('btn-confirm-challenge').disabled = true;
|
|
289
|
+
openDialog($('challenge-dialog'));
|
|
290
|
+
input.focus();
|
|
291
|
+
} else {
|
|
292
|
+
executeMutationDirect(action, entity, pk, payload, false, '');
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function executeMutationDirect(action, entity, pk, payload, confirmProd, challengePhrase) {
|
|
297
|
+
var method = action === 'INSERT' ? 'POST' : action === 'UPDATE' ? 'PUT' : 'DELETE';
|
|
298
|
+
var body = Object.assign({
|
|
299
|
+
env: state.dataEnv,
|
|
300
|
+
entity: entity
|
|
301
|
+
}, payload);
|
|
302
|
+
if (pk != null) body.primaryKey = pk;
|
|
303
|
+
if (confirmProd) {
|
|
304
|
+
body.confirmProd = true;
|
|
305
|
+
body.challengePhrase = challengePhrase;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
api('/api/data', {
|
|
309
|
+
method: method,
|
|
310
|
+
headers: { 'Content-Type': 'application/json' },
|
|
311
|
+
body: JSON.stringify(body)
|
|
312
|
+
}).then(function () {
|
|
313
|
+
if (confirmProd) {
|
|
314
|
+
toast('Production ' + action.toLowerCase() + ' executed. Logged to .nativ/prod_audit.log', 'ok');
|
|
315
|
+
$('challenge-dialog').close();
|
|
316
|
+
} else {
|
|
317
|
+
toast(action + ' executed successfully', 'ok');
|
|
318
|
+
}
|
|
319
|
+
$('record-dialog').close();
|
|
320
|
+
fetchDataRecords();
|
|
321
|
+
}, function (err) {
|
|
322
|
+
toast(action + ' failed: ' + err.message);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const databaseConnectionScript: string;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Database Studio connection modal.
|
|
2
|
+
export const databaseConnectionScript = String.raw ` // ─── Database Studio: connection modal ─────────────────────────────────────
|
|
3
|
+
function renderModalStatus() {
|
|
4
|
+
['dev', 'prod'].forEach(function (env) {
|
|
5
|
+
var s = state.status && state.status[env];
|
|
6
|
+
$('mdot-' + env).className = 'dot ' + (s && s.connected ? 'on' : 'off');
|
|
7
|
+
$('current-' + env).innerHTML = s && s.maskedUrl
|
|
8
|
+
? 'Current: <code>' + esc(s.maskedUrl) + '</code><span class="copy-wrap"><span class="tooltip">Copied!</span><button class="btn small" type="button" data-copy-masked="' + env + '">Copy</button></span>'
|
|
9
|
+
: 'Current: <span class="muted">not configured</span>';
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function renderTemplateChips() {
|
|
14
|
+
var host = $('conn-template-chips');
|
|
15
|
+
if (!host) return;
|
|
16
|
+
if (!state.envInfo || !state.envInfo.templateFound || !state.envInfo.detectedKeys || state.envInfo.detectedKeys.length === 0) {
|
|
17
|
+
host.innerHTML = '';
|
|
18
|
+
host.hidden = true;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
var html = '<div class="template-helper-box">' +
|
|
22
|
+
'<div class="template-helper-label">Discovered in <code>' + esc(state.envInfo.templateFile || '.env.example') + '</code>:</div>' +
|
|
23
|
+
'<div class="template-chips">';
|
|
24
|
+
state.envInfo.detectedKeys.forEach(function (dk) {
|
|
25
|
+
var eng = dk.engine ? ' (' + (ENGINE_LABEL[dk.engine] || dk.engine) + ')' : '';
|
|
26
|
+
var statusDot = dk.isConfiguredInEnv ? '<span class="chip-dot on"></span>' : '<span class="chip-dot off"></span>';
|
|
27
|
+
var title = dk.isConfiguredInEnv ? 'Configured in .env' : 'Missing in .env';
|
|
28
|
+
html += '<button type="button" class="btn small template-chip" data-template-key="' + esc(dk.key) + '" data-template-engine="' + esc(dk.engine || '') + '" data-template-env="' + esc(dk.targetEnv) + '" title="' + title + '">' +
|
|
29
|
+
statusDot + esc(dk.key) + eng +
|
|
30
|
+
'</button>';
|
|
31
|
+
});
|
|
32
|
+
html += '</div></div>';
|
|
33
|
+
host.innerHTML = html;
|
|
34
|
+
host.hidden = false;
|
|
35
|
+
|
|
36
|
+
host.querySelectorAll('.template-chip').forEach(function (btn) {
|
|
37
|
+
btn.onclick = function () {
|
|
38
|
+
var key = btn.getAttribute('data-template-key');
|
|
39
|
+
var eng = btn.getAttribute('data-template-engine');
|
|
40
|
+
var targetEnv = btn.getAttribute('data-template-env') || 'dev';
|
|
41
|
+
// Fragmented component keys (DB_HOST, DB_PORT, ...) map to the parameter form, not a URL.
|
|
42
|
+
var isComponent = /(^|_)(HOST|HOSTNAME|SERVER|PORT|USER|USERNAME|PASS|PASSWORD|DATABASE|NAME|CONNECTION|DRIVER)$/i.test(key) && eng !== 'mongodb' && eng !== 'firestore' && eng !== 'sqlite';
|
|
43
|
+
$('engine-' + targetEnv).value = eng === 'firestore' ? 'firestore' : isComponent ? 'form' : 'url';
|
|
44
|
+
setEngineMode(targetEnv);
|
|
45
|
+
if (isComponent) {
|
|
46
|
+
if (eng === 'mysql' || eng === 'postgresql') $('pengine-' + targetEnv).value = eng;
|
|
47
|
+
$('phost-' + targetEnv).focus();
|
|
48
|
+
} else {
|
|
49
|
+
$((eng === 'firestore' ? 'fsproject-' : 'url-') + targetEnv).focus();
|
|
50
|
+
}
|
|
51
|
+
toast('Selected ' + key + ' for ' + targetEnv.toUpperCase());
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function openSettings() {
|
|
57
|
+
renderModalStatus();
|
|
58
|
+
renderTemplateChips();
|
|
59
|
+
['dev', 'prod'].forEach(function (env) { $('result-' + env).textContent = ''; $('result-' + env).className = 'result'; });
|
|
60
|
+
openDialog($('conn-dialog'));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isFirestoreMode(env) { return $('engine-' + env).value === 'firestore'; }
|
|
64
|
+
function isFormMode(env) { return $('engine-' + env).value === 'form'; }
|
|
65
|
+
|
|
66
|
+
var PARAM_FIELDS = ['phost', 'pport', 'puser', 'ppass', 'pdb'];
|
|
67
|
+
var DEFAULT_PORT = { mysql: '3306', postgresql: '5432' };
|
|
68
|
+
|
|
69
|
+
function setEngineMode(env) {
|
|
70
|
+
var fs = isFirestoreMode(env);
|
|
71
|
+
var form = isFormMode(env);
|
|
72
|
+
$('url-' + env).hidden = fs || form;
|
|
73
|
+
document.querySelector('[data-toggle="' + env + '"]').hidden = fs || form;
|
|
74
|
+
$('fsproject-' + env).hidden = !fs;
|
|
75
|
+
$('fsemu-' + env).hidden = !fs;
|
|
76
|
+
$('params-' + env).hidden = !form;
|
|
77
|
+
$('result-' + env).textContent = '';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Quick-fill for a stock XAMPP install: MySQL on 127.0.0.1:3306 as root with an empty password. */
|
|
81
|
+
function applyXamppDefaults(env) {
|
|
82
|
+
$('engine-' + env).value = 'form';
|
|
83
|
+
setEngineMode(env);
|
|
84
|
+
$('pengine-' + env).value = 'mysql';
|
|
85
|
+
$('phost-' + env).value = '127.0.0.1';
|
|
86
|
+
$('pport-' + env).value = '3306';
|
|
87
|
+
$('puser-' + env).value = 'root';
|
|
88
|
+
$('ppass-' + env).value = '';
|
|
89
|
+
$('pdb-' + env).focus();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function hasInput(env) {
|
|
93
|
+
if (isFirestoreMode(env)) return !!$('fsproject-' + env).value.trim();
|
|
94
|
+
if (isFormMode(env)) return !!$('phost-' + env).value.trim();
|
|
95
|
+
return !!$('url-' + env).value.trim();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Parameter form: sent as the contract's components payload; the server assembles the URL in memory. */
|
|
99
|
+
function componentsBody(env) {
|
|
100
|
+
var engine = $('pengine-' + env).value;
|
|
101
|
+
var host = $('phost-' + env).value.trim();
|
|
102
|
+
var port = $('pport-' + env).value.trim() || DEFAULT_PORT[engine];
|
|
103
|
+
if (!host) return { error: 'Enter a host, e.g. 127.0.0.1.' };
|
|
104
|
+
if (!/^[A-Za-z0-9._:\[\]-]{1,253}$/.test(host)) return { error: 'Host must be a hostname or IP address.' };
|
|
105
|
+
if (!/^\d{1,5}$/.test(port) || +port < 1 || +port > 65535) return { error: 'Port must be a number between 1 and 65535.' };
|
|
106
|
+
var components = { engine: engine, host: host, port: +port };
|
|
107
|
+
var user = $('puser-' + env).value.trim();
|
|
108
|
+
var password = $('ppass-' + env).value;
|
|
109
|
+
var database = $('pdb-' + env).value.trim();
|
|
110
|
+
if (user) components.user = user;
|
|
111
|
+
if (password) components.password = password;
|
|
112
|
+
if (database) components.database = database;
|
|
113
|
+
return { body: { env: env, engine: engine, components: components } };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Builds the /api/connect body. Firestore is sent as a firestore:// URL plus the contract's firestoreConfig. */
|
|
117
|
+
function connectBody(env) {
|
|
118
|
+
if (isFormMode(env)) return componentsBody(env);
|
|
119
|
+
if (!isFirestoreMode(env)) {
|
|
120
|
+
var url = $('url-' + env).value.trim();
|
|
121
|
+
return url ? { body: { env: env, connectionUrl: url } } : { error: 'Enter a connection string first.' };
|
|
122
|
+
}
|
|
123
|
+
var projectId = $('fsproject-' + env).value.trim();
|
|
124
|
+
var emulatorHost = $('fsemu-' + env).value.trim();
|
|
125
|
+
if (!/^[a-z0-9][a-z0-9-]{2,62}$/.test(projectId)) return { error: 'Enter a valid Firestore project ID (lowercase letters, digits, hyphens).' };
|
|
126
|
+
if (emulatorHost && !/^[A-Za-z0-9.\-\[\]:]+:\d{2,5}$/.test(emulatorHost)) return { error: 'Emulator host must look like localhost:8080.' };
|
|
127
|
+
var firestoreUrl = 'firestore://' + encodeURIComponent(projectId) + (emulatorHost ? '?emulator=' + emulatorHost : '');
|
|
128
|
+
var config = { projectId: projectId };
|
|
129
|
+
if (emulatorHost) config.emulatorHost = emulatorHost;
|
|
130
|
+
return { body: { env: env, engine: 'firestore', connectionUrl: firestoreUrl, firestoreConfig: config } };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function ping(env) {
|
|
134
|
+
var out = $('result-' + env);
|
|
135
|
+
var req = connectBody(env);
|
|
136
|
+
if (req.error) { out.className = 'result err'; out.textContent = req.error; return Promise.resolve(false); }
|
|
137
|
+
out.className = 'result'; out.textContent = 'Testing connection…';
|
|
138
|
+
return postJson('/api/connect', req.body).then(function (r) {
|
|
139
|
+
if (r.success) {
|
|
140
|
+
var count = r.entityCount != null ? r.entityCount : r.tableCount;
|
|
141
|
+
out.className = 'result ok';
|
|
142
|
+
out.textContent = 'Connected to ' + (ENGINE_LABEL[r.engine] || r.engine) + ' in ' + Math.round(r.pingMs) + 'ms · ' + count + ' ' +
|
|
143
|
+
entityNoun({ engine: r.engine }, true).toLowerCase() + ' · saved to session as ' + r.maskedUrl;
|
|
144
|
+
$('url-' + env).value = '';
|
|
145
|
+
$('ppass-' + env).value = '';
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
out.className = 'result err';
|
|
149
|
+
out.textContent = 'Error: ' + (r.error || 'Connection failed');
|
|
150
|
+
var hint = hintFor(r.error);
|
|
151
|
+
if (hint) out.textContent += ' — ' + hint;
|
|
152
|
+
return false;
|
|
153
|
+
}, function (e) {
|
|
154
|
+
out.className = 'result err'; out.textContent = 'Error: ' + e.message; return false;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const databaseDataScript: string;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Database Studio data loading and shell.
|
|
2
|
+
export const databaseDataScript = String.raw ` // ─── View 5: Database Studio — data loading ────────────────────────────────
|
|
3
|
+
function load(fresh) {
|
|
4
|
+
state.loading = true;
|
|
5
|
+
renderDb();
|
|
6
|
+
var q = fresh ? '?refresh=1' : '';
|
|
7
|
+
var diffPath = '/api/diff?target=' + state.diffTarget + (fresh ? '&refresh=1' : '');
|
|
8
|
+
return Promise.all([
|
|
9
|
+
api('/api/status' + q).catch(function (e) { toast(e.message); return null; }),
|
|
10
|
+
api('/api/schema' + q).catch(function (e) { toast(e.message); return { devTables: [], prodTables: [] }; }),
|
|
11
|
+
api(diffPath).then(function (d) { state.diffError = null; return d; }, function (e) { state.diffError = e.message; return null; }),
|
|
12
|
+
api('/api/env-info').catch(function () { return null; })
|
|
13
|
+
]).then(function (results) {
|
|
14
|
+
state.status = results[0];
|
|
15
|
+
state.schema = results[1];
|
|
16
|
+
state.diff = results[2];
|
|
17
|
+
state.envInfo = results[3];
|
|
18
|
+
state.diffTargetUsed = state.diffTarget;
|
|
19
|
+
state.loading = false;
|
|
20
|
+
if (state.status) {
|
|
21
|
+
['dev', 'prod'].forEach(function (env) {
|
|
22
|
+
var s = state.status[env];
|
|
23
|
+
if (s && !s.connected && s.error && !/No (Dev|Prod) database configured/.test(s.error)) {
|
|
24
|
+
toast((env === 'dev' ? 'Dev' : 'Prod') + ' connection failed: ' + s.error);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
renderDb();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ─── Database Studio: shell ────────────────────────────────────────────────
|
|
33
|
+
/** DB_HOST -> DB_*, PROD_DB_HOST -> PROD_DB_*, PGHOST -> PG*. */
|
|
34
|
+
function keyFamily(key) {
|
|
35
|
+
var m = /^(.*_)[^_]*$/.exec(key);
|
|
36
|
+
return m ? m[1] + '*' : key.slice(0, 2) + '*';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Local-stack hint when fragmented DB_* keys resolve to a server that refuses connections. */
|
|
40
|
+
function fragmentedDiagnostic(s) {
|
|
41
|
+
var cfg = state.envInfo && state.envInfo.fragmentedConfig;
|
|
42
|
+
if (!s || s.connected || !s.synthesized || !cfg) return '';
|
|
43
|
+
if (!/ECONNREFUSED|ETIMEDOUT|EHOSTUNREACH/i.test(s.error || '')) return '';
|
|
44
|
+
var keys = cfg.sourceKeys.filter(function (k) { return /HOST|DATABASE|_DB$|DB_NAME$/.test(k); });
|
|
45
|
+
var label = ENGINE_LABEL[cfg.engine] || cfg.engine;
|
|
46
|
+
var action = cfg.engine === 'mysql'
|
|
47
|
+
? 'Ensure MySQL is started in XAMPP Control Panel.'
|
|
48
|
+
: 'Ensure the PostgreSQL service is running on ' + cfg.host + ':' + cfg.port + '.';
|
|
49
|
+
return 'Detected fragmented ' + label + ' config (' + (keys.length ? keys : cfg.sourceKeys).join(', ') + '). ' + action;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function statusPill(env, s) {
|
|
53
|
+
var label = env.toUpperCase();
|
|
54
|
+
if (!s) return '<span class="pill"><span class="dot"></span><b>' + label + ':</b> loading…</span>';
|
|
55
|
+
if (!s.connected) {
|
|
56
|
+
var why = /No (Dev|Prod) database configured/.test(s.error || '') ? 'not configured' : 'offline';
|
|
57
|
+
return '<span class="pill" title="' + esc(s.error || '') + '"><span class="dot off"></span><b>' + label + ':</b> ' + why +
|
|
58
|
+
' <span class="badge badge-offline">OFFLINE</span></span>';
|
|
59
|
+
}
|
|
60
|
+
var sourceBadge = s.synthesized && s.sourceKey
|
|
61
|
+
? ' <span class="badge badge-synth" title="Assembled in memory from fragmented .env keys">synthesized from ' + esc(keyFamily(s.sourceKey)) + '</span>'
|
|
62
|
+
: s.detectedFromExample && s.sourceKey
|
|
63
|
+
? ' <span class="badge badge-source" title="Detected in ' + esc(s.exampleFile || '.env.example') + '">via ' + esc(s.sourceKey) + '</span>'
|
|
64
|
+
: (s.sourceKey ? ' <span class="badge badge-source">via ' + esc(s.sourceKey) + '</span>' : '');
|
|
65
|
+
return '<span class="pill" title="' + esc(s.maskedUrl) + '"><span class="dot on"></span><b>' + label + ':</b> ' +
|
|
66
|
+
esc(ENGINE_LABEL[s.engine] || s.engine) + (s.database ? ' · ' + esc(s.database) : '') + ' (' + Math.round(s.pingMs) + 'ms) – ' +
|
|
67
|
+
entityCount(s) + ' ' + entityNoun(s, true) + sourceBadge + ' <span class="badge badge-online">ONLINE</span></span>';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function renderDbShell() {
|
|
71
|
+
var st = state.status || {};
|
|
72
|
+
$('telemetry').innerHTML = statusPill('dev', st.dev) + statusPill('prod', st.prod);
|
|
73
|
+
|
|
74
|
+
var bannerEl = $('diagnostic-banner');
|
|
75
|
+
if (bannerEl) {
|
|
76
|
+
var devS = st.dev;
|
|
77
|
+
var prodS = st.prod;
|
|
78
|
+
var activeS = state.envMode === 'prod' ? prodS : devS;
|
|
79
|
+
var suggestion = fragmentedDiagnostic(activeS) || fragmentedDiagnostic(devS) ||
|
|
80
|
+
(activeS && !activeS.connected && activeS.suggestion) ||
|
|
81
|
+
(devS && !devS.connected && devS.suggestion) ||
|
|
82
|
+
(prodS && !prodS.connected && prodS.suggestion);
|
|
83
|
+
|
|
84
|
+
if (!suggestion && state.envInfo && state.envInfo.missingKeys && state.envInfo.missingKeys.length > 0) {
|
|
85
|
+
var missingForActive = state.envInfo.missingKeys.find(function (k) {
|
|
86
|
+
return k.targetEnv === (state.envMode === 'prod' ? 'prod' : 'dev');
|
|
87
|
+
}) || state.envInfo.missingKeys[0];
|
|
88
|
+
if (missingForActive && ((missingForActive.targetEnv === 'dev' && (!devS || !devS.connected)) || (missingForActive.targetEnv === 'prod' && (!prodS || !prodS.connected)))) {
|
|
89
|
+
var engLabel = missingForActive.engine ? ' (' + (ENGINE_LABEL[missingForActive.engine] || missingForActive.engine) + ')' : '';
|
|
90
|
+
suggestion = 'Detected "' + missingForActive.key + '"' + engLabel + ' in ' + (state.envInfo.templateFile || '.env.example') + ', but it is not set in your .env file.';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (suggestion && ((!devS || !devS.connected) || (!prodS || !prodS.connected))) {
|
|
95
|
+
bannerEl.innerHTML = '<div class="diagnostic-banner">' +
|
|
96
|
+
'<div class="diagnostic-content">' +
|
|
97
|
+
'<span class="diagnostic-tag">[DIAGNOSTIC]</span> ' +
|
|
98
|
+
'<span class="diagnostic-msg">' + esc(suggestion) + '</span>' +
|
|
99
|
+
'</div>' +
|
|
100
|
+
'<div class="diagnostic-actions">' +
|
|
101
|
+
'<button class="btn small" type="button" id="btn-diag-settings">Open Connection Settings</button>' +
|
|
102
|
+
'</div>' +
|
|
103
|
+
'</div>';
|
|
104
|
+
bannerEl.hidden = false;
|
|
105
|
+
var btnDiag = $('btn-diag-settings');
|
|
106
|
+
if (btnDiag) {
|
|
107
|
+
btnDiag.onclick = function () { openSettings(); };
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
bannerEl.innerHTML = '';
|
|
111
|
+
bannerEl.hidden = true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
$('count-explorer').textContent = String((state.schema.devTables || []).length + (state.schema.prodTables || []).length);
|
|
116
|
+
var activeEnv = state.envMode === 'prod' ? 'prod' : 'dev';
|
|
117
|
+
var dataCount = state.dataResult && state.dataResult.totalCount != null
|
|
118
|
+
? state.dataResult.totalCount
|
|
119
|
+
: ((state.schema[activeEnv + 'Tables'] || []).length);
|
|
120
|
+
$('count-data').textContent = String(dataCount);
|
|
121
|
+
var d = state.diff && state.diff.summary;
|
|
122
|
+
$('count-drift').textContent = d ? String(d.addedTablesCount + d.alteredTablesCount + d.droppedTablesCount) : '–';
|
|
123
|
+
var script = state.diff ? generateScript(state.diff) : { text: '', lang: 'sql', count: 0 };
|
|
124
|
+
state.sql = script.text;
|
|
125
|
+
state.scriptLang = script.lang;
|
|
126
|
+
state.scriptCount = script.count;
|
|
127
|
+
$('count-sql').textContent = String(script.count);
|
|
128
|
+
document.querySelectorAll('.subtab').forEach(function (t) {
|
|
129
|
+
var active = t.getAttribute('data-tab') === state.tab;
|
|
130
|
+
t.setAttribute('aria-selected', active ? 'true' : 'false');
|
|
131
|
+
t.setAttribute('tabindex', active ? '0' : '-1');
|
|
132
|
+
});
|
|
133
|
+
document.querySelectorAll('.env-seg-btn').forEach(function (btn) {
|
|
134
|
+
var m = btn.getAttribute('data-env-mode');
|
|
135
|
+
btn.setAttribute('aria-pressed', String(m === state.envMode));
|
|
136
|
+
});
|
|
137
|
+
$('view').setAttribute('aria-labelledby', 'tab-' + state.tab);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function renderDb() {
|
|
141
|
+
renderDbShell();
|
|
142
|
+
mediaRegistry = [];
|
|
143
|
+
hideThumb();
|
|
144
|
+
var view = $('view');
|
|
145
|
+
if (state.loading) { view.innerHTML = dbSkeleton(); return; }
|
|
146
|
+
if (state.tab === 'explorer') view.innerHTML = renderExplorer();
|
|
147
|
+
else if (state.tab === 'data') view.innerHTML = renderData();
|
|
148
|
+
else if (state.tab === 'drift') view.innerHTML = renderDrift();
|
|
149
|
+
else view.innerHTML = renderSql();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function dbSkeleton() {
|
|
153
|
+
var col = '<div class="pane card">' + '<div class="skeleton short"></div><div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div></div>';
|
|
154
|
+
return '<div class="sr-only">Loading database schema…</div><div class="panes" aria-busy="true">' + col + col + '</div>';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
var DB_ICON = '<svg class="icon empty-icon" width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" aria-hidden="true">' +
|
|
158
|
+
'<ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v6c0 1.66 3.58 3 8 3s8-1.34 8-3V5"/><path d="M4 11v6c0 1.66 3.58 3 8 3s8-1.34 8-3v-6"/></svg>';
|
|
159
|
+
|
|
160
|
+
function emptyState(title, text) {
|
|
161
|
+
return '<div class="empty card">' + DB_ICON + '<h3>' + esc(title) + '</h3><p>' + esc(text) + '</p>' +
|
|
162
|
+
'<button class="btn primary" type="button" data-action="open-settings">Connection Settings</button></div>';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
`;
|