pgkiln 0.48.0 → 0.50.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 (63) hide show
  1. package/SECURITY.md +3 -2
  2. package/db/migrations/116_dump_jobs.sql +41 -0
  3. package/db/migrations/117_rest_handlers.sql +63 -0
  4. package/db/migrations/118_change_log.sql +54 -0
  5. package/docs/apex-feature-parity.md +9 -9
  6. package/docs/guide/01-installation.md +7 -0
  7. package/docs/guide/03-builder.md +227 -10
  8. package/docs/guide/08-security.md +11 -0
  9. package/docs/guide/12-development.md +64 -13
  10. package/docs/guide/13-rest-api.md +59 -1
  11. package/docs/guide/15-extensions.md +1 -1
  12. package/examples/hr/hr_62_rest_module.sql +39 -0
  13. package/package.json +1 -1
  14. package/public/app.css +1 -1
  15. package/public/builder-icons.svg +2 -0
  16. package/public/builder.css +478 -173
  17. package/public/builder.js +48 -0
  18. package/public/code-editor.css +32 -79
  19. package/public/code-editor.js +58 -3
  20. package/public/data-modeler.css +17 -14
  21. package/public/sql-monitor.js +73 -0
  22. package/public/worksheet.css +14 -7
  23. package/public/worksheet.js +119 -3
  24. package/src/app.ts +6 -0
  25. package/src/automations.ts +6 -0
  26. package/src/binds.ts +2 -1
  27. package/src/builder/advisor.ts +24 -2
  28. package/src/builder/autorest.ts +3 -2
  29. package/src/builder/changelog.ts +240 -0
  30. package/src/builder/components.ts +1 -1
  31. package/src/builder/data-modeler.ts +1 -1
  32. package/src/builder/datapump.ts +364 -0
  33. package/src/builder/db-users.ts +2 -2
  34. package/src/builder/ddl-tools.ts +13 -4
  35. package/src/builder/find-workspace.ts +2 -2
  36. package/src/builder/object-browser.ts +4 -4
  37. package/src/builder/plan-view.ts +25 -0
  38. package/src/builder/querybuilder.ts +1 -1
  39. package/src/builder/quicksql.ts +1 -1
  40. package/src/builder/rest-modules.ts +629 -0
  41. package/src/builder/rest.ts +1 -0
  42. package/src/builder/routes.ts +4 -2
  43. package/src/builder/scheduling.ts +495 -0
  44. package/src/builder/scripts.ts +1 -1
  45. package/src/builder/sql-charts.ts +1 -1
  46. package/src/builder/sql-monitor.ts +397 -0
  47. package/src/builder/sql-tools.ts +6 -2
  48. package/src/builder/sql-utilities.ts +7 -39
  49. package/src/builder/sql.ts +6 -0
  50. package/src/builder/table-editor.ts +6 -3
  51. package/src/builder/top-sql.ts +24 -6
  52. package/src/builder/ui.ts +6 -4
  53. package/src/builder/worksheet.ts +173 -41
  54. package/src/changelog.ts +109 -0
  55. package/src/cron-schedule.ts +282 -0
  56. package/src/datapump.ts +546 -0
  57. package/src/dumpscript.ts +407 -0
  58. package/src/pgcron.ts +225 -0
  59. package/src/runtime/rest-handlers.ts +75 -0
  60. package/src/runtime/rest.ts +116 -22
  61. package/src/session.ts +5 -0
  62. package/src/sqlworksheet.ts +9 -6
  63. package/src/worksheet-txn.ts +270 -0
package/public/builder.js CHANGED
@@ -1234,3 +1234,51 @@ document.querySelectorAll('input[data-filter-tree]').forEach((el) => {
1234
1234
  // the button is for pages without JavaScript
1235
1235
  picker.form.querySelector('button')?.setAttribute('hidden', '');
1236
1236
  })();
1237
+
1238
+ // (sprint 101, DA-12) SQL Workshop › Scheduling: the schedule builder's five fields and the common schedules fill the
1239
+ // schedule; the preview (in words, next five runs) follows as you type. Without JavaScript the Use / Preview buttons
1240
+ // post the form and it comes back with the same.
1241
+ (() => {
1242
+ const form = document.querySelector('form[data-cron-form]');
1243
+ const input = form?.querySelector('[data-cron-schedule]');
1244
+ const preview = form?.querySelector('[data-cron-preview]');
1245
+ if (!form || !input || !preview) return;
1246
+ const fields = [...form.querySelectorAll('[data-cron-field]')];
1247
+ let timer = 0;
1248
+ let seq = 0;
1249
+ const refresh = () => {
1250
+ clearTimeout(timer);
1251
+ timer = setTimeout(async () => {
1252
+ const mine = ++seq;
1253
+ try {
1254
+ const res = await fetch(`${preview.dataset.cronPreview}?schedule=${encodeURIComponent(input.value)}`, { headers: { accept: 'text/html' }, credentials: 'same-origin' });
1255
+ if (!res.ok || mine !== seq) return;
1256
+ const doc = new DOMParser().parseFromString(`<div>${await res.text()}</div>`, 'text/html');
1257
+ preview.replaceChildren(...doc.body.firstElementChild.childNodes);
1258
+ } catch {
1259
+ /* the Preview button still works */
1260
+ }
1261
+ }, 250);
1262
+ };
1263
+ const split = () => {
1264
+ const parts = input.value.trim().split(/\s+/);
1265
+ if (parts.length === 5) fields.forEach((f, i) => (f.value = parts[i] === '*' ? '' : parts[i]));
1266
+ };
1267
+ input.addEventListener('input', () => {
1268
+ split();
1269
+ refresh();
1270
+ });
1271
+ for (const f of fields) {
1272
+ f.addEventListener('input', () => {
1273
+ input.value = fields.map((x) => x.value.trim() || '*').join(' ');
1274
+ refresh();
1275
+ });
1276
+ }
1277
+ form.querySelector('[data-cron-preset]')?.addEventListener('change', (e) => {
1278
+ if (!e.target.value) return;
1279
+ input.value = e.target.value;
1280
+ split();
1281
+ refresh();
1282
+ });
1283
+ for (const b of form.querySelectorAll('[data-cron-nojs]')) b.hidden = true;
1284
+ })();
@@ -2,79 +2,23 @@
2
2
  transparent and lies exactly over the highlighted copy (.ce-code), so both
3
3
  must share font, size, line height, padding and tab size. */
4
4
  :root {
5
- --ce-bg: #fbfcfd;
6
- --ce-gutter: #f1f3f6;
7
- --ce-gutter-text: #5d6876;
8
- --ce-text: #1b2330;
9
- --ce-kw: #8b2bb9;
10
- --ce-type: #0b6e8a;
11
- --ce-fn: #1d5fc4;
12
- --ce-str: #1d7a3a;
13
- --ce-num: #b35400;
14
- --ce-com: #6b7685;
15
- --ce-bind: #b0306a;
16
- --ce-qid: #7a5b00;
17
- --ce-tag: #1d5fc4;
18
- --ce-attr: #8b2bb9;
19
- --ce-err: #c23028;
20
- --ce-match: rgb(11 99 197 / 18%);
21
- --ce-select: rgb(11 99 197 / 22%);
5
+ /* the colours (--ce-bg, --ce-kw, …, --ce-line, --ce-select) are tokens in builder.css, light and dark */
22
6
  --ce-font: 0.84rem;
23
7
  --ce-lh: 20px; /* whole pixels: fractional line heights drift apart over thousands of lines */
24
8
  }
25
- @media (prefers-color-scheme: dark) {
26
- :root:not([data-theme="light"]) {
27
- --ce-bg: #12161d;
28
- --ce-gutter: #171c24;
29
- --ce-gutter-text: #8f99a8;
30
- --ce-text: #e3e7ed;
31
- --ce-kw: #d39bff;
32
- --ce-type: #5fd0e6;
33
- --ce-fn: #82b6ff;
34
- --ce-str: #8fd694;
35
- --ce-num: #ffb46b;
36
- --ce-com: #8591a3;
37
- --ce-bind: #ff8cc0;
38
- --ce-qid: #e8c66a;
39
- --ce-tag: #82b6ff;
40
- --ce-attr: #d39bff;
41
- --ce-err: #ff7a70;
42
- --ce-match: rgb(105 164 255 / 25%);
43
- --ce-select: rgb(105 164 255 / 30%);
44
- }
45
- }
46
- :root[data-theme="dark"] {
47
- --ce-bg: #12161d;
48
- --ce-gutter: #171c24;
49
- --ce-gutter-text: #8f99a8;
50
- --ce-text: #e3e7ed;
51
- --ce-kw: #d39bff;
52
- --ce-type: #5fd0e6;
53
- --ce-fn: #82b6ff;
54
- --ce-str: #8fd694;
55
- --ce-num: #ffb46b;
56
- --ce-com: #8591a3;
57
- --ce-bind: #ff8cc0;
58
- --ce-qid: #e8c66a;
59
- --ce-tag: #82b6ff;
60
- --ce-attr: #d39bff;
61
- --ce-err: #ff7a70;
62
- --ce-match: rgb(105 164 255 / 25%);
63
- --ce-select: rgb(105 164 255 / 30%);
64
- }
65
9
  @media (pointer: coarse) {
66
10
  :root { --ce-font: 16px; --ce-lh: 24px; } /* the same as other fields: no zoom on focus in iOS */
67
11
  }
68
12
 
69
13
  .ce {
70
14
  position: relative; display: grid; grid-template-columns: auto minmax(0, 1fr);
71
- min-width: 0; max-width: 100%; border: 1px solid var(--border); border-radius: 6px; background: var(--ce-bg);
15
+ min-width: 0; max-width: 100%; border: 1px solid var(--field-border, var(--border)); border-radius: var(--radius-sm, 6px); background: var(--ce-bg);
72
16
  }
73
- .ce:focus-within { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
17
+ .ce:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--ring, transparent); }
74
18
  .ce-gutter {
75
19
  --ce-digits: 2;
76
20
  width: calc(var(--ce-digits) * 1ch + 1.1rem); overflow: hidden; position: relative;
77
- background: var(--ce-gutter); border-right: 1px solid var(--border); border-radius: 6px 0 0 0;
21
+ background: var(--ce-gutter); border-right: 1px solid var(--border); border-radius: var(--radius-sm, 6px) 0 0 0;
78
22
  color: var(--ce-gutter-text); user-select: none;
79
23
  }
80
24
  .ce-gutter, .ce .ce-code, .ce textarea.ce-input {
@@ -82,6 +26,11 @@
82
26
  font-variant-ligatures: none; letter-spacing: 0; font-kerning: none;
83
27
  }
84
28
  .ce-nums { position: absolute; top: 0; left: 0; right: 0; white-space: pre; text-align: right; padding: 0.5rem 0.45rem 0 0; will-change: transform; }
29
+ /* the caret's line (sprint 108): a band behind the text and its number in the gutter, while the editor has the focus */
30
+ .ce-cur, .ce-cur-num { position: absolute; left: 0; right: 0; top: 0; height: var(--ce-lh); will-change: transform; display: none; }
31
+ .ce-cur { background: var(--ce-line); }
32
+ .ce-cur-num { padding-right: 0.45rem; text-align: right; color: var(--ce-gutter-active); background: var(--ce-gutter); font-weight: 650; }
33
+ .ce:focus-within :is(.ce-cur, .ce-cur-num):not([hidden]) { display: block; }
85
34
  .ce-main { position: relative; min-width: 0; }
86
35
  .ce .ce-hl { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
87
36
  .ce .ce-code {
@@ -99,29 +48,33 @@
99
48
  .ce textarea.ce-input:focus-visible { outline: none; }
100
49
  .ce textarea.sql-editor { min-height: 18rem; }
101
50
 
102
- /* tokens */
103
- .ce-code .t-kw { color: var(--ce-kw); font-weight: 600; }
104
- .ce-code .t-type { color: var(--ce-type); }
105
- .ce-code .t-fn { color: var(--ce-fn); }
106
- .ce-code .t-str { color: var(--ce-str); }
107
- .ce-code .t-key { color: var(--ce-fn); }
108
- .ce-code .t-num { color: var(--ce-num); }
109
- .ce-code .t-com { color: var(--ce-com); font-style: italic; }
110
- .ce-code .t-bind, .ce-code .t-sub { color: var(--ce-bind); font-weight: 600; }
111
- .ce-code .t-qid { color: var(--ce-qid); }
112
- .ce-code .t-dol { color: var(--ce-str); font-weight: 600; }
113
- .ce-code .t-tag { color: var(--ce-tag); }
114
- .ce-code .t-attr { color: var(--ce-attr); }
115
- .ce-code .t-ent { color: var(--ce-num); }
116
- .ce-code .t-err { color: var(--ce-err); text-decoration: wavy underline; }
117
- .ce-code .t-op, .ce-code .t-pun, .ce-code .t-br, .ce-code .t-id { color: var(--ce-text); }
118
- .ce-code .t-match { background: var(--ce-match); border-radius: 2px; outline: 1px solid var(--ce-match); }
51
+ /* tokens: in the editor and in read-only code (<pre data-code-view>, sprint 108) */
52
+ :is(.ce-code, .ce-view) .t-kw { color: var(--ce-kw); font-weight: 600; }
53
+ :is(.ce-code, .ce-view) .t-type { color: var(--ce-type); }
54
+ :is(.ce-code, .ce-view) .t-fn { color: var(--ce-fn); }
55
+ :is(.ce-code, .ce-view) .t-str { color: var(--ce-str); }
56
+ :is(.ce-code, .ce-view) .t-key { color: var(--ce-fn); }
57
+ :is(.ce-code, .ce-view) .t-num { color: var(--ce-num); }
58
+ :is(.ce-code, .ce-view) .t-com { color: var(--ce-com); font-style: italic; }
59
+ :is(.ce-code, .ce-view) .t-bind, :is(.ce-code, .ce-view) .t-sub { color: var(--ce-bind); font-weight: 600; }
60
+ :is(.ce-code, .ce-view) .t-qid { color: var(--ce-qid); }
61
+ :is(.ce-code, .ce-view) .t-dol { color: var(--ce-str); font-weight: 600; }
62
+ :is(.ce-code, .ce-view) .t-tag { color: var(--ce-tag); }
63
+ :is(.ce-code, .ce-view) .t-attr { color: var(--ce-attr); }
64
+ :is(.ce-code, .ce-view) .t-ent { color: var(--ce-num); }
65
+ :is(.ce-code, .ce-view) .t-err { color: var(--ce-err); text-decoration: wavy underline; }
66
+ :is(.ce-code, .ce-view) .t-op { color: var(--ce-op); }
67
+ :is(.ce-code, .ce-view) .t-pun, :is(.ce-code, .ce-view) .t-br, :is(.ce-code, .ce-view) .t-id { color: var(--ce-text); }
68
+ .ce-code .t-match { background: var(--ce-match); border-radius: 2px; outline: 1px solid var(--ce-match-ring); }
69
+
70
+ pre.ce-view { background: var(--ce-bg); color: var(--ce-text); border-color: var(--border); }
71
+ pre.ce-view::selection, pre.ce-view ::selection { background: var(--ce-select); }
119
72
 
120
73
  /* status bar: language, hint, caret position, check */
121
74
  .ce-bar {
122
75
  grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: center; gap: 0.2rem 0.75rem;
123
76
  padding: 0.2rem 0.5rem; border-top: 1px solid var(--border); background: var(--ce-gutter);
124
- border-radius: 0 0 6px 6px; font-size: 0.75rem; color: var(--muted); min-width: 0;
77
+ border-radius: 0 0 var(--radius-sm, 6px) var(--radius-sm, 6px); font-size: 0.75rem; color: var(--muted); min-width: 0;
125
78
  }
126
79
  .ce-lang { font-weight: 650; letter-spacing: 0.02em; }
127
80
  .ce-hint { flex: 1 1 12rem; min-width: 0; }
@@ -138,7 +91,7 @@
138
91
  position: absolute; z-index: 30; left: 0; top: 0; margin: 0; padding: 0.25rem 0; list-style: none;
139
92
  width: max-content; min-width: 14rem; max-width: min(34rem, calc(100% - 0.5rem)); max-height: 15rem; overflow: auto;
140
93
  background: var(--surface); color: var(--text); border: 1px solid var(--border); border-radius: 6px;
141
- box-shadow: 0 8px 24px rgb(16 24 40 / 18%); font-size: 0.82rem;
94
+ box-shadow: var(--shadow-lg, 0 8px 24px rgb(16 24 40 / 18%)); font-size: 0.82rem;
142
95
  }
143
96
  .ce-opt { display: flex; align-items: baseline; gap: 0.75rem; padding: 0.2rem 0.6rem 0.2rem 0.4rem; cursor: pointer; white-space: nowrap; }
144
97
  .ce-opt::before {
@@ -15,6 +15,8 @@
15
15
  // - "Check" plans the SQL like the Advisor (POST /builder/code/check)
16
16
  // - screen readers keep the plain textarea; the list is announced through
17
17
  // aria-activedescendant and a polite live region
18
+ // - (sprint 108) read-only code, <pre data-code-view="sql|plpgsql|json|html">, is
19
+ // highlighted with the same lexer and colours (DDL, generated SQL)
18
20
  //
19
21
  // Strict CSP: no inline styles; geometry is set through the CSSOM. Suggestions
20
22
  // and highlighted text are built as text nodes, never parsed as HTML.
@@ -437,15 +439,20 @@
437
439
  gutter.setAttribute('aria-hidden', 'true');
438
440
  const nums = document.createElement('div');
439
441
  nums.className = 'ce-nums';
440
- gutter.append(nums);
442
+ // the caret's line: a band behind the text, its number over the gutter's
443
+ const curNum = document.createElement('div');
444
+ curNum.className = 'ce-cur-num';
445
+ gutter.append(nums, curNum);
441
446
  const main = document.createElement('div');
442
447
  main.className = 'ce-main';
443
448
  const hl = document.createElement('div');
444
449
  hl.className = 'ce-hl';
445
450
  hl.setAttribute('aria-hidden', 'true');
451
+ const cur = document.createElement('div');
452
+ cur.className = 'ce-cur';
446
453
  const inner = document.createElement('pre');
447
454
  inner.className = 'ce-code';
448
- hl.append(inner);
455
+ hl.append(cur, inner);
449
456
  ta.parentNode.insertBefore(wrap, ta);
450
457
  main.append(hl, ta);
451
458
  wrap.append(gutter, main);
@@ -502,7 +509,7 @@
502
509
  ta.setAttribute('aria-autocomplete', 'list');
503
510
  ta.setAttribute('aria-haspopup', 'listbox');
504
511
  ta.setAttribute('aria-controls', list.id);
505
- Object.assign(this, { wrap, gutter, nums, main, hl, inner, list, bar, langEl: lang, hint, pos, msg, check, live, suggest });
512
+ Object.assign(this, { wrap, gutter, nums, cur, curNum, main, hl, inner, list, bar, langEl: lang, hint, pos, msg, check, live, suggest });
506
513
  this.applyLang();
507
514
  }
508
515
 
@@ -661,9 +668,27 @@
661
668
  this.digits = digits;
662
669
  this.gutter.style.setProperty('--ce-digits', String(Math.max(2, digits)));
663
670
  }
671
+ this.placeLine();
664
672
  if (this.pop) this.place();
665
673
  }
666
674
 
675
+ /** The current-line band and number: on the caret's line; hidden while a selection spans lines. */
676
+ placeLine() {
677
+ const ta = this.ta;
678
+ const lines = this.lineStarts;
679
+ const a = lowerBound(lines, ta.selectionStart + 1);
680
+ const b = lowerBound(lines, ta.selectionEnd + 1);
681
+ const hide = a !== b;
682
+ this.cur.hidden = hide;
683
+ this.curNum.hidden = hide;
684
+ if (hide) return;
685
+ const { lh, padTop } = this.metrics();
686
+ const y = padTop + (a - 1) * lh - ta.scrollTop;
687
+ this.cur.style.transform = `translateY(${y}px)`;
688
+ this.curNum.style.transform = `translateY(${y}px)`;
689
+ this.curNum.textContent = String(a);
690
+ }
691
+
667
692
  tokenAt(off) {
668
693
  this.sync();
669
694
  const { T, S, E } = this.toks;
@@ -703,6 +728,7 @@
703
728
  this.sync();
704
729
  if (this.pop && this.ta.selectionStart !== this.ta.selectionEnd) this.close();
705
730
  if (this.updateMatch()) this.schedule(true);
731
+ else this.placeLine();
706
732
  const c = this.ta.selectionStart;
707
733
  const line = lowerBound(this.lineStarts, c + 1);
708
734
  this.pos.textContent = `Ln ${line}, Col ${c - this.lineStarts[line - 1] + 1}`;
@@ -997,12 +1023,41 @@
997
1023
  return lo;
998
1024
  }
999
1025
 
1026
+ /** Highlight a read-only <pre data-code-view>: its text, as spans (text nodes only). Long ones stay plain. */
1027
+ function colorize(pre) {
1028
+ const lang = pre.dataset.codeView in LANG_LABEL ? pre.dataset.codeView : 'text';
1029
+ pre.classList.add('ce-view');
1030
+ const text = pre.textContent;
1031
+ if (lang === 'text' || text.length > 200000) return;
1032
+ const { T, S, E } = lex(lang, text);
1033
+ const frag = document.createDocumentFragment();
1034
+ let pos = 0;
1035
+ for (let k = 0; k < T.length; k++) {
1036
+ const st = Math.max(S[k], pos);
1037
+ if (E[k] <= st) continue;
1038
+ if (st > pos) frag.append(text.slice(pos, st));
1039
+ const span = document.createElement('span');
1040
+ span.className = `t-${T[k]}`;
1041
+ span.textContent = text.slice(st, E[k]);
1042
+ frag.append(span);
1043
+ pos = E[k];
1044
+ }
1045
+ if (pos < text.length) frag.append(text.slice(pos));
1046
+ pre.replaceChildren(frag);
1047
+ }
1048
+
1000
1049
  const editors = new WeakMap();
1050
+ const viewed = new WeakSet();
1001
1051
  function enhance(root) {
1002
1052
  for (const ta of (root || document).querySelectorAll('textarea[data-code]')) {
1003
1053
  if (editors.has(ta) || ta.closest('.ce')) continue;
1004
1054
  editors.set(ta, new Editor(ta));
1005
1055
  }
1056
+ for (const pre of (root || document).querySelectorAll('pre[data-code-view]')) {
1057
+ if (viewed.has(pre)) continue;
1058
+ viewed.add(pre);
1059
+ colorize(pre);
1060
+ }
1006
1061
  }
1007
1062
  window.pgkilnCodeEditor = { enhance, editorOf: (ta) => editors.get(ta) };
1008
1063
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => enhance());
@@ -8,19 +8,20 @@
8
8
  .dm-toolbar .btn[disabled] { opacity: 0.5; }
9
9
  .dm-status { min-width: 0; }
10
10
  .dm-stage { position: relative; }
11
- .dm-canvas { overflow: auto; max-width: 100%; height: 65vh; min-height: 18rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); }
11
+ .dm-canvas { overflow: auto; max-width: 100%; height: 65vh; min-height: 18rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--canvas, var(--bg)); box-shadow: var(--shadow-xs, none); }
12
12
  .dm-canvas:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
13
13
  .dm-svg { display: block; }
14
- .dm-svg .dm-bg { fill: var(--bg); }
15
- .dm-svg .dm-frame { fill: var(--surface); stroke: var(--border); }
16
- .dm-svg .dm-view .dm-frame { fill: var(--surface-2); }
17
- .dm-svg .dm-head { fill: var(--accent-soft); stroke: var(--border); }
18
- .dm-svg .dm-title { fill: var(--text); }
19
- .dm-svg .dm-col { fill: var(--text); }
20
- .dm-svg .dm-type, .dm-svg .dm-kind, .dm-svg .dm-more { fill: var(--muted); }
21
- .dm-svg .dm-key { fill: var(--gold, #b45309); }
22
- .dm-svg .dm-line, .dm-svg .dm-end { stroke: var(--muted); }
23
- .dm-svg .dm-opt { fill: var(--bg); stroke: var(--muted); }
14
+ /* the boxes and lines: no .dm-svg ancestor in the selectors, so the mini-map's <use> copy (a shadow tree) is themed too */
15
+ .dm-svg .dm-bg { fill: var(--canvas, var(--bg)); }
16
+ .dm-frame { fill: var(--block, var(--surface)); stroke: var(--border-strong, var(--border)); }
17
+ .dm-view .dm-frame { fill: var(--surface-2); }
18
+ path.dm-head { fill: var(--accent-soft); stroke: var(--border-strong, var(--border)); }
19
+ .dm-title { fill: var(--text); }
20
+ .dm-col { fill: var(--text); }
21
+ :is(.dm-type, .dm-kind, .dm-more) { fill: var(--muted); }
22
+ .dm-key { fill: var(--gold, #b45309); }
23
+ :is(.dm-line, .dm-end) { stroke: var(--muted); }
24
+ .dm-opt { fill: var(--canvas, var(--bg)); stroke: var(--muted); }
24
25
  .dm-svg .dm-fk:hover .dm-line, .dm-svg .dm-fk:hover .dm-end { stroke: var(--accent); stroke-width: 2.5; }
25
26
  .dm-svg .dm-link:hover .dm-title, .dm-svg .dm-link:focus .dm-title { fill: var(--accent); text-decoration: underline; }
26
27
  .dm-live .dm-box { cursor: grab; touch-action: none; }
@@ -29,9 +30,9 @@
29
30
  .dm-svg .dm-box:focus-visible .dm-frame { stroke: var(--accent); stroke-width: 2.5; }
30
31
  .dm-svg .dm-dragging .dm-frame { stroke: var(--accent); }
31
32
  .dm-svg text { user-select: none; }
32
- .dm-minimap { position: absolute; right: 12px; bottom: 12px; width: 200px; height: 140px; border: 1px solid var(--border); border-radius: var(--radius);
33
- background: var(--surface); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); cursor: pointer; touch-action: none; }
34
- .dm-minimap-bg { fill: var(--surface); }
33
+ .dm-minimap { position: absolute; right: 12px; bottom: 12px; width: 200px; height: 140px; border: 1px solid var(--border-strong, var(--border)); border-radius: var(--radius);
34
+ background: var(--canvas, var(--surface)); box-shadow: var(--shadow-md, 0 2px 8px rgb(0 0 0 / 20%)); cursor: pointer; touch-action: none; overflow: hidden; }
35
+ .dm-minimap-bg { fill: var(--canvas, var(--surface)); }
35
36
  .dm-minimap-view { fill: var(--accent); fill-opacity: 0.15; stroke: var(--accent); stroke-width: 4; vector-effect: non-scaling-stroke; }
36
37
  .dm-legend { margin: 6px 0 12px; }
37
38
  .dm-search-row { display: flex; gap: 6px; flex-wrap: wrap; }
@@ -54,3 +55,5 @@
54
55
  .dm-svg { max-width: 100%; height: auto; }
55
56
  .dm-dict-table { break-inside: avoid-page; }
56
57
  }
58
+ /* sprint 108: the Mini-map toggle shows that it is on */
59
+ .dm-toolbar .btn[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
@@ -0,0 +1,73 @@
1
+ // pgkiln builder: the SQL Workshop's SQL Monitor (sprint 102, src/builder/sql-monitor.ts).
2
+ //
3
+ // Progressive enhancement of the Sessions view: the live part ([data-sm-live]: tiles, sessions, wait events,
4
+ // progress) is fetched again (?fragment=1) every 5, 15 or 60 seconds and swapped in place, with a Pause button.
5
+ // The interval and the pause are remembered in localStorage (pgkiln.sqlMonitor). No refresh while the tab is
6
+ // hidden or a session's confirmation is open; a failed or redirected fetch (signed out) stops it. Without
7
+ // JavaScript the page's Refresh link reloads it.
8
+ (() => {
9
+ 'use strict';
10
+ const live = document.querySelector('[data-sm-live]');
11
+ const controls = document.querySelector('[data-sm-controls]');
12
+ if (!live || !controls) return;
13
+ const select = controls.querySelector('[data-sm-interval]');
14
+ const pause = controls.querySelector('[data-sm-pause]');
15
+ const KEY = 'pgkiln.sqlMonitor';
16
+ const load = () => { try { return JSON.parse(localStorage.getItem(KEY) || 'null') || {}; } catch { return {}; } };
17
+ const save = (v) => { try { localStorage.setItem(KEY, JSON.stringify(v)); } catch { /* private mode */ } };
18
+ const pref = load();
19
+ const allowed = [...select.options].map((o) => Number(o.value));
20
+ let seconds = allowed.includes(Number(pref.seconds)) ? Number(pref.seconds) : allowed[0];
21
+ let paused = pref.paused === true;
22
+ let timer = 0;
23
+ let busy = false;
24
+ select.value = String(seconds);
25
+ controls.hidden = false;
26
+
27
+ const show = () => {
28
+ pause.setAttribute('aria-pressed', paused ? 'true' : 'false');
29
+ pause.textContent = paused ? 'Resume' : 'Pause';
30
+ live.classList.toggle('is-paused', paused);
31
+ };
32
+ const schedule = () => {
33
+ window.clearTimeout(timer);
34
+ if (!paused) timer = window.setTimeout(refresh, seconds * 1000);
35
+ };
36
+ async function refresh() {
37
+ if (busy || paused) return;
38
+ if (document.hidden) return schedule();
39
+ busy = true;
40
+ try {
41
+ const res = await fetch(live.dataset.smLive, { credentials: 'same-origin', headers: { accept: 'text/html' }, cache: 'no-store' });
42
+ if (!res.ok || res.redirected) throw new Error(String(res.status));
43
+ const text = await res.text();
44
+ // keep focus on a button that survives the swap (by its label)
45
+ const focused = document.activeElement && live.contains(document.activeElement) ? document.activeElement.getAttribute('aria-label') : null;
46
+ live.innerHTML = text;
47
+ if (focused) live.querySelector(`[aria-label="${CSS.escape(focused)}"]`)?.focus();
48
+ busy = false;
49
+ schedule();
50
+ } catch {
51
+ busy = false;
52
+ paused = true;
53
+ show();
54
+ const note = live.querySelector('[data-sm-updated]');
55
+ if (note) note.textContent = 'Refresh stopped: the monitor could not be reached (signed out?). Reload the page.';
56
+ }
57
+ }
58
+ select.addEventListener('change', () => {
59
+ seconds = Number(select.value);
60
+ save({ seconds, paused });
61
+ schedule();
62
+ });
63
+ pause.addEventListener('click', () => {
64
+ paused = !paused;
65
+ save({ seconds, paused });
66
+ show();
67
+ if (paused) window.clearTimeout(timer);
68
+ else refresh();
69
+ });
70
+ document.addEventListener('visibilitychange', () => { if (!document.hidden && !paused) refresh(); });
71
+ show();
72
+ schedule();
73
+ })();
@@ -60,8 +60,8 @@
60
60
  .ws-grid .null { color: var(--muted); font-style: italic; }
61
61
  .ws-log { margin: 0; padding: 8px; background: var(--code-bg); border: 1px solid var(--border); border-radius: var(--radius); font-size: 12.5px; line-height: 1.45; white-space: pre; overflow: auto; }
62
62
  .ws-log-sql { color: var(--muted); }
63
- .ws-log-ok { color: #2f7d32; font-weight: 600; }
64
- .ws-log-err { color: #c0392b; white-space: pre-wrap; }
63
+ .ws-log-ok { color: var(--success); font-weight: 600; }
64
+ .ws-log-err { color: var(--danger); white-space: pre-wrap; }
65
65
  .ws-log-warn { color: var(--warn); white-space: pre-wrap; }
66
66
  .ws-notices, .ws-history { list-style: none; margin: 0; padding: 0; font-size: 13px; }
67
67
  .ws-notices li, .ws-history li { display: flex; gap: 8px; align-items: baseline; padding: 4px 0; border-bottom: 1px solid var(--border); min-width: 0; }
@@ -72,7 +72,7 @@
72
72
  .ws-status { display: flex; align-items: center; gap: 12px; padding: 4px 10px; border-top: 1px solid var(--border); background: var(--surface-2); font-size: 12.5px; min-width: 0; }
73
73
  .ws-status .icon { width: 13px; height: 13px; vertical-align: -2px; }
74
74
  .ws-count { color: var(--muted); white-space: nowrap; }
75
- .ws-count.is-err { color: #c0392b; }
75
+ .ws-count.is-err { color: var(--danger); }
76
76
  .ws-count.is-warn { color: var(--warn); }
77
77
  .ws-last { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
78
78
  .ws-time { margin-left: auto; }
@@ -156,8 +156,8 @@ th[aria-sort=descending] .ws-sort::after { content: ' ▼'; font-size: 0.75em; }
156
156
  .ws-hist-bar { display: flex; gap: 6px; margin-bottom: 6px; }
157
157
  .ws-hist-bar input { flex: 1; min-width: 0; }
158
158
  .ws-hist-icon .icon { width: 14px; height: 14px; }
159
- .ws-hist-icon.is-ok { color: #2f7d32; }
160
- .ws-hist-icon.is-err { color: #c0392b; }
159
+ .ws-hist-icon.is-ok { color: var(--success); }
160
+ .ws-hist-icon.is-err { color: var(--danger); }
161
161
  .ws-history li { align-items: center; }
162
162
  .ws-history .btn { flex: none; }
163
163
  @media (max-width: 640px) { .ws-hist-meta { display: none; } }
@@ -166,8 +166,8 @@ th[aria-sort=descending] .ws-sort::after { content: ' ▼'; font-size: 0.75em; }
166
166
  .ws-wtabs { display: none; align-items: stretch; gap: 0; overflow-x: auto; border-bottom: 1px solid var(--border); background: var(--surface-2); flex: none; }
167
167
  .js .ws-wtabs { display: flex; }
168
168
  .ws-wtab { display: flex; align-items: center; border-right: 1px solid var(--border); }
169
- .ws-wtab [role=tab] { border: 0; background: none; color: var(--muted); padding: 5px 4px 5px 10px; font: inherit; font-size: 12.5px; cursor: pointer; white-space: nowrap; max-width: 14rem; overflow: hidden; text-overflow: ellipsis; }
170
- .ws-wtab [role=tab][aria-selected=true] { color: var(--text); box-shadow: inset 0 -2px 0 var(--hot); }
169
+ .ws-wtab [data-ws-wtab] { border: 0; background: none; color: var(--muted); padding: 5px 4px 5px 10px; font: inherit; font-size: 12.5px; cursor: pointer; white-space: nowrap; max-width: 14rem; overflow: hidden; text-overflow: ellipsis; }
170
+ .ws-wtab [data-ws-wtab][aria-current=true] { color: var(--text); box-shadow: inset 0 -2px 0 var(--hot); }
171
171
  .ws-wtab-close, .ws-wtab-new { border: 0; background: none; color: var(--muted); cursor: pointer; padding: 2px 8px; font-size: 14px; line-height: 1; }
172
172
  .ws-wtab-close:hover, .ws-wtab-new:hover { color: var(--text); }
173
173
  .ws-file > summary { list-style: none; max-width: 14rem; }
@@ -291,3 +291,10 @@ details.menu.obj-menu > summary::before, details.menu.obj-menu > summary::after
291
291
  .ch-card { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); padding: 10px 12px; min-width: 0; }
292
292
  .ch-card h2 { font-size: 14px; margin: 0 0 6px; }
293
293
  .ch-edit { max-width: 48rem; margin-top: 16px; }
294
+
295
+ /* (sprint 107) transaction mode: Autocommit / Transaction, the badge, Commit and Rollback */
296
+ .ws-txn { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px; min-width: 0; }
297
+ .ws-txn-badge { font-size: 12px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--border); color: var(--muted); max-width: 22rem; overflow-wrap: anywhere; }
298
+ .ws-txn-badge[data-state="open"] { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 55%, var(--border)); background: color-mix(in srgb, var(--warn) 10%, transparent); font-weight: 600; }
299
+ .ws-txn-badge[data-state="aborted"] { color: var(--danger); border-color: var(--danger); background: var(--danger-soft); font-weight: 600; }
300
+ .ws-txn-alert { margin: 6px 8px; }