@njeybe/nativ 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/README.md +15 -0
  3. package/dist/server/studio-ui/document.d.ts +3 -0
  4. package/dist/server/studio-ui/document.js +22 -0
  5. package/dist/server/studio-ui/pages/benchmarks/index.d.ts +2 -0
  6. package/dist/server/studio-ui/pages/benchmarks/index.js +11 -0
  7. package/dist/server/studio-ui/pages/benchmarks/script.d.ts +1 -0
  8. package/dist/server/studio-ui/pages/benchmarks/script.js +104 -0
  9. package/dist/server/studio-ui/pages/benchmarks/styles.d.ts +1 -0
  10. package/dist/server/studio-ui/pages/benchmarks/styles.js +30 -0
  11. package/dist/server/studio-ui/pages/database/browser.d.ts +1 -0
  12. package/dist/server/studio-ui/pages/database/browser.js +326 -0
  13. package/dist/server/studio-ui/pages/database/connection.d.ts +1 -0
  14. package/dist/server/studio-ui/pages/database/connection.js +158 -0
  15. package/dist/server/studio-ui/pages/database/data-shell.d.ts +1 -0
  16. package/dist/server/studio-ui/pages/database/data-shell.js +165 -0
  17. package/dist/server/studio-ui/pages/database/drift.d.ts +1 -0
  18. package/dist/server/studio-ui/pages/database/drift.js +301 -0
  19. package/dist/server/studio-ui/pages/database/events.d.ts +1 -0
  20. package/dist/server/studio-ui/pages/database/events.js +231 -0
  21. package/dist/server/studio-ui/pages/database/explorer.d.ts +1 -0
  22. package/dist/server/studio-ui/pages/database/explorer.js +183 -0
  23. package/dist/server/studio-ui/pages/database/index.d.ts +2 -0
  24. package/dist/server/studio-ui/pages/database/index.js +28 -0
  25. package/dist/server/studio-ui/pages/database/markup.d.ts +1 -0
  26. package/dist/server/studio-ui/pages/database/markup.js +32 -0
  27. package/dist/server/studio-ui/pages/database/state.d.ts +2 -0
  28. package/dist/server/studio-ui/pages/database/state.js +57 -0
  29. package/dist/server/studio-ui/pages/database/styles.d.ts +1 -0
  30. package/dist/server/studio-ui/pages/database/styles.js +118 -0
  31. package/dist/server/studio-ui/pages/flow/index.d.ts +2 -0
  32. package/dist/server/studio-ui/pages/flow/index.js +11 -0
  33. package/dist/server/studio-ui/pages/flow/layout.d.ts +1 -0
  34. package/dist/server/studio-ui/pages/flow/layout.js +105 -0
  35. package/dist/server/studio-ui/pages/flow/plan-view.d.ts +1 -0
  36. package/dist/server/studio-ui/pages/flow/plan-view.js +67 -0
  37. package/dist/server/studio-ui/pages/flow/script.d.ts +1 -0
  38. package/dist/server/studio-ui/pages/flow/script.js +61 -0
  39. package/dist/server/studio-ui/pages/flow/styles.d.ts +1 -0
  40. package/dist/server/studio-ui/pages/flow/styles.js +59 -0
  41. package/dist/server/studio-ui/pages/flow/timeline-model.d.ts +1 -0
  42. package/dist/server/studio-ui/pages/flow/timeline-model.js +95 -0
  43. package/dist/server/studio-ui/pages/flow/timeline-view.d.ts +1 -0
  44. package/dist/server/studio-ui/pages/flow/timeline-view.js +92 -0
  45. package/dist/server/studio-ui/pages/home/index.d.ts +2 -0
  46. package/dist/server/studio-ui/pages/home/index.js +11 -0
  47. package/dist/server/studio-ui/pages/home/script.d.ts +1 -0
  48. package/dist/server/studio-ui/pages/home/script.js +160 -0
  49. package/dist/server/studio-ui/pages/home/styles.d.ts +1 -0
  50. package/dist/server/studio-ui/pages/home/styles.js +48 -0
  51. package/dist/server/studio-ui/pages/index.d.ts +2 -0
  52. package/dist/server/studio-ui/pages/index.js +17 -0
  53. package/dist/server/studio-ui/pages/tasks/index.d.ts +2 -0
  54. package/dist/server/studio-ui/pages/tasks/index.js +11 -0
  55. package/dist/server/studio-ui/pages/tasks/script.d.ts +1 -0
  56. package/dist/server/studio-ui/pages/tasks/script.js +96 -0
  57. package/dist/server/studio-ui/pages/tasks/styles.d.ts +1 -0
  58. package/dist/server/studio-ui/pages/tasks/styles.js +37 -0
  59. package/dist/server/studio-ui/pages/team/index.d.ts +2 -0
  60. package/dist/server/studio-ui/pages/team/index.js +11 -0
  61. package/dist/server/studio-ui/pages/team/script.d.ts +1 -0
  62. package/dist/server/studio-ui/pages/team/script.js +112 -0
  63. package/dist/server/studio-ui/pages/team/styles.d.ts +1 -0
  64. package/dist/server/studio-ui/pages/team/styles.js +22 -0
  65. package/dist/server/studio-ui/pages/types.d.ts +13 -0
  66. package/dist/server/studio-ui/pages/types.js +1 -0
  67. package/dist/server/studio-ui/pages/worktrees/index.d.ts +2 -0
  68. package/dist/server/studio-ui/pages/worktrees/index.js +11 -0
  69. package/dist/server/studio-ui/pages/worktrees/script.d.ts +1 -0
  70. package/dist/server/studio-ui/pages/worktrees/script.js +104 -0
  71. package/dist/server/studio-ui/pages/worktrees/styles.d.ts +1 -0
  72. package/dist/server/studio-ui/pages/worktrees/styles.js +15 -0
  73. package/dist/server/studio-ui/shell/actions.d.ts +1 -0
  74. package/dist/server/studio-ui/shell/actions.js +49 -0
  75. package/dist/server/studio-ui/shell/boot.d.ts +2 -0
  76. package/dist/server/studio-ui/shell/boot.js +13 -0
  77. package/dist/server/studio-ui/shell/console.d.ts +1 -0
  78. package/dist/server/studio-ui/shell/console.js +194 -0
  79. package/dist/server/studio-ui/shell/core.d.ts +1 -0
  80. package/dist/server/studio-ui/shell/core.js +50 -0
  81. package/dist/server/studio-ui/shell/detail-view.d.ts +1 -0
  82. package/dist/server/studio-ui/shell/detail-view.js +80 -0
  83. package/dist/server/studio-ui/shell/detail.d.ts +1 -0
  84. package/dist/server/studio-ui/shell/detail.js +83 -0
  85. package/dist/server/studio-ui/shell/dispatch.d.ts +1 -0
  86. package/dist/server/studio-ui/shell/dispatch.js +156 -0
  87. package/dist/server/studio-ui/shell/events-tools.d.ts +1 -0
  88. package/dist/server/studio-ui/shell/events-tools.js +94 -0
  89. package/dist/server/studio-ui/shell/format.d.ts +1 -0
  90. package/dist/server/studio-ui/shell/format.js +151 -0
  91. package/dist/server/studio-ui/shell/icons.d.ts +2 -0
  92. package/dist/server/studio-ui/shell/icons.js +20 -0
  93. package/dist/server/studio-ui/shell/markup-head.d.ts +1 -0
  94. package/dist/server/studio-ui/shell/markup-head.js +43 -0
  95. package/dist/server/studio-ui/shell/markup-panels.d.ts +2 -0
  96. package/dist/server/studio-ui/shell/markup-panels.js +7 -0
  97. package/dist/server/studio-ui/shell/markup-tools.d.ts +1 -0
  98. package/dist/server/studio-ui/shell/markup-tools.js +222 -0
  99. package/dist/server/studio-ui/shell/model.d.ts +1 -0
  100. package/dist/server/studio-ui/shell/model.js +192 -0
  101. package/dist/server/studio-ui/shell/paint.d.ts +1 -0
  102. package/dist/server/studio-ui/shell/paint.js +154 -0
  103. package/dist/server/studio-ui/shell/palette-view.d.ts +1 -0
  104. package/dist/server/studio-ui/shell/palette-view.js +60 -0
  105. package/dist/server/studio-ui/shell/palette.d.ts +1 -0
  106. package/dist/server/studio-ui/shell/palette.js +80 -0
  107. package/dist/server/studio-ui/shell/pipeline-state.d.ts +1 -0
  108. package/dist/server/studio-ui/shell/pipeline-state.js +152 -0
  109. package/dist/server/studio-ui/shell/proposals.d.ts +1 -0
  110. package/dist/server/studio-ui/shell/proposals.js +197 -0
  111. package/dist/server/studio-ui/shell/pure.d.ts +3 -0
  112. package/dist/server/studio-ui/shell/pure.js +9 -0
  113. package/dist/server/studio-ui/shell/router.d.ts +1 -0
  114. package/dist/server/studio-ui/shell/router.js +95 -0
  115. package/dist/server/studio-ui/shell/sidebar-view.d.ts +1 -0
  116. package/dist/server/studio-ui/shell/sidebar-view.js +38 -0
  117. package/dist/server/studio-ui/shell/sync.d.ts +1 -0
  118. package/dist/server/studio-ui/shell/sync.js +120 -0
  119. package/dist/server/studio-ui/shell/toasts-format.d.ts +1 -0
  120. package/dist/server/studio-ui/shell/toasts-format.js +89 -0
  121. package/dist/server/studio-ui/shell/triage.d.ts +1 -0
  122. package/dist/server/studio-ui/shell/triage.js +210 -0
  123. package/dist/server/studio-ui/styles/console.d.ts +1 -0
  124. package/dist/server/studio-ui/styles/console.js +83 -0
  125. package/dist/server/studio-ui/styles/dialogs.d.ts +1 -0
  126. package/dist/server/studio-ui/styles/dialogs.js +118 -0
  127. package/dist/server/studio-ui/styles/responsive.d.ts +1 -0
  128. package/dist/server/studio-ui/styles/responsive.js +28 -0
  129. package/dist/server/studio-ui/styles/shell.d.ts +1 -0
  130. package/dist/server/studio-ui/styles/shell.js +117 -0
  131. package/dist/server/studio-ui/styles/strategist.d.ts +1 -0
  132. package/dist/server/studio-ui/styles/strategist.js +77 -0
  133. package/dist/server/studio-ui/styles/tokens-base.d.ts +1 -0
  134. package/dist/server/studio-ui/styles/tokens-base.js +131 -0
  135. package/dist/server/studio-ui.d.ts +1 -10
  136. package/dist/server/studio-ui.js +63 -5314
  137. package/package.json +2 -2
  138. package/plugin/.claude-plugin/plugin.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.3.0
4
+
5
+ ### Changed
6
+
7
+ - **Studio redesign.** New Home, Tasks, Flow and Team pages focused on what needs you, what
8
+ finished, how the work connects, and how each agent is doing. Ctrl+K command palette to find
9
+ any task, a detail panel to read and act on one task, and light and dark themes.
10
+
11
+ ### Removed
12
+
13
+ - Agent Cockpits and Workflow Canvas pages and the simulation controls in the UI. The
14
+ simulation server endpoints remain for now and will be removed in a later cleanup.
15
+
3
16
  ## 2.2.0
4
17
 
5
18
  ### Added
@@ -34,7 +47,7 @@
34
47
 
35
48
  Documentation and package metadata only; no behaviour changes.
36
49
 
37
- - **npm is the only install path.** The source repository is private, so the instructions for `npm install -g github:…` and for adding the plugin marketplace were removed from the README and the installation guide. `nativ setup` is the complete route: it writes the agents, the enforcement hook, the MCP entry and the permission rules, which a plugin cannot ship.
50
+ - **npm is the only install path.** The source repository is private, so the instructions for `npm install -g github:...` and for adding the plugin marketplace were removed from the README and the installation guide. `nativ setup` is the complete route: it writes the agents, the enforcement hook, the MCP entry and the permission rules, which a plugin cannot ship.
38
51
  - The plugin files still ship inside the package (`plugin/`) and can be tried for one session with `claude --plugin-dir`.
39
52
  - `package.json` no longer declares `repository`, `homepage` or `bugs`, so the npm page has no dead links.
40
53
  - The 2.0.0 entry below still mentions the plugin marketplace; that route is not available outside the private repository.
package/README.md CHANGED
@@ -234,6 +234,21 @@ nativ bench --scenario concurrency --concurrency 8
234
234
  nativ bench --json
235
235
  ```
236
236
 
237
+ ### Studio
238
+
239
+ `nativ studio` runs a local dashboard at http://localhost:4983 with live updates. It loads
240
+ nothing from the internet and runs under a strict content security policy. The sidebar
241
+ groups pages by role.
242
+
243
+ **Home** shows what needs you, what's running now, and what just finished.
244
+ **Tasks** displays one milestone at a time, sorted newest completed first.
245
+ **Flow** shows how the work connects and visualizes how the run really happened.
246
+ **Team** lists each agent's activity in plain terms: tasks done, time spent, cost.
247
+ **Worktrees, Benchmarks, Database** are system tools for branches, performance and schemas.
248
+
249
+ Press Ctrl+K (or Cmd+K or /) to find any task by id, title, agent or file name. The theme
250
+ toggle switches between light and dark; your choice is saved locally.
251
+
237
252
  ### Database studio and schema telemetry (masked, structure only)
238
253
 
239
254
  ```bash
@@ -0,0 +1,3 @@
1
+ export declare function escapeHtml(value: string): string;
2
+ export declare const documentStart: string;
3
+ export declare const styleEnd = "</style>\n";
@@ -0,0 +1,22 @@
1
+ export function escapeHtml(value) {
2
+ return value.replace(/[&<>"']/g, (ch) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[ch]);
3
+ }
4
+ // Runs before first paint so a saved theme never flashes the wrong colours.
5
+ const themeScript = String.raw `(function () {
6
+ try {
7
+ var t = localStorage.getItem('nativ-studio:theme');
8
+ if (t === 'dark' || t === 'light') document.documentElement.setAttribute('data-theme', t);
9
+ } catch (e) { /* storage blocked */ }
10
+ })();`;
11
+ // Opening of the document up to the style block; the CSS chunks follow it.
12
+ export const documentStart = String.raw `<!doctype html>
13
+ <html lang="en">
14
+ <head>
15
+ <meta charset="utf-8">
16
+ <meta name="viewport" content="width=device-width, initial-scale=1">
17
+ <meta name="color-scheme" content="light dark">
18
+ <title>Nativ Studio</title>
19
+ <script>${themeScript}</script>
20
+ <style>
21
+ `;
22
+ export const styleEnd = '</style>\n';
@@ -0,0 +1,2 @@
1
+ import type { PageModule } from '../types.js';
2
+ export declare const benchmarksPage: PageModule;
@@ -0,0 +1,11 @@
1
+ import { ICON_PATHS } from '../../shell/icons.js';
2
+ import { benchmarksCss } from './styles.js';
3
+ import { benchmarksScript } from './script.js';
4
+ export const benchmarksPage = {
5
+ id: 'benchmarks',
6
+ group: 'system',
7
+ label: 'Benchmarks',
8
+ icon: ICON_PATHS.benchmarks,
9
+ css: benchmarksCss,
10
+ script: benchmarksScript,
11
+ };
@@ -0,0 +1 @@
1
+ export declare const benchmarksScript: string;
@@ -0,0 +1,104 @@
1
+ // Benchmarks view script.
2
+ export const benchmarksScript = String.raw ` // ─── View 4: Synthetic benchmarks ──────────────────────────────────────────
3
+ var SCENARIOS = [
4
+ { id: 'concurrency', label: 'Multi-Agent Concurrency' },
5
+ { id: 'telemetry', label: 'Telemetry Engine' },
6
+ { id: 'governor', label: 'Governor Invariants' },
7
+ { id: 'verification', label: 'Gatekeeper' },
8
+ { id: 'e2e_pipeline', label: 'E2E Lifecycle' }
9
+ ];
10
+
11
+ function renderBenchmarks() {
12
+ var running = !!pipe.busy.bench;
13
+ var runBtn = '<button class="btn primary" type="button" data-act="bench-run"' + (running ? ' disabled' : '') + '>' +
14
+ (running ? '<span class="spinner" aria-hidden="true"></span>Running…' : ICON.play + 'Run Benchmark') + '</button>';
15
+ var head = panelHead('Synthetic Benchmarks', 'Sandboxed stress runs of plan locking, telemetry, the contract governor, the verification gatekeeper and the full agent lifecycle.', runBtn);
16
+ if (!pipe.loaded.benchmarks) return head + '<div class="card section" aria-hidden="true"><div class="skeleton tall"></div></div><div class="scenario-grid" aria-hidden="true">' + skeletonCards(5, 'scenario') + '</div>';
17
+ if (pipe.errors.benchmarks && !pipe.report) return head + errorCard('benchmark report', pipe.errors.benchmarks, 'benchmarks');
18
+ var r = pipe.report;
19
+ if (!r) {
20
+ return head + '<div class="card state" aria-busy="' + running + '"><div class="state-icon">' + (running ? '<span class="spinner" aria-hidden="true"></span>' : ICON.gauge) + '</div>' +
21
+ '<h2>' + (running ? 'Running benchmark matrix…' : 'No benchmark report yet') + '</h2>' +
22
+ '<p>' + (running ? 'Scenarios run in throwaway sandboxes; this usually takes a few seconds.' : 'Run the synthetic matrix to measure throughput and score all five pipeline scenarios. Results are cached in .ai/benchmark_report.json.') + '</p></div>';
23
+ }
24
+ return head + staleNote('benchmarks') + benchHero(r, running) + '<div class="scenario-grid">' + scenarioCards(r) + '</div>';
25
+ }
26
+
27
+ function benchHero(r, running) {
28
+ var s = r.summary || {};
29
+ var env = r.environment || {};
30
+ var meta = [];
31
+ if (r.timestamp) meta.push('Ran ' + esc(fmtDate(r.timestamp)));
32
+ if (env.platform) meta.push(esc(env.platform + (env.arch ? ' ' + env.arch : '')));
33
+ if (env.nodeVersion) meta.push('Node ' + esc(env.nodeVersion));
34
+ if (env.cpuCount) meta.push(env.cpuCount + ' CPUs');
35
+ var score = clampPct(s.score);
36
+ return '<section class="card hero' + (running ? ' is-running' : '') + '" aria-labelledby="hero-h" aria-busy="' + running + '">' +
37
+ '<div><h2 class="eyebrow" id="hero-h">Average throughput</h2>' +
38
+ '<div class="hero-value">' + num(Math.round(s.averageThroughputOpsPerSec || 0)) + '<span class="badge badge-accent">ops/sec</span></div>' +
39
+ '<p class="hero-meta">' + meta.join(' · ') + '</p></div>' +
40
+ '<dl class="hero-stats">' +
41
+ '<div><dt>Score</dt><dd class="' + (score < 100 ? 'text-danger' : '') + '">' + score + '%</dd></div>' +
42
+ '<div><dt>Scenarios passed</dt><dd>' + num(s.passedScenarios) + '/' + num(s.totalScenarios) + '</dd></div>' +
43
+ '<div><dt>Total duration</dt><dd>' + fmtMs(s.totalDurationMs) + '</dd></div>' +
44
+ '<div><dt>Operations</dt><dd>' + num(s.totalOperations) + '</dd></div>' +
45
+ '</dl></section>';
46
+ }
47
+
48
+ function scenarioCards(r) {
49
+ var byId = {}, known = {};
50
+ (r.scenarios || []).forEach(function (s) { byId[s.id] = s; });
51
+ var cards = SCENARIOS.map(function (meta) { known[meta.id] = true; return scenarioCard(meta.label, byId[meta.id]); });
52
+ (r.scenarios || []).forEach(function (s) { if (!known[s.id]) cards.push(scenarioCard(s.name || s.id, s)); });
53
+ return cards.join('');
54
+ }
55
+
56
+ function scenarioCard(label, s) {
57
+ if (!s) {
58
+ return '<article class="card scenario is-idle"><header class="scenario-head"><h3>' + esc(label) + '</h3><span class="badge badge-neutral">Not run</span></header>' +
59
+ '<p class="muted">Not part of the last run. Run the full matrix to score it.</p></article>';
60
+ }
61
+ var total = (s.assertionsPassed || 0) + (s.assertionsFailed || 0);
62
+ var passed = s.status === 'passed';
63
+ var score = total ? Math.round((s.assertionsPassed || 0) / total * 100) : (passed ? 100 : 0);
64
+ var details = Object.keys(s.details || {}).slice(0, 4).map(function (k) {
65
+ var v = s.details[k];
66
+ // Lists (e.g. invariant names) get a full-width row of chips instead of a cramped value column.
67
+ if (Array.isArray(v)) {
68
+ return '<dt class="wide">' + esc(humanize(k)) + '</dt><dd class="wide">' + v.map(function (item) { return '<span class="chip">' + esc(detailValue(item)) + '</span>'; }).join('') + '</dd>';
69
+ }
70
+ return '<dt>' + esc(humanize(k)) + '</dt><dd>' + esc(detailValue(v)) + '</dd>';
71
+ }).join('');
72
+ var errors = (s.errors || []).length ? '<ul class="scenario-errors">' + s.errors.map(function (e) { return '<li>' + esc(e) + '</li>'; }).join('') + '</ul>' : '';
73
+ return '<article class="card scenario"><header class="scenario-head"><h3>' + esc(label) + '</h3>' +
74
+ '<span class="badge badge-' + (passed ? 'success' : 'danger') + '">' + (passed ? 'Passed' : 'Failed') + '</span></header>' +
75
+ (s.name && s.name !== label ? '<p class="scenario-sub">' + esc(s.name) + '</p>' : '') +
76
+ '<div class="metrics"><div><span class="metric-v">' + score + '%</span><span class="metric-l">Score</span></div>' +
77
+ '<div><span class="metric-v">' + fmtMs(s.durationMs) + '</span><span class="metric-l">Latency</span></div>' +
78
+ '<div><span class="metric-v">' + num(Math.round(s.throughputOpsPerSec || 0)) + '</span><span class="metric-l">ops/sec</span></div></div>' +
79
+ progressBar(score, 'sc-' + s.id, label + ' score', passed ? 'success' : 'danger') +
80
+ (details ? '<dl class="details">' + details + '</dl>' : '') + errors + '</article>';
81
+ }
82
+
83
+ function runBenchmark() {
84
+ if (pipe.busy.bench) return;
85
+ pipe.busy.bench = true;
86
+ repaint('benchmarks');
87
+ postJson('/api/pipeline/benchmarks/run', {}).then(function (r) {
88
+ if (r.report) { pipe.report = r.report; pipe.errors.benchmarks = null; pipe.loaded.benchmarks = true; }
89
+ var s = r.report && r.report.summary;
90
+ toast('Benchmark finished' + (s ? ': score ' + clampPct(s.score) + '%, ' + s.passedScenarios + '/' + s.totalScenarios + ' scenarios passed' : ''), 'ok');
91
+ }, function (e) {
92
+ toast('Benchmark run failed: ' + e.message);
93
+ }).then(function () {
94
+ pipe.busy.bench = false;
95
+ repaint('benchmarks');
96
+ });
97
+ }
98
+
99
+ Studio.registerPage('benchmarks', {
100
+ deps: ['benchmarks'],
101
+ render: function () { return renderBenchmarks(); },
102
+ actions: { 'bench-run': function () { runBenchmark(); } }
103
+ });
104
+ `;
@@ -0,0 +1 @@
1
+ export declare const benchmarksCss: string;
@@ -0,0 +1,30 @@
1
+ // Benchmarks view styles.
2
+ export const benchmarksCss = String.raw `/* Benchmarks */
3
+ .hero { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 20px 32px; padding: 22px; margin-bottom: 16px; transition: opacity .2s; }
4
+ .hero.is-running { opacity: .6; }
5
+ .eyebrow { margin: 0; font-size: 12px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
6
+ .hero-value { display: flex; align-items: center; gap: 10px; margin: 6px 0; font-size: 40px; font-weight: 650; letter-spacing: -0.03em; line-height: 1.1; font-variant-numeric: tabular-nums; }
7
+ .hero-value .badge { font-size: 12px; letter-spacing: 0; }
8
+ .hero-meta { margin: 0; font-size: 13.5px; color: var(--muted); }
9
+ .hero-stats { display: grid; grid-template-columns: repeat(4, minmax(96px, auto)); gap: 4px 28px; margin: 0; }
10
+ .hero-stats dt { font-size: 12.5px; color: var(--muted); }
11
+ .hero-stats dd { margin: 0; font-size: 18px; font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
12
+ .scenario-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; }
13
+ .scenario { display: flex; flex-direction: column; gap: 10px; min-width: 0; padding: 16px; }
14
+ .scenario.is-idle { box-shadow: none; border-style: dashed; }
15
+ .scenario-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
16
+ .scenario-head h3 { margin: 0; font-size: 14.5px; font-weight: 600; }
17
+ .scenario-sub { margin: -6px 0 0; font-size: 13px; color: var(--muted); }
18
+ .scenario p { margin: 0; font-size: 13.5px; line-height: 1.5; }
19
+ .metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
20
+ .metric-v { display: block; font-size: 17px; font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
21
+ .metric-l { display: block; font-size: 12px; color: var(--muted); }
22
+ .details { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 4px 12px; margin: 0; padding-top: 10px; font-size: 12.5px; border-top: 1px solid var(--border); }
23
+ .details dt { color: var(--muted); }
24
+ .details dd { margin: 0; text-align: right; font-family: var(--mono); font-size: 12px; overflow-wrap: anywhere; }
25
+ .details .wide { grid-column: 1 / -1; }
26
+ .details dd.wide { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 4px; text-align: left; }
27
+ .details dd.wide .chip { font-size: 11px; }
28
+ .scenario-errors { margin: 0; padding: 8px 10px 8px 26px; font-size: 12.5px; color: var(--danger); background: var(--danger-bg); border: 1px solid var(--danger-line); border-radius: var(--r-control); }
29
+
30
+ `;
@@ -0,0 +1 @@
1
+ export declare const databaseBrowserScript: string;
@@ -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;