Skip to content

💄 v3.4.1 Clean up Dash styling, add webterm controls#238

Merged
bmeares merged 59 commits into
mainfrom
dev
Jun 17, 2026
Merged

💄 v3.4.1 Clean up Dash styling, add webterm controls#238
bmeares merged 59 commits into
mainfrom
dev

Conversation

@bmeares

@bmeares bmeares commented Jun 17, 2026

Copy link
Copy Markdown
Owner

v3.4.1

  • Scope the dbc_dark theme so it no longer leaks onto every Dash page.
    Several rule blocks in dbc_dark.css (.form-control, the .dash-dropdown-* family, and .dash-options-list) were unscoped, so their background-color: var(--bs-dark) !important declarations applied to every Dash app served by the API — forcing plugins to fight each one with higher-specificity !important overrides. These blocks are now scoped under .dbc_dark, making the dark theme opt-in.

    To keep the web console (including its body-portaled dropdown menus) unchanged, the Dash app now sets class="dbc_dark" on <body> by default. A plugin page can remove the class from <body> to render cleanly without the theme's !important overrides.

  • Fix starting jobs whose target reaches unpicklable state (e.g. an asyncio.Task).
    Starting a job (e.g. sync pipes ... --loop --name foo) could fail with TypeError: cannot pickle '_asyncio.Task' object when a connector cached a live async task. The daemon pickled its target function by value, dragging in the target's global graph; that walk reached the unpicklable state. Importable, top-level targets are now pickled by reference (re-imported in the daemon process), so the global graph is never serialized. Closures still pickle by value, unchanged.

  • Add a Termux-style on-screen key row to the web console terminal (mobile).
    On small screens the Webterm now shows a row of keys above the terminal — ESC, CTRL, SHIFT, TAB, and arrow keys laid out like a physical keyboard. CTRL/SHIFT are sticky modifiers that apply to the next keypress (tap again to toggle off), and tapping a key keeps the soft keyboard open. The row is hidden on larger screens with a physical keyboard.

  • Web console UX and accessibility improvements.

    • Loading spinners while pipes, jobs, and tokens load.
    • A confirmation dialog before deleting a job.
    • The tokens table is now horizontally scrollable on small screens.
    • Tooltips/labels on icon-only buttons and alt text on the logo and banner images.
    • Errors that were previously swallowed (CSV download, a pipe's columns / recent-data accordion) now surface their message.
  • Fix the web console fullscreen terminal rendering at half width.
    Toggling the Webterm's fullscreen button clobbered the column's responsive classes and left the terminal narrow without refitting. Fullscreen now preserves the responsive layout and the terminal refits to fill the width.

  • Fix the web console "Plugins" button raising a ValueError.
    Clicking Plugins in the dashboard raised too many values to unpack after the plugins view started returning pagination metadata. The dashboard now ignores the extra trailing values.

bmeares and others added 30 commits May 29, 2026 17:06
Disk-saving and maintenance tooling for v0.3.2.

vacuum/analyze:
- `vacuum pipes` (pipe.vacuum(full=)) reclaims dead-tuple disk space:
  VACUUM/VACUUM FULL (Postgres family, autocommit), VACUUM on TimescaleDB
  hypertables (recurses chunks), OPTIMIZE TABLE (MySQL/MariaDB), ALTER
  TABLE REBUILD (MSSQL), whole-db VACUUM (SQLite).
- `analyze pipes` (pipe.analyze()) refreshes planner statistics.
- Mirrors the compress feature end-to-end: SQLConnector methods, Pipe
  methods, instance-connector defaults, API client + routes (protected
  table guard), and the `--full` CLI flag. get_pipe_size reports reclaimed
  bytes in a stats table.

Native range partitioning (PostgreSQL/postgis):
- Opt-in via parameters['hypertable']=True on a non-TimescaleDB pipe with
  a datetime column; partition width reuses verify.chunk_minutes.
- Parent table created PARTITION BY RANGE(dt) with the partition column
  folded into a composite primary key; child partitions are pre-created
  in sync_pipe before insert, epoch-aligned for stable boundaries.
- get_pipe_size sums the partition tree (parent holds no rows).
- MySQL/MariaDB, MSSQL, and CockroachDB partitioning are staged next;
  dispatch is generic (PARTITIONABLE_FLAVORS, partition_by_column).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds on the PostgreSQL range partitioning: opt-in via
parameters['hypertable']=True on a non-TimescaleDB pipe with a datetime
column, width from verify.chunk_minutes.

MySQL/MariaDB use PARTITION BY RANGE COLUMNS(dt) on the DATETIME column
(timezone-naive literals). Since an empty RANGE-partitioned table is
invalid, the initial partitions are declared inline at CREATE TABLE
(computed from the creation dataframe); later syncs append partitions via
ALTER TABLE ADD PARTITION, walking the interval grid from the highest
existing boundary (read from information_schema.PARTITIONS) up to the new
data's max. Values below the highest boundary already land in an existing
partition. The partition column is folded into the primary key
(primary-key-first ordering so AUTO_INCREMENT stays the first key column).

get_create_table_queries gains a partition_bounds parameter; the
_partition module dispatches per family (PG_PARTITION_FLAVORS vs
MYSQL_PARTITION_FLAVORS). Verified end-to-end on mysql and mariadb
(create, upward extension, upsert, size, non-partitioned regression);
test_sync.py passes on mariadb.

MSSQL and CockroachDB partitioning remain staged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes native range partitioning across the non-TimescaleDB SQL
families. Opt-in via parameters['hypertable']=True on an MSSQL pipe with a
datetime column.

MSSQL partitions via database-level objects: a partition function
(RANGE RIGHT on DATETIMEOFFSET/DATETIME2) and a partition scheme are
created (idempotently) before the table, and the table's clustered primary
key is placed on the scheme with the partition column as the leading key
(this is what partitions the storage). Later syncs extend the function with
ALTER PARTITION SCHEME ... NEXT USED + ALTER PARTITION FUNCTION ...
SPLIT RANGE, walking the epoch-aligned grid from the highest existing
boundary (read from sys.partition_range_values) to the new data's max.
drop_pipe drops the scheme then the function so the objects don't outlive
the table and block re-creation.

get_create_table_queries gains partition_scheme_name; the _partition
module dispatches per family. Verified end-to-end on mssql (create,
SPLIT-based extension, upsert, size via dm_db_partition_stats, drop
cleanup, non-partitioned regression); test_sync.py passes on mssql.

CockroachDB partitioning (enterprise-gated, distinct syntax) remains the
only staged flavor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
get_chunk_bounds() gains an `align` parameter. When True, interior chunk
boundaries snap to the same fixed Unix-epoch grid used by native range
partitioning (SQLConnector._partition_bounds) instead of being anchored to
`begin`; the first/last chunk edges are still clamped to begin/end. This
makes verify chunks coincide with a pipe's partitions and stay
deterministic across re-syncs regardless of the requested begin.

Default is False (preserves the begin-anchored behavior for data iteration
and everything else); Pipe.verify() opts in with align=True. Verified the
aligned interior bounds match the partition grid; test_sync.py passes on
sqlite and timescaledb.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`get_parameters()` resolves reference pipes and `{{ Pipe() }}` / `MRSM{}`
symlinks on every call. It is a hot path (hit by `.dtypes`, `.columns`,
`.precision`, etc.) and the reference walk can build connectors and incur
cold-connection latency, e.g. minutes of stalls while building compress
queries on a pipe with references.

Memoize the resolved result in memory, keyed on the identity of the raw
`_attributes['parameters']` dict. Every mutation path (`update_parameters`,
the setter, `edit`) reassigns that dict to a new object, so identity change
is a reliable invalidation signal. `refresh=True` drops the memo explicitly.
Only the top-level, symlink-resolving, non-refresh call is memoized; the
result is deep-copied on return so callers that mutate it (e.g.
`infer_dtypes(persist=True)`) cannot corrupt the cache, and `_symlinks` is
stashed and restored on hits.

Schema freshness is unaffected: dynamic-schema dtypes are handled separately
by `get_columns_types()`' TTL cache, so dynamic pipes stay correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The dbc_dark.css form-control, dash-dropdown, and dash-options-list rule
blocks were unscoped, so their `background-color: var(--bs-dark) !important`
declarations leaked onto every Dash app served by the API — forcing any
plugin (e.g. the Swamp Rabbit Analytics portal) to fight each one with a
higher-specificity `!important` override.

Scope those blocks under `.dbc_dark` so the dark theme is opt-in, and set
`class="dbc_dark"` on <body> by default so the console — including its
body-portaled dropdown menus, which can't be reached by a descendant
`.dbc_dark` selector otherwise — keeps its current look. A plugin page can
now remove the class from <body> to render cleanly without `!important`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dash console quick wins, no restructuring:
- Fix duplicate invalidate_token_click name (delete-modal opener); drop dead
  table_header line and unused get_web_connector import in tokens.py
- Add title tooltips to icon-only buttons and alt text to logo/banner images
- Wrap jobs/tokens/dashboard content in dcc.Loading spinners
- Gate job deletion behind a confirmation modal (start/stop/pause unchanged)
- Surface real errors: CSV-download failures logged via warn; accordion
  columns/recent-data handlers include the exception text

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bmeares and others added 11 commits June 16, 2026 22:20
- Remove the content-div-right dcc.Loading: update_content writes both the
  webterm and content divs in one callback, so the spinner fired during the
  slow webterm startup. Jobs/tokens loaders kept.
- Tokens table: responsive=True for horizontal scroll on small screens.
- Add a mobile-only Termux-style extra-keys row above the webterm
  (ESC/CTRL/SHIFT/TAB/arrows). Arrows/ESC/TAB post raw escape sequences to the
  terminal iframe; CTRL/SHIFT are sticky modifiers applied to the next key via
  a capture-phase keydown handler in termpage.html, with button highlight
  cleared on use. Hidden on md+ (physical keyboard).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Daemon.__getstate__ pickled self.target (always `entry` for jobs) with dill
by value, serializing the target's entire global graph. That graph can reach
live, unpicklable state — e.g. a connector caching an `_asyncio.Task` — raising
"TypeError: cannot pickle '_asyncio.Task' object" when starting a job.

dill only pickles a function by reference when it can match it by identity;
with two importable copies of meerschaum (a stale install shadowing a venv) the
identity check fails and dill falls back to by-value, so byref=True alone does
not help.

Store importable, top-level targets by {module, qualname} reference and
re-import them in the daemon process instead. Closures/lambdas (run_daemon)
still pickle via dill. Backward-compatible: pickles without target_ref use the
existing dill path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
update_content unpacked exactly two values from each trigger handler, but
get_plugins_cards returns four (cards, alerts, total_pages, total_count) for
pagination, raising "ValueError: too many values to unpack (expected 2, got 4)"
when clicking Plugins. Take the first two values so trailing extras are ignored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Higher contrast: light outline buttons instead of dark secondary text.
- Smaller font (0.7rem) so labels fit.
- Lay the keys out as two rows of four on the top-left (4-col grid),
  balancing the terminal controls on the top-right; controls pinned right
  via margin-left:auto.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Keep the soft keyboard open when tapping a key button: preventDefault on
  pointerdown stops the button from stealing focus from the terminal textarea.
- Move ESC/CTRL/SHIFT/TAB to a row along the bottom, right above the webterm,
  with a buffer above; controls stay top-right.
- Move the arrow keys to the right, stacked like a real keyboard (↑ on top,
  ← ↓ → below) via a CSS grid, filling the space up to the controls.

CTRL/SHIFT already toggle off when clicked again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
They were full-width and stacked in a column. Use a 2-column grid (3.6em
columns, a touch wider than the arrow keys) so they sit two-up, two-down.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ESC/CTRL/SHIFT/TAB grid and the arrow keys sit on the left (small gap between
them); the refresh/fullscreen/new-tab controls move onto the same row, pinned
right via margin-left:auto.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The fullscreen toggle replaced content-col-right's className with a bare
col-12/col-6, clobbering its responsive classes (col-md-12 col-lg-6) and
inferring state from leftCol's display, which could leave the column at the
wrong width. The embedded terminal also never refit, keeping its old narrow
column count.

Track fullscreen state on a data attribute, drive width via inline flex/width
styles (preserving the responsive classes), and dispatch a resize on the iframe
so the terminal refits to the new width.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@bmeares bmeares marked this pull request as ready for review June 17, 2026 13:08
bmeares and others added 17 commits June 17, 2026 09:23
Explain the two dark layers (global Darkly base + class-scoped dbc_dark) under
the @web_page decorator section, and show how a plugin opts out of dbc_dark.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plugin pages inherit the console's dbc_dark component theme by default. A page
can now opt out declaratively with @web_page(dark_theme=False) instead of
hand-injecting JS to toggle the body class.

The Web Console tracks opt-out endpoints and toggles the `dbc_dark` class on
<body> per route (removing it on opted-out pages, restoring it elsewhere,
including on in-app navigation) via a dbc-dark-store + clientside callback.
Opt-out is per-page because the body class is route-scoped; there is no
plugin-wide switch.

Docs: document the two theme layers and the kwarg under @web_page. Changelog
updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously dark_theme=False only dropped the dbc_dark overrides, leaving the
global Darkly base (dark background) in place — the name over-promised. Now both
the dark (Bootswatch Darkly) and light (Flatly, v5.1.0 to match) Bootstrap themes
are loaded, with exactly one enabled per route. An opted-out page enables Flatly
and drops the dbc_dark class; the console and all other pages stay on Darkly. The
light sheet is disabled before first paint to avoid a flash.

Vendors bootstrap_light.min.css (Bootswatch Flatly 5.1.0, MIT). Docs + changelog
updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The offcanvas nav buttons forced background-color: var(--bs-dark), which showed
a jarring dark box on the light theme and a tight teal-gray box (smaller than
the row) on hover in dark. Make the buttons transparent with theme-aware text
(inherit), and move the hover highlight to the full list-group-item row using a
subtle neutral color that reads well in both themes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- The previous sidebar restyle zeroed list-group-item padding/borders globally,
  shrinking rows and dropping borders. Restore the original structure (keep
  Bootstrap padding/borders, re-add the accordion body padding rule) while
  keeping the color changes: transparent buttons, theme-aware text, subtle
  full-row hover.
- Give each plugin AccordionItem an explicit item_id. Without it, dbc's
  accordion read item_id off an undefined item (TypeError: can't access
  property "item_id"), tripping React's error boundary and remounting the
  offcanvas — which closed it and required a second click to open.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- The always_open Accordion had no active_item, so dbc called .join on an
  undefined value and read item_id off nothing (TypeError on render, seen when
  navigating). Pass a list active_item (the group holding the current page, else
  empty) so the value is always defined.
- Style the accordion group headers (e.g. "Settings") like the other nav links
  — transparent and theme-aware — instead of a dark box.
- Highlight the current page with a slightly darker "selected" shade and expand
  its accordion group. build_pages_offcanvas_children now takes the active path
  (passed from the toggle callback via mrsm-location.pathname).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The page-nav accordion still crashed on navigation (TypeError: item_id of
undefined). always_open=True is the trigger — dbc reads item_id/.join off an
undefined active_item during the mount/unmount that navigation causes. Mirror
the pipe accordion that never crashes: drop always_open/active_item, give the
Accordion an id, keep item_id on items.

Also make the accordion container/item/button transparent so the Settings group
isn't darker than the other items on the dark theme.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pages offcanvas lived inside pages_navbar, which is part of every page's
layout in page-layout-div — so navigating destroyed and recreated the offcanvas
(and its dbc accordion) each time, triggering "TypeError: can't access property
item_id" during the unmount/remount.

Hoist the single offcanvas into the persistent top-level app layout so it is
created once and survives navigation; remove it from pages_navbar. The logo in
the navbars still toggles it via the existing callback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add mrsm-location.pathname as a trigger to the offcanvas callback: navigation
(selecting a page) closes the sidebar, while the logo still toggles it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hoisting the offcanvas to the top-level layout left the old per-page copies in
the web-console layout (pages/dashboard.py) and the pipes navbar (pipes.py), so
those pages rendered two components with id "pages-offcanvas". The duplicate id
left the top-level offcanvas dead: pages with a local copy (web console, pipes)
still toggled theirs, but pages_navbar pages (jobs, tokens, plugins,
password-reset), which rely on the top-level one, stopped responding to the
logo. Keep only the single top-level offcanvas.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The combined callback fired on mrsm-location.pathname (initial load + every
navigation) while also declaring Input logo-img.n_clicks. On the initial load
the routed page isn't rendered yet, so logo-img doesn't exist — Dash raised
"A nonexistent object was used in an Input ... logo-img". Move close-on-navigation
into its own callback that only depends on the pathname; the logo toggle keeps
its own callback. MultiplexerTransform allows both to output is_open.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The page navbar (pages_navbar) and its logo/sign-out were module-level singletons
reused across every page that includes them. Reusing one component object in
multiple layouts is a Dash anti-pattern: React keeps the shared subtree mounted
and mis-reconciles its siblings on navigation, so a previous page's components
linger in the DOM. A plugin page whose URL callback lacks a pathname guard then
repaints into its lingering div — e.g. the test plugin's H1 showing on the
password-reset page.

Convert the navbar pieces to factories (build_pages_navbar / build_logo_row /
build_sign_out_button) and call them fresh at every page/use site, so navigation
fully remounts page subtrees and stale components are torn down.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…itch

Building fresh navbar objects wasn't enough: React reconciles by type + position
(not Dash id), so navigating between two pages with the same top-level shape
([Div, Container]) reused the existing DOM nodes and only patched props — leaving
the previous page's subtree (e.g. the test plugin's test-location/test-output-div)
mounted, so its unguarded URL callback kept repainting its header onto the next
page.

Wrap the routed layout in a path-keyed container so React fully unmounts the old
page and mounts the new one. Because the navbar (and its logo) now remount on
navigation, the logo's n_clicks resets and re-fires the offcanvas toggle; return
no_update for is_open there so it doesn't fight the close-on-navigation callback
and re-open the sidebar (the glitch seen navigating to Jobs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove delay_hide=1000 on the jobs page dcc.Loading so the spinner no
longer lingers a full second after content is ready, and bump the
refresh-jobs-interval from 1s to 3s to cut background churn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove delay_hide=1000 on the pipes and tokens page dcc.Loading
components, matching the jobs page fix, so the spinner no longer
lingers a full second after content is ready.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the dcc.Loading wrapper entirely on the jobs and tokens pages (and
the now-unused dcc import in tokens.py) so content paints as soon as the
page-local render callback returns.

Add api/dash/README.md documenting the hand-rolled routing model
(_paths router, per-page lazy Location rendering, the page-content key),
refresh intervals, and the delay_hide spinner pitfall for future agents.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@bmeares bmeares merged commit 074a5c7 into main Jun 17, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant