mkui 0.3.0__tar.gz → 0.3.2__tar.gz

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 (58) hide show
  1. {mkui-0.3.0 → mkui-0.3.2}/PKG-INFO +35 -6
  2. {mkui-0.3.0 → mkui-0.3.2}/README.md +34 -5
  3. {mkui-0.3.0 → mkui-0.3.2}/mkui/__init__.py +1 -1
  4. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/examples/mkio-table/config/client.toml +92 -5
  5. mkui-0.3.2/mkui/static/examples/mkio-table/server.toml +137 -0
  6. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/core.js +1 -1
  7. {mkui-0.3.0 → mkui-0.3.2}/mkui.egg-info/PKG-INFO +35 -6
  8. {mkui-0.3.0 → mkui-0.3.2}/mkui.egg-info/SOURCES.txt +2 -1
  9. {mkui-0.3.0 → mkui-0.3.2}/pyproject.toml +1 -1
  10. mkui-0.3.2/tests/test_examples.py +460 -0
  11. mkui-0.3.0/mkui/static/examples/mkio-table/server.toml +0 -77
  12. {mkui-0.3.0 → mkui-0.3.2}/LICENSE +0 -0
  13. {mkui-0.3.0 → mkui-0.3.2}/mkui/__main__.py +0 -0
  14. {mkui-0.3.0 → mkui-0.3.2}/mkui/control.py +0 -0
  15. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/examples/history/config/client.toml +0 -0
  16. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/examples/history/seed.py +0 -0
  17. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/examples/history/server.toml +0 -0
  18. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/examples/history/static/index.html +0 -0
  19. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/examples/library-js/index.html +0 -0
  20. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/examples/mkio-table/control.py +0 -0
  21. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/examples/mkio-table/seed.csv +0 -0
  22. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/examples/mkio-table/seed.py +0 -0
  23. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/examples/mkio-table/static/app.js +0 -0
  24. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/examples/mkio-table/static/index.html +0 -0
  25. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/examples/standalone-json/client.json +0 -0
  26. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/examples/standalone-json/index.html +0 -0
  27. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/auth.js +0 -0
  28. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/components/app.js +0 -0
  29. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/components/frame.js +0 -0
  30. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/components/menubar.js +0 -0
  31. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/components/statusbar.js +0 -0
  32. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/components/workspace.js +0 -0
  33. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/index.js +0 -0
  34. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/layout/drag.js +0 -0
  35. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/layout/tree.js +0 -0
  36. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/layouts.js +0 -0
  37. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/lib/copy.js +0 -0
  38. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/lib/expr.js +0 -0
  39. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/lib/expressions.js +0 -0
  40. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/lib/history.js +0 -0
  41. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/lib/icons.js +0 -0
  42. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/lib/layouts.js +0 -0
  43. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/lib/links.js +0 -0
  44. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/lib/rich.js +0 -0
  45. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/lib/timeparse.js +0 -0
  46. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/mkio-bridge.js +0 -0
  47. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/widgets/button.js +0 -0
  48. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/widgets/mkio-history.js +0 -0
  49. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/widgets/mkio-table.js +0 -0
  50. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/widgets/mkui-dialog.js +0 -0
  51. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/src/widgets/text.js +0 -0
  52. {mkui-0.3.0 → mkui-0.3.2}/mkui/static/styles/mkui.css +0 -0
  53. {mkui-0.3.0 → mkui-0.3.2}/mkui.egg-info/dependency_links.txt +0 -0
  54. {mkui-0.3.0 → mkui-0.3.2}/mkui.egg-info/entry_points.txt +0 -0
  55. {mkui-0.3.0 → mkui-0.3.2}/mkui.egg-info/top_level.txt +0 -0
  56. {mkui-0.3.0 → mkui-0.3.2}/setup.cfg +0 -0
  57. {mkui-0.3.0 → mkui-0.3.2}/tests/test_cli.py +0 -0
  58. {mkui-0.3.0 → mkui-0.3.2}/tests/test_control.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkui
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Config-driven, zero-dependency web GUI framework with dockable panes
5
5
  Author: Mark Kim
6
6
  License-Expression: Apache-2.0
@@ -211,7 +211,7 @@ mkio server and verifies its identity. Connection is two-phase:
211
211
  "name": "order-book",
212
212
  "version": "1.0",
213
213
  "protocol": "1.0",
214
- "mkio": "0.2",
214
+ "mkio": "0.3",
215
215
  "expr": "1"
216
216
  },
217
217
  "connected": { "status.message": "Connected", "status.background": null },
@@ -227,6 +227,15 @@ to the server's `_mkio` service). When `expect` is absent, the `_mkio`
227
227
  query still runs to confirm it is an mkio server and to populate
228
228
  `mkio.server.*` state paths (name, version, protocol, mkio).
229
229
 
230
+ Semver matching is stricter below 1.0, where the minor number carries the
231
+ breaking changes: `"0.3"` accepts 0.3.x and rejects 0.4.0, so pinning
232
+ `mkio` at a pre-1.0 version means every minor release of the library
233
+ invalidates the pin. A stale pin is quiet — the app connects, the
234
+ `incompatible` map is applied, and the statusbar reads "Wrong server"
235
+ while nothing is wrong with the server. Leave `mkio` out unless the app
236
+ truly depends on the library's version, and pin `name` and `version`,
237
+ which are your own, instead.
238
+
230
239
  The same reply says what the server can do, which lands in
231
240
  `mkio.server.services` (service name → protocol),
232
241
  `mkio.server.versioned` (the tables whose every change it records — see
@@ -802,7 +811,8 @@ python3 -m http.server 8000
802
811
  # http://localhost:8000/examples/library-js/
803
812
  ```
804
813
 
805
- The mkio-table example requires [mkio](https://pypi.org/project/mkio/):
814
+ The mkio-table example requires [mkio](https://pypi.org/project/mkio/) 0.3.0 or
815
+ later (its `orders` table is versioned):
806
816
 
807
817
  ```
808
818
  cd mkui/static/examples/mkio-table
@@ -824,6 +834,8 @@ Its `orders` table is `versioned = true`, so mkio records every version of every
824
834
 
825
835
  **All Orders** broadcasts the selected orders' `id` and `symbol`; the **Child Orders** frame listens for the id on its `parent_id` column and **Pending** for the symbol, so a selection filters both (the chips on each toolbar show and pause the links, and a saved layout keeps them). `python control.py` serves the example in place of `mkio serve` and rewires those links from Python a few seconds after a browser connects. The seeder places child orders under pending parents (and some under other children), so the **Order Tree** tab shows the same orders nested by `parent_id`: expand a parent with its caret, place a child order under the selected row with *+ Child Order*, and try the View menu's expand/collapse actions and alt/option-clicking a filter button for the scope row. The toolbar buttons show both styling forms: the **Pending** tab's Fill and Cancel wear a plain `style` map (green, and black on red in capitals) at all times, while the All Orders Cancel turns red only once a pending order is selected, through a rule conditioned on `enabled`.
826
836
 
837
+ The `orders` table is `versioned = true`, so mkio records every version of every row in `orders__history`, and `server.toml` writes the four services that expose it — one record's chain, where a record sits in it, the live tape, and the table as at a moment. **All Orders** names them in a `history` block and shows `_mkio_version` beside the data, which puts *Undo*, *Redo* and *As of…* on its toolbar: filling or cancelling an order moves it to a second version, Undo steps it back with a confirmation that says what will change, and *As of…* shows the book as it stood before the seeder's last few minutes, read-only until *Live* re-subscribes. Undo, Redo and *As of…* arrive with the block; the version timeline does not, so All Orders adds a **History** button of its own beside them — a `table.history` action over one selected record — and Record → History… is the same step from the menubar. Either opens that order's version timeline with its diff and blame views. Record → Audit Tape opens a parked query pane over the history table — every recorded version, newest first, live as they land — whose *Go to order* selects the record in All Orders rather than filtering it (a key All Orders' `status` and `notional` filters hide is reported back rather than revealed). An existing `orders.db` picks all this up on the next start: `auto_migrate = "safe"` creates the history table and records every live row as its baseline version.
838
+
827
839
  ## Project layout
828
840
 
829
841
  ```
@@ -861,9 +873,10 @@ mkui/ Python package (pip install mkui)
861
873
  standalone-json/ Loaded from a static config
862
874
  library-js/ Built imperatively from JS
863
875
  mkio-table/ Live table backed by mkio query/subpub services
864
- (config-only derived columns, styling rules, and
865
- enable conditions; static/app.js registers an
866
- expression function and the order-detail pane)
876
+ (config-only derived columns, styling rules,
877
+ enable conditions, and record history over a
878
+ versioned orders table; static/app.js registers
879
+ an expression function and the order-detail pane)
867
880
  history/ Versioned table: the audit tape, the version
868
881
  timeline with diff and blame, record undo/redo,
869
882
  and the table as at a moment (config only)
@@ -872,13 +885,29 @@ tests/
872
885
  layout.test.js Layout tree unit tests (node:test)
873
886
  state.test.js State + connection lifecycle tests (node:test)
874
887
  table.test.js mkio-table pane tests (node:test)
888
+ pane-filters.test.js Configured filters, sort, visible: specs and API (node:test)
889
+ links.test.js LinkHub and table-to-table linking (node:test)
890
+ history.test.js Versioned tables: capabilities, spec, chain logic (node:test)
891
+ history-pane.test.js mkio-history pane: versions, diff, blame (node:test)
892
+ copy.test.js Clipboard grids: TSV quoting and HTML (node:test)
893
+ timeparse.test.js Time detection, parsing, bounds, presets (node:test)
894
+ icons.test.js Icon library: every name resolves to an SVG (node:test)
875
895
  dialog.test.js Dialog expression + submission tests (node:test)
876
896
  auth.test.js Authentication module + state lifecycle tests (node:test)
877
897
  expressions.test.js Expression conformance fixtures + mkui wrapper tests (node:test)
878
898
  vendor-sync.test.js lib/expr.js and expr_cases.json match the installed mkio (node:test)
879
899
  rich.test.js Rich text type, mkui function library, clipboard HTML (node:test)
880
900
  styles.test.js mkui.css layout invariants (node:test)
901
+ tabbar.test.js Tab bar: drag, reorder, overflow, rename (node:test)
902
+ tab-focus.test.js Active tab group and focus routing (node:test)
903
+ edit-routing.test.js edit.* actions and shortcuts reach the pane (node:test)
904
+ pointer-guards.test.js Button/modifier guards on every drag start (node:test)
905
+ windows-menu.test.js Windows and layouts menu expansion (node:test)
881
906
  layouts.test.js Saved layouts: format, stores, workspace restore, menu (node:test)
882
907
  version.test.js The four version strings agree (node:test)
883
908
  test_cli.py CLI init/serve tests (unittest)
909
+ test_control.py ControlService: push actions to browsers (unittest)
910
+ test_examples.py Each example config against its server: the
911
+ [mkio.expect] pins, pane services, history
912
+ blocks, menu and frame pane ids (unittest)
884
913
  ```
@@ -191,7 +191,7 @@ mkio server and verifies its identity. Connection is two-phase:
191
191
  "name": "order-book",
192
192
  "version": "1.0",
193
193
  "protocol": "1.0",
194
- "mkio": "0.2",
194
+ "mkio": "0.3",
195
195
  "expr": "1"
196
196
  },
197
197
  "connected": { "status.message": "Connected", "status.background": null },
@@ -207,6 +207,15 @@ to the server's `_mkio` service). When `expect` is absent, the `_mkio`
207
207
  query still runs to confirm it is an mkio server and to populate
208
208
  `mkio.server.*` state paths (name, version, protocol, mkio).
209
209
 
210
+ Semver matching is stricter below 1.0, where the minor number carries the
211
+ breaking changes: `"0.3"` accepts 0.3.x and rejects 0.4.0, so pinning
212
+ `mkio` at a pre-1.0 version means every minor release of the library
213
+ invalidates the pin. A stale pin is quiet — the app connects, the
214
+ `incompatible` map is applied, and the statusbar reads "Wrong server"
215
+ while nothing is wrong with the server. Leave `mkio` out unless the app
216
+ truly depends on the library's version, and pin `name` and `version`,
217
+ which are your own, instead.
218
+
210
219
  The same reply says what the server can do, which lands in
211
220
  `mkio.server.services` (service name → protocol),
212
221
  `mkio.server.versioned` (the tables whose every change it records — see
@@ -782,7 +791,8 @@ python3 -m http.server 8000
782
791
  # http://localhost:8000/examples/library-js/
783
792
  ```
784
793
 
785
- The mkio-table example requires [mkio](https://pypi.org/project/mkio/):
794
+ The mkio-table example requires [mkio](https://pypi.org/project/mkio/) 0.3.0 or
795
+ later (its `orders` table is versioned):
786
796
 
787
797
  ```
788
798
  cd mkui/static/examples/mkio-table
@@ -804,6 +814,8 @@ Its `orders` table is `versioned = true`, so mkio records every version of every
804
814
 
805
815
  **All Orders** broadcasts the selected orders' `id` and `symbol`; the **Child Orders** frame listens for the id on its `parent_id` column and **Pending** for the symbol, so a selection filters both (the chips on each toolbar show and pause the links, and a saved layout keeps them). `python control.py` serves the example in place of `mkio serve` and rewires those links from Python a few seconds after a browser connects. The seeder places child orders under pending parents (and some under other children), so the **Order Tree** tab shows the same orders nested by `parent_id`: expand a parent with its caret, place a child order under the selected row with *+ Child Order*, and try the View menu's expand/collapse actions and alt/option-clicking a filter button for the scope row. The toolbar buttons show both styling forms: the **Pending** tab's Fill and Cancel wear a plain `style` map (green, and black on red in capitals) at all times, while the All Orders Cancel turns red only once a pending order is selected, through a rule conditioned on `enabled`.
806
816
 
817
+ The `orders` table is `versioned = true`, so mkio records every version of every row in `orders__history`, and `server.toml` writes the four services that expose it — one record's chain, where a record sits in it, the live tape, and the table as at a moment. **All Orders** names them in a `history` block and shows `_mkio_version` beside the data, which puts *Undo*, *Redo* and *As of…* on its toolbar: filling or cancelling an order moves it to a second version, Undo steps it back with a confirmation that says what will change, and *As of…* shows the book as it stood before the seeder's last few minutes, read-only until *Live* re-subscribes. Undo, Redo and *As of…* arrive with the block; the version timeline does not, so All Orders adds a **History** button of its own beside them — a `table.history` action over one selected record — and Record → History… is the same step from the menubar. Either opens that order's version timeline with its diff and blame views. Record → Audit Tape opens a parked query pane over the history table — every recorded version, newest first, live as they land — whose *Go to order* selects the record in All Orders rather than filtering it (a key All Orders' `status` and `notional` filters hide is reported back rather than revealed). An existing `orders.db` picks all this up on the next start: `auto_migrate = "safe"` creates the history table and records every live row as its baseline version.
818
+
807
819
  ## Project layout
808
820
 
809
821
  ```
@@ -841,9 +853,10 @@ mkui/ Python package (pip install mkui)
841
853
  standalone-json/ Loaded from a static config
842
854
  library-js/ Built imperatively from JS
843
855
  mkio-table/ Live table backed by mkio query/subpub services
844
- (config-only derived columns, styling rules, and
845
- enable conditions; static/app.js registers an
846
- expression function and the order-detail pane)
856
+ (config-only derived columns, styling rules,
857
+ enable conditions, and record history over a
858
+ versioned orders table; static/app.js registers
859
+ an expression function and the order-detail pane)
847
860
  history/ Versioned table: the audit tape, the version
848
861
  timeline with diff and blame, record undo/redo,
849
862
  and the table as at a moment (config only)
@@ -852,13 +865,29 @@ tests/
852
865
  layout.test.js Layout tree unit tests (node:test)
853
866
  state.test.js State + connection lifecycle tests (node:test)
854
867
  table.test.js mkio-table pane tests (node:test)
868
+ pane-filters.test.js Configured filters, sort, visible: specs and API (node:test)
869
+ links.test.js LinkHub and table-to-table linking (node:test)
870
+ history.test.js Versioned tables: capabilities, spec, chain logic (node:test)
871
+ history-pane.test.js mkio-history pane: versions, diff, blame (node:test)
872
+ copy.test.js Clipboard grids: TSV quoting and HTML (node:test)
873
+ timeparse.test.js Time detection, parsing, bounds, presets (node:test)
874
+ icons.test.js Icon library: every name resolves to an SVG (node:test)
855
875
  dialog.test.js Dialog expression + submission tests (node:test)
856
876
  auth.test.js Authentication module + state lifecycle tests (node:test)
857
877
  expressions.test.js Expression conformance fixtures + mkui wrapper tests (node:test)
858
878
  vendor-sync.test.js lib/expr.js and expr_cases.json match the installed mkio (node:test)
859
879
  rich.test.js Rich text type, mkui function library, clipboard HTML (node:test)
860
880
  styles.test.js mkui.css layout invariants (node:test)
881
+ tabbar.test.js Tab bar: drag, reorder, overflow, rename (node:test)
882
+ tab-focus.test.js Active tab group and focus routing (node:test)
883
+ edit-routing.test.js edit.* actions and shortcuts reach the pane (node:test)
884
+ pointer-guards.test.js Button/modifier guards on every drag start (node:test)
885
+ windows-menu.test.js Windows and layouts menu expansion (node:test)
861
886
  layouts.test.js Saved layouts: format, stores, workspace restore, menu (node:test)
862
887
  version.test.js The four version strings agree (node:test)
863
888
  test_cli.py CLI init/serve tests (unittest)
889
+ test_control.py ControlService: push actions to browsers (unittest)
890
+ test_examples.py Each example config against its server: the
891
+ [mkio.expect] pins, pane services, history
892
+ blocks, menu and frame pane ids (unittest)
864
893
  ```
@@ -2,7 +2,7 @@
2
2
 
3
3
  from pathlib import Path
4
4
 
5
- __version__ = "0.3.0"
5
+ __version__ = "0.3.2"
6
6
 
7
7
  static_dir = Path(__file__).parent / "static"
8
8
  """Path to the directory containing mkui's JS, CSS, and example files.
@@ -27,6 +27,24 @@ items = [
27
27
  { label = "Order Detail", action = "pane.show", args = "order-detail" },
28
28
  ]
29
29
 
30
+ [[menubar]]
31
+ label = "Record"
32
+ items = [
33
+ # The version timeline for whatever All Orders has selected: the chain
34
+ # as a table, with a diff and a blame view under it. One history pane
35
+ # per table, re-pointed as the selection moves rather than re-opened.
36
+ { label = "History…", action = "table.history", args = { pane = "all-orders" } },
37
+ # `keys` names the record instead, so a menu item can be a deep link.
38
+ { label = "History of order 1", action = "table.history", args = { pane = "all-orders", keys = ["1"] } },
39
+ { sep = true },
40
+ # The steps the All Orders toolbar's Undo and Redo take, on the focused
41
+ # pane. Deliberately without a shortcut: this writes to shared state.
42
+ { label = "Undo record", action = "edit.undo" },
43
+ { label = "Redo record", action = "edit.redo" },
44
+ { sep = true },
45
+ { label = "Audit Tape", action = "pane.show", args = "audit-tape" },
46
+ ]
47
+
30
48
  [[menubar]]
31
49
  label = "View"
32
50
  items = [
@@ -106,8 +124,12 @@ protocol = "query"
106
124
  # exists in no row, so it must be listed in `columns` explicitly. Derived
107
125
  # values are what the table sorts, filters, and copies. SPREAD_BPS is an
108
126
  # application function registered in static/app.js.
109
- columns = ["id", "ts", "side", "symbol", "qty", "price", "notional", "status"]
110
- labels = { ts = "Time" }
127
+ columns = ["id", "_mkio_version", "ts", "side", "symbol", "qty", "price", "notional", "status"]
128
+ # `_mkio_version` is mkio's own column — the cursor into the record's
129
+ # chain, and the one thing the row itself says about its history. Naming
130
+ # it here is what shows it; mkio's columns are hidden otherwise, and
131
+ # `labels` overrides the default heading ("Version") as for any column.
132
+ labels = { ts = "Time", _mkio_version = "Ver" }
111
133
  values = { notional = "ROUND(qty * price, 2)" }
112
134
  # Which columns show, in this order. `id` and `price` start hidden — the
113
135
  # Columns button at the header's right edge (badge: 2) opens the picker
@@ -115,14 +137,14 @@ values = { notional = "ROUND(qty * price, 2)" }
115
137
  # columns still work: `notional` is derived from the hidden
116
138
  # `price`, and sorts and filters on hidden columns still apply. Without
117
139
  # `visible`, every column shows, including ones added later.
118
- visible = ["ts", "side", "symbol", "qty", "notional", "status"]
140
+ visible = ["ts", "side", "symbol", "qty", "notional", "status", "_mkio_version"]
119
141
  # Column groups categorise the picker (sections with show/hide-the-group
120
142
  # toggles; ungrouped columns fall into "Other"). Structure only — `visible`
121
143
  # says what shows. With hundreds of columns this is how a user finds the
122
144
  # few they want; the config's `visible` is the curated default they can
123
145
  # always reset to.
124
146
  groups = [
125
- { label = "Order", columns = ["id", "ts", "side", "symbol"] },
147
+ { label = "Order", columns = ["id", "_mkio_version", "ts", "side", "symbol"] },
126
148
  { label = "Pricing", columns = ["qty", "price", "notional"] },
127
149
  ]
128
150
  # Mirror the current row into app state; the order-detail pane follows it.
@@ -169,6 +191,26 @@ side = "${BADGE(value, IF(value == 'Buy', 'green', 'red'))}"
169
191
  notional = "${NUM(value, digits: 2, group: TRUE)}"
170
192
  status = "${ICON(CASE(value == 'filled', 'check', value == 'cancelled', 'close', 'clock'))} ${TITLE(value)}"
171
193
 
194
+ # Where this table's record history lives. `versioned = true` on the
195
+ # server's orders table is what records it; mkio advertises no history
196
+ # table and writes no service for one, so this block is the only way the
197
+ # client can know — every name here is a service in server.toml.
198
+ #
199
+ # What it buys: Undo and Redo on the toolbar, stepping the selected orders
200
+ # along their own versions with a confirmation that says what will change;
201
+ # an *As of…* button that shows the table as it stood at a moment,
202
+ # read-only, until *Live* re-subscribes; and Record → History…, the
203
+ # version timeline for the selected order.
204
+ [panes.all-orders.history]
205
+ table = "orders" # checked against the server's versioned tables
206
+ key = ["id"] # asked of the server when omitted
207
+ versions = "order_versions" # one record's chain: the history pane
208
+ state = "order_state" # where the row sits in it: arms Redo
209
+ feed = "order_history" # the audit tape
210
+ undo = { service = "orders", op = "undo" }
211
+ redo = { service = "orders", op = "redo" }
212
+ asOf = "order_as_of" # the table as it stood at a moment
213
+
172
214
  [[panes.all-orders.buttons]]
173
215
  label = "+ New Order"
174
216
  enable = { connected = true }
@@ -235,6 +277,17 @@ style = [
235
277
  ]
236
278
  action = { type = "transaction", service = "orders", op = "cancel", data = { id = "${row.id}" } }
237
279
 
280
+ # Undo, Redo and "As of…" arrive on their own — the `history` block above
281
+ # is what puts them there. The version timeline does not: it opens through
282
+ # the `table.history` action, so a table that wants it next to them says
283
+ # so. `unit = "row"` means exactly one record, which is what a history is
284
+ # about; with no `keys` the action takes the pane's current selection.
285
+ [[panes.all-orders.buttons]]
286
+ label = "History"
287
+ unit = "row"
288
+ enable = { connected = true }
289
+ action = { type = "action", name = "table.history", args = { pane = "all-orders" } }
290
+
238
291
  [panes.pending]
239
292
  title = "Pending"
240
293
  type = "mkio-table"
@@ -374,6 +427,35 @@ labels = { parent_id = "Parent" }
374
427
  link.listen = { order_id = "parent_id" }
375
428
  link.broadcast = { child_id = "id" }
376
429
 
430
+ # Every recorded version of every order, newest first, arriving live as
431
+ # changes land — an ordinary query pane over the history table, with no
432
+ # history block of its own. `_mkio_op`, `_mkio_user` and `_mkio_ref` are
433
+ # mkio's own columns, shown because they are named. The pane is parked (no
434
+ # frame lists it): Record → Audit Tape opens it in a frame of its own.
435
+ [panes.audit-tape]
436
+ title = "Audit Tape"
437
+ type = "mkio-table"
438
+ service = "order_history"
439
+ protocol = "query"
440
+ columns = ["_mkio_ref", "id", "_mkio_version", "_mkio_op", "_mkio_user", "side", "symbol", "qty", "price", "status"]
441
+ labels = { _mkio_ref = "When", _mkio_version = "Ver", id = "Order", _mkio_user = "Changed by" }
442
+ sort = "-_mkio_ref"
443
+
444
+ [panes.audit-tape.display]
445
+ # The ref is a UTC timestamp; REF_TIME reads it in the browser's zone.
446
+ _mkio_ref = "${REF_TIME(value, fmt: '%H:%M:%S', tz: 'local')}"
447
+ _mkio_op = "${BADGE(value, CASE(value == 'insert', 'green', value == 'baseline', 'gray', '#0e639c'))}"
448
+ _mkio_user = "${value ?? MUTED('(system)')}"
449
+
450
+ # Filtering would narrow what you are looking at; this jumps to the record
451
+ # instead, leaving All Orders showing everything around it. A key its
452
+ # filters hide is reported back rather than revealed, so an order below
453
+ # the pane's `notional` floor says so instead of silently doing nothing.
454
+ [[panes.audit-tape.buttons]]
455
+ label = "Go to order"
456
+ enable = { minSelected = 1, maxSelected = 1 }
457
+ action = { type = "action", name = "table.select", args = { pane = "all-orders", keys = ["${row.id}"] } }
458
+
377
459
  [panes.last-trade]
378
460
  title = "Last Trade"
379
461
  type = "mkio-table"
@@ -417,11 +499,16 @@ url = "ws://localhost:8080/ws"
417
499
  # it too, so the subscription always succeeds.
418
500
  control = "_mkui"
419
501
 
502
+ # What this app requires of the server it connects to; a mismatch applies
503
+ # `incompatible` below. Below 1.0 semver matching is exact-minor, so the
504
+ # `mkio` pin has to be bumped with every minor release of the library —
505
+ # tests/test_examples.py fails when it goes stale, rather than letting the
506
+ # statusbar quietly read "Wrong server".
420
507
  [mkio.expect]
421
508
  name = "order-book"
422
509
  version = "1.0"
423
510
  protocol = "1.0"
424
- mkio = "0.2"
511
+ mkio = "0.3"
425
512
  expr = "1"
426
513
 
427
514
  [mkio.connected]
@@ -0,0 +1,137 @@
1
+ name = "order-book"
2
+ version = "1.0.0"
3
+ port = 8080
4
+ host = "0.0.0.0"
5
+ db_path = "orders.db"
6
+ # Apply safe schema changes (like adding a column) to an existing orders.db.
7
+ auto_migrate = "safe"
8
+
9
+ [tables.orders]
10
+ # `ts` is stamped by SQLite in UTC (`YYYY-MM-DD HH:MM:SS`), a format the
11
+ # table recognises as a time, so its filter dropdown offers a date range.
12
+ # `parent_id` nests child orders under a parent (the Order Tree pane): a
13
+ # row whose parent_id is empty is a top-level order.
14
+ columns = { id = "INTEGER PRIMARY KEY AUTOINCREMENT", ts = "TEXT DEFAULT CURRENT_TIMESTAMP", side = "TEXT NOT NULL", symbol = "TEXT NOT NULL", qty = "INTEGER", price = "REAL DEFAULT 0", status = "TEXT DEFAULT 'pending'", parent_id = "INTEGER" }
15
+ # `versioned = true` is the whole switch: mkio numbers every version of
16
+ # every row (`_mkio_version`, 1 on insert and +1 per edit) and records it
17
+ # in a companion `orders__history` table, stamped with the ref, the user,
18
+ # and the service the change came through. `auto_migrate = "safe"` above
19
+ # turns it on for an orders.db that predates this line — the history table
20
+ # is created and every live row recorded as its baseline version.
21
+ versioned = true
22
+
23
+ # Write endpoint: place and manage orders
24
+ [services.orders]
25
+ protocol = "transaction"
26
+
27
+ [services.orders.ops]
28
+ new = [{ table = "orders", op_type = "insert", fields = ["side", "symbol", "qty", "price"] }]
29
+ child = [{ table = "orders", op_type = "insert", fields = ["side", "symbol", "qty", "price", "parent_id"] }]
30
+ fill = [{ table = "orders", op_type = "update", key = ["id"], fields = ["status"], defaults = { status = "filled" } }]
31
+ cancel = [{ table = "orders", op_type = "update", key = ["id"], fields = ["status"], defaults = { status = "cancelled" } }]
32
+ # undo/redo take only the key — the values come from the history table —
33
+ # and record no new version, so a step is always reversible. Undoing
34
+ # version 1 removes the row and keeps its versions, so redo can rebuild it.
35
+ undo = [{ table = "orders", op_type = "undo", key = ["id"] }]
36
+ redo = [{ table = "orders", op_type = "redo", key = ["id"] }]
37
+
38
+ # Live table of all orders (mkui subscribes here)
39
+ [services.all_orders]
40
+ protocol = "query"
41
+ primary_table = "orders"
42
+ filterable = ["status", "symbol"]
43
+
44
+ # Last filled trade per symbol
45
+ [services.last_trade]
46
+ protocol = "subpub"
47
+ primary_table = "orders"
48
+ topic = "symbol"
49
+ where = "status == 'filled'"
50
+
51
+ [services.last_trade.publish]
52
+ symbol = "symbol"
53
+ side = "side"
54
+ price = "price"
55
+ qty = "qty"
56
+
57
+ [services.last_trade.defaults]
58
+ symbol = "_mkio_topic"
59
+
60
+ # ─── Record history ───────────────────────────────────────────────────
61
+ # mkio never advertises a history table and writes no service for one, so
62
+ # these four are the deliberate choice to expose it. Each would take its
63
+ # own `access` rule on a server with authentication enabled — audit
64
+ # visibility is a permission like any other.
65
+
66
+ # One record's chain, oldest first: what the history pane reads.
67
+ [services.order_versions]
68
+ protocol = "reqrep"
69
+ sql = "SELECT * FROM orders__history WHERE id = :id ORDER BY _mkio_version"
70
+
71
+ # Where a record sits in its chain: the version its live row is on, and the
72
+ # highest recorded. `current` is null once the row has been undone away.
73
+ # This is what tells the client whether redo has anywhere to go.
74
+ [services.order_state]
75
+ protocol = "reqrep"
76
+ sql = "SELECT (SELECT _mkio_version FROM orders WHERE id = :id) AS current, MAX(_mkio_version) AS top FROM orders__history WHERE id = :id"
77
+
78
+ # The audit tape: every recorded version, newest first, live as they land.
79
+ # The seeder fills and cancels orders as it runs, so the chains build up
80
+ # on their own.
81
+ [services.order_history]
82
+ protocol = "query"
83
+ primary_table = "orders__history"
84
+ sql = "SELECT * FROM orders__history ORDER BY _mkio_ref DESC"
85
+ filterable = ["id", "symbol", "status", "_mkio_op", "_mkio_user"]
86
+ change_log_size = 10000
87
+
88
+ # The table as it stood at a moment: for each id, the newest version
89
+ # recorded at or before `as_of`. Refs sort lexicographically, so the cutoff
90
+ # is an ordinary range scan.
91
+ #
92
+ # Reconstructed from recorded versions, which is not quite the same as
93
+ # "what the table showed then": undo and redo move a row's cursor without
94
+ # recording anything, so a row undone at 3pm still reads, as of 2pm, as
95
+ # whatever its newest version by 2pm was.
96
+ [services.order_as_of]
97
+ protocol = "reqrep"
98
+ sql = """
99
+ SELECT h.* FROM orders__history h
100
+ JOIN (SELECT id, MAX(_mkio_version) AS v
101
+ FROM orders__history
102
+ WHERE _mkio_ref <= :as_of
103
+ GROUP BY id) latest
104
+ ON h.id = latest.id AND h._mkio_version = latest.v
105
+ ORDER BY h.id
106
+ """
107
+
108
+ # Saved window layouts for mkui's Layout menu: one row per save, the
109
+ # newest being the current layout (`delete` is how the client prunes the
110
+ # history). With no login on this server every save goes to the default
111
+ # history (owner "").
112
+ [tables.mkui_layouts]
113
+ columns = { id = "INTEGER PRIMARY KEY AUTOINCREMENT", app = "TEXT NOT NULL DEFAULT ''", owner = "TEXT NOT NULL DEFAULT ''", saved = "TEXT DEFAULT CURRENT_TIMESTAMP", layout = "TEXT NOT NULL" }
114
+
115
+ [services.mkui_layouts]
116
+ protocol = "transaction"
117
+
118
+ [services.mkui_layouts.ops]
119
+ save = [{ table = "mkui_layouts", op_type = "insert", fields = ["app", "owner", "layout"] }]
120
+ delete = [{ table = "mkui_layouts", op_type = "delete", key = ["id"] }]
121
+
122
+ [services.mkui_layouts_list]
123
+ protocol = "reqrep"
124
+ sql = "SELECT id, saved FROM mkui_layouts WHERE app = :app AND owner = :owner ORDER BY id DESC"
125
+
126
+ [services.mkui_layouts_get]
127
+ protocol = "reqrep"
128
+ sql = "SELECT id, saved, layout FROM mkui_layouts WHERE id = :id"
129
+
130
+ # Serve frontend: app at /, mkui framework assets at /mkui/
131
+ [static]
132
+ "/" = "./static"
133
+ "/mkui" = "../../"
134
+
135
+ # Serve config files
136
+ [config]
137
+ "/config" = "./config"
@@ -4,7 +4,7 @@
4
4
  // The state store is intentionally tiny: it's a Proxy over a plain object,
5
5
  // supports dot-path get/set, and notifies subscribers per path.
6
6
 
7
- export const VERSION = "0.3.0";
7
+ export const VERSION = "0.3.2";
8
8
  export function version() { return VERSION; }
9
9
 
10
10
  import { registerExprFunction, registerExprLibrary, registerExprType, expr } from "./lib/expressions.js";
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkui
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Config-driven, zero-dependency web GUI framework with dockable panes
5
5
  Author: Mark Kim
6
6
  License-Expression: Apache-2.0
@@ -211,7 +211,7 @@ mkio server and verifies its identity. Connection is two-phase:
211
211
  "name": "order-book",
212
212
  "version": "1.0",
213
213
  "protocol": "1.0",
214
- "mkio": "0.2",
214
+ "mkio": "0.3",
215
215
  "expr": "1"
216
216
  },
217
217
  "connected": { "status.message": "Connected", "status.background": null },
@@ -227,6 +227,15 @@ to the server's `_mkio` service). When `expect` is absent, the `_mkio`
227
227
  query still runs to confirm it is an mkio server and to populate
228
228
  `mkio.server.*` state paths (name, version, protocol, mkio).
229
229
 
230
+ Semver matching is stricter below 1.0, where the minor number carries the
231
+ breaking changes: `"0.3"` accepts 0.3.x and rejects 0.4.0, so pinning
232
+ `mkio` at a pre-1.0 version means every minor release of the library
233
+ invalidates the pin. A stale pin is quiet — the app connects, the
234
+ `incompatible` map is applied, and the statusbar reads "Wrong server"
235
+ while nothing is wrong with the server. Leave `mkio` out unless the app
236
+ truly depends on the library's version, and pin `name` and `version`,
237
+ which are your own, instead.
238
+
230
239
  The same reply says what the server can do, which lands in
231
240
  `mkio.server.services` (service name → protocol),
232
241
  `mkio.server.versioned` (the tables whose every change it records — see
@@ -802,7 +811,8 @@ python3 -m http.server 8000
802
811
  # http://localhost:8000/examples/library-js/
803
812
  ```
804
813
 
805
- The mkio-table example requires [mkio](https://pypi.org/project/mkio/):
814
+ The mkio-table example requires [mkio](https://pypi.org/project/mkio/) 0.3.0 or
815
+ later (its `orders` table is versioned):
806
816
 
807
817
  ```
808
818
  cd mkui/static/examples/mkio-table
@@ -824,6 +834,8 @@ Its `orders` table is `versioned = true`, so mkio records every version of every
824
834
 
825
835
  **All Orders** broadcasts the selected orders' `id` and `symbol`; the **Child Orders** frame listens for the id on its `parent_id` column and **Pending** for the symbol, so a selection filters both (the chips on each toolbar show and pause the links, and a saved layout keeps them). `python control.py` serves the example in place of `mkio serve` and rewires those links from Python a few seconds after a browser connects. The seeder places child orders under pending parents (and some under other children), so the **Order Tree** tab shows the same orders nested by `parent_id`: expand a parent with its caret, place a child order under the selected row with *+ Child Order*, and try the View menu's expand/collapse actions and alt/option-clicking a filter button for the scope row. The toolbar buttons show both styling forms: the **Pending** tab's Fill and Cancel wear a plain `style` map (green, and black on red in capitals) at all times, while the All Orders Cancel turns red only once a pending order is selected, through a rule conditioned on `enabled`.
826
836
 
837
+ The `orders` table is `versioned = true`, so mkio records every version of every row in `orders__history`, and `server.toml` writes the four services that expose it — one record's chain, where a record sits in it, the live tape, and the table as at a moment. **All Orders** names them in a `history` block and shows `_mkio_version` beside the data, which puts *Undo*, *Redo* and *As of…* on its toolbar: filling or cancelling an order moves it to a second version, Undo steps it back with a confirmation that says what will change, and *As of…* shows the book as it stood before the seeder's last few minutes, read-only until *Live* re-subscribes. Undo, Redo and *As of…* arrive with the block; the version timeline does not, so All Orders adds a **History** button of its own beside them — a `table.history` action over one selected record — and Record → History… is the same step from the menubar. Either opens that order's version timeline with its diff and blame views. Record → Audit Tape opens a parked query pane over the history table — every recorded version, newest first, live as they land — whose *Go to order* selects the record in All Orders rather than filtering it (a key All Orders' `status` and `notional` filters hide is reported back rather than revealed). An existing `orders.db` picks all this up on the next start: `auto_migrate = "safe"` creates the history table and records every live row as its baseline version.
838
+
827
839
  ## Project layout
828
840
 
829
841
  ```
@@ -861,9 +873,10 @@ mkui/ Python package (pip install mkui)
861
873
  standalone-json/ Loaded from a static config
862
874
  library-js/ Built imperatively from JS
863
875
  mkio-table/ Live table backed by mkio query/subpub services
864
- (config-only derived columns, styling rules, and
865
- enable conditions; static/app.js registers an
866
- expression function and the order-detail pane)
876
+ (config-only derived columns, styling rules,
877
+ enable conditions, and record history over a
878
+ versioned orders table; static/app.js registers
879
+ an expression function and the order-detail pane)
867
880
  history/ Versioned table: the audit tape, the version
868
881
  timeline with diff and blame, record undo/redo,
869
882
  and the table as at a moment (config only)
@@ -872,13 +885,29 @@ tests/
872
885
  layout.test.js Layout tree unit tests (node:test)
873
886
  state.test.js State + connection lifecycle tests (node:test)
874
887
  table.test.js mkio-table pane tests (node:test)
888
+ pane-filters.test.js Configured filters, sort, visible: specs and API (node:test)
889
+ links.test.js LinkHub and table-to-table linking (node:test)
890
+ history.test.js Versioned tables: capabilities, spec, chain logic (node:test)
891
+ history-pane.test.js mkio-history pane: versions, diff, blame (node:test)
892
+ copy.test.js Clipboard grids: TSV quoting and HTML (node:test)
893
+ timeparse.test.js Time detection, parsing, bounds, presets (node:test)
894
+ icons.test.js Icon library: every name resolves to an SVG (node:test)
875
895
  dialog.test.js Dialog expression + submission tests (node:test)
876
896
  auth.test.js Authentication module + state lifecycle tests (node:test)
877
897
  expressions.test.js Expression conformance fixtures + mkui wrapper tests (node:test)
878
898
  vendor-sync.test.js lib/expr.js and expr_cases.json match the installed mkio (node:test)
879
899
  rich.test.js Rich text type, mkui function library, clipboard HTML (node:test)
880
900
  styles.test.js mkui.css layout invariants (node:test)
901
+ tabbar.test.js Tab bar: drag, reorder, overflow, rename (node:test)
902
+ tab-focus.test.js Active tab group and focus routing (node:test)
903
+ edit-routing.test.js edit.* actions and shortcuts reach the pane (node:test)
904
+ pointer-guards.test.js Button/modifier guards on every drag start (node:test)
905
+ windows-menu.test.js Windows and layouts menu expansion (node:test)
881
906
  layouts.test.js Saved layouts: format, stores, workspace restore, menu (node:test)
882
907
  version.test.js The four version strings agree (node:test)
883
908
  test_cli.py CLI init/serve tests (unittest)
909
+ test_control.py ControlService: push actions to browsers (unittest)
910
+ test_examples.py Each example config against its server: the
911
+ [mkio.expect] pins, pane services, history
912
+ blocks, menu and frame pane ids (unittest)
884
913
  ```
@@ -51,4 +51,5 @@ mkui/static/src/widgets/mkui-dialog.js
51
51
  mkui/static/src/widgets/text.js
52
52
  mkui/static/styles/mkui.css
53
53
  tests/test_cli.py
54
- tests/test_control.py
54
+ tests/test_control.py
55
+ tests/test_examples.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mkui"
7
- version = "0.3.0"
7
+ version = "0.3.2"
8
8
  description = "Config-driven, zero-dependency web GUI framework with dockable panes"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"