Add pluggable mapper architecture and Mappers management UI#42
Merged
Add pluggable mapper architecture and Mappers management UI#42
Conversation
Co-authored-by: secondfry <400605+secondfry@users.noreply.github.com>
Co-authored-by: secondfry <400605+secondfry@users.noreply.github.com>
Co-authored-by: secondfry <400605+secondfry@users.noreply.github.com>
Co-authored-by: secondfry <400605+secondfry@users.noreply.github.com>
Co-authored-by: secondfry <400605+secondfry@users.noreply.github.com>
Copilot
AI
changed the title
[WIP] Investigate API for shortcircuit consumption
Add modular mapper architecture for multiple data sources
Feb 14, 2026
secondfry
requested changes
Feb 14, 2026
Co-authored-by: secondfry <400605+secondfry@users.noreply.github.com>
secondfry
requested changes
Feb 14, 2026
Co-authored-by: secondfry <400605+secondfry@users.noreply.github.com>
secondfry
requested changes
Feb 14, 2026
…prove integration Co-authored-by: secondfry <400605+secondfry@users.noreply.github.com>
Co-authored-by: secondfry <400605+secondfry@users.noreply.github.com>
secondfry
requested changes
Feb 14, 2026
…ate architecture docs Co-authored-by: secondfry <400605+secondfry@users.noreply.github.com>
The previous AGENTS.md only documented commit/PR message style. That left an agent walking into this repo without the context they need to be productive — no sense of what Short Circuit actually is, how to run or test it, the worker-thread/mapper-registry control flow that the wormhole fetch workflow depends on, or where the Qt-generated files come from. Agents had to rediscover all of that from scratch on every task, and were prone to editing the generated view/gui_*.py files by hand (which get clobbered on the next generate_gui run). This rewrite frontloads the non-obvious things: environment/test/build commands, the three-layer split, the app.py -> NavProcessor -> Navigation -> MapperRegistry -> MapperSource -> SolarMap chain for wormhole data fetch, the threading rule that mappers/ESI must not be called from UI handlers, and the pointer to TODO.md so agents align with deferred work instead of inventing parallel plans. The commit and PR sections are preserved but trimmed — the benefits/don't-do lists were restating the same point multiple times. Also gitignore .claude/settings.local.json, which is per-user local permission overrides and should never be shared across contributors.
PR #42 landed the MapperSource / MapperRegistry abstraction so the runtime could already fan out to any number of mapper sources, but the UI and persistence layer still hardcoded exactly one Tripwire plus a single Eve Scout toggle stuffed into the Tripwire dialog. The "table window interface for managing mapper configurations" called out in TODO.md was the last thing blocking the branch from actually delivering what the PR promised. This commit ships it. The persistence layer moves to a single QSettings key `MapperConfigs` holding a JSON list of {type, name, enabled, url, user, password} entries. This matches the mogglemoss fork's cleanest design choice — round-trips trivially, no index-gap bugs, no per-field key explosion to migrate later — while sidestepping that fork's mistakes (two redundant config dialogs, a busy-wait on the UI thread, a Tripwire singleton that leaked through the multi-instance facade). `migrate_legacy` runs once on first load and rewrites either the flat `MainWindow/tripwire_*` layout or the grouped `Tripwire/*` layout into the new list, then deletes the old keys so upgrades are transparent. If no legacy or saved configs exist, `default_configs()` provides a sensible first-run seed (Tripwire enabled, Eve Scout disabled). `Navigation.setup_mappers()` now loops over `app_obj.mapper_configs` and dispatches each entry through a `type → builder` map, so multiple Tripwire instances come for free. Unknown types are logged and skipped rather than crashing — important for forward-compat when settings from a newer version land in an older binary. `NavProcessor.finished` is `Signal(dict)` emitting the registry result verbatim, and the UI handler replaces the `(tripwire, evescout)` tuple assumption with a per-source state dict keyed by name. Renames in the Mappers dialog preserve state for kept entries and drop state for removed ones. The new `MappersDialog` is a QTableWidget with inline enable/disable checkboxes plus Add/Edit/Remove. Editing a Tripwire instance reuses the now-slimmed `TripwireDialog` (Eve Scout checkbox and logo gone, name field added) while Eve Scout editing is a simple QInputDialog since only the name is meaningfully user-editable. The dialog works on a deep-copied list so Cancel actually discards; the status bar collapses from two hardcoded labels into one aggregated `status_mappers` that renders `"Name: count"` per enabled source and colours by the worst outcome (error > ok > neutral). A separate `worker_thread.wait()` replaces the old `while isRunning(): time.sleep()` busy-loop that blocked the UI thread — found while auditing the fork and worth fixing here too. The per-instance "Test Connection" button, drag-reorder, and config import/export that the fork also implemented remain TODO; they're additive on top of this data model and not needed to close the PR.
Two fixes driven by reading the file fresh: the "still open" heading on Multiple Mapper Instance Management implied the whole section was unfinished, but the core work (add/edit/remove, list persistence, dynamic status) shipped last commit — only three enhancements remain, so the heading becomes "remaining enhancements" to match reality. Separately, Configuration Validation and Test Connection both listed credential testing, which is redundant and invites future contributors to implement it twice. Keep Test Connection under multi-instance (it's a per-instance UX feature with a natural home next to the table controls) and narrow Configuration Validation to pre-save input validation (URL format, reachability probe) with a cross-reference so the relationship is explicit.
Feedback from running the new UI: the Enabled column in the Mappers table looked sloppy because a checkable QTableWidgetItem pins its checkbox to the left of the cell, and the column is sized to fit the wider "Enabled" header, so every row had a trailing void of whitespace. Replace the checkable item with a QCheckBox inside a centered QHBoxLayout set as the cell widget — the box sits in the middle of the column regardless of header width, and the per-row toggle wires directly to configs[row].enabled via partial(self._set_enabled, row). The Tripwire Configuration dialog was worse. It inherited a 400x240 fixed-size window from the old single-instance days, which (a) squeezed the new Name row in above URL/User/Pass/Proxy, and (b) clipped the 382x100 tripwire_banner pixmap because the QGridLayout didn't have enough vertical room to honor its natural size — the banner was either vertically compressed or overlapped the first input rows depending on which constraint QGridLayout decided to respect first. Three changes fix it: unlock the dialog's maximum size (Preferred sizePolicy, min 480x420), pin the banner label to exactly 100px vertically via sizePolicy=Fixed + matching min/max height so the grid allocates it deterministically, and center-align the pixmap so the leftover ~49px of horizontal whitespace is split evenly instead of dumped on the right. Now the banner displays at its intended size and the form rows lay out normally beneath it.
The Options panel laid out Log in / Fetch Mappers / Mappers… in a 2x2 grid (login row 0 col 0, fetch row 1 col 0, config row 1 col 1), which at the main window's default width clipped both the "Log in with EvE" and "Fetch Mappers" labels mid-word. A vertical stack is more legible, matches the narrow Options column, and leaves room for future buttons without redoing the grid. Also move the "Fetch Mappers" / "Mappers…" strings into the .ui so startup doesn't need to patch the buttons via setText — the runtime overrides were a leftover from when the button text was still "Tripwire".
secondfry
pushed a commit
that referenced
this pull request
Apr 15, 2026
## Summary
Turns Short Circuit's single-Tripwire + optional-Eve-Scout hardcoding into a pluggable mapper architecture: a `MapperSource` interface, a `MapperRegistry` that fans out to every registered source, a JSON-backed configuration list persisted in `QSettings` under `MapperConfigs`, a dedicated "Mappers" management dialog (add / edit / remove / enable), a dynamic status-bar summary that adapts to whatever's configured, and one-shot migration from the legacy flat and grouped settings layouts.
## Context
The previous design wired Tripwire and Eve Scout directly into `Navigation`, `NavProcessor`, and `MainWindow`. Adding a second Tripwire server, a corp-internal mapper, or anything else meant editing all three. The UI also conflated the two — Eve Scout was a checkbox inside the Tripwire configuration dialog, the status bar had two hardcoded labels, and the fetch signal was a fixed `(int, int)` tuple. Several review comments on early revisions of this PR pointed at the same root cause: the runtime needed a generic interface, and the UI + persistence needed to move with it.
## Approach
Runtime split into `MapperSource` (abstract) and `MapperRegistry` (iterates sources, isolates per-source failures, aggregates results). `Navigation.setup_mappers()` loops over a list of `MapperConfig` dataclasses and dispatches through a `type → builder` map, so multiple Tripwire instances and mixed types work without special-casing. `NavProcessor.finished` is now `Signal(dict)` emitting `{source_name: count}` so the UI can render whatever is configured instead of two specific mappers.
Persistence uses a single `MapperConfigs` QSettings key holding a JSON list. This sidesteps the index-gap / per-field-migration problems of an array-of-keys layout and matches the cleanest approach found in the mogglemoss vibe-coded fork (other choices from that fork — plaintext token encryption, two redundant config dialogs, a UI-thread busy-wait on worker shutdown — were intentionally not copied). `migrate_legacy()` runs once on first load, reads either the historic flat `MainWindow/tripwire_*` layout or the grouped `Tripwire/*` layout, rewrites them as `MapperConfigs`, and deletes the old keys; `default_configs()` seeds first-run installs.
The UI surfaces the config list through a new `MappersDialog` (QTableWidget with centered enable checkboxes, Add / Edit / Remove, works on a deep copy so Cancel actually discards). Tripwire instances are edited via the now-slimmed `TripwireDialog` (Eve Scout checkbox removed, Name field added, banner sized correctly so the form rows don't overlap it). Eve Scout instances use a simple `QInputDialog` since only the name is meaningfully user-editable. The three "Options" buttons (Log in / Fetch Mappers / Mappers…) stack vertically for legibility at the panel's narrow width.
## Trade-offs
- **`Navigation.app_obj` still holds the configuration reference.** Moving it to a separate configuration object was considered; kept as-is because `MainWindow` is already the canonical owner of QSettings and adding a parallel configuration plumbing would be more churn than it's worth. Revisit if the coupling grows.
- **`validate_config()` stays on `MapperSource`.** Form-level validation (URL format, reachability probe) and per-instance "Test Connection" in the Mappers dialog are both tracked in `TODO.md` — they're additive on top of this data model and not blocking a functional multi-mapper experience.
- **Sequential fetch.** One slow source still blocks others within a single fetch. Parallel fetching is a known follow-up (see `TODO.md`).
- **Connection deduplication deferred.** If two sources provide the same wormhole, both get added. Tracked in `TODO.md`.
## Testing
- `pipenv run pytest src/shortcircuit/model` — 71 tests pass, including 9 new tests for `MapperConfig` JSON round-trip, legacy migration (both flat and grouped layouts, partial legacy, no-op case), and default seeding.
- Headless smoke test: `MainWindow` instantiates cleanly, legacy migration runs once on real QSettings data, `worker_thread_done({...})` produces the expected dynamic status string, deep-copy semantics in `MappersDialog` prevent Cancel from leaking edits.
- Visual verification of the Mappers dialog, Tripwire edit dialog, and stacked Options buttons on Windows.
## Risks
Low. Existing installs migrate transparently on first launch (log line `Migrated legacy mapper settings into MapperConfigs (N entries)` confirms the one-shot). Generated Qt files (`view/gui_*.py`) were regenerated via `generate_gui.bat`; `gui_mappers.ui` is now included in both `generate_gui.{bat,sh}`. `.claude/settings.local.json` is gitignored so per-developer permission overrides don't leak.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Turns Short Circuit's single-Tripwire + optional-Eve-Scout hardcoding into a pluggable mapper architecture: a
MapperSourceinterface, aMapperRegistrythat fans out to every registered source, a JSON-backed configuration list persisted inQSettingsunderMapperConfigs, a dedicated "Mappers" management dialog (add / edit / remove / enable), a dynamic status-bar summary that adapts to whatever's configured, and one-shot migration from the legacy flat and grouped settings layouts.Context
The previous design wired Tripwire and Eve Scout directly into
Navigation,NavProcessor, andMainWindow. Adding a second Tripwire server, a corp-internal mapper, or anything else meant editing all three. The UI also conflated the two — Eve Scout was a checkbox inside the Tripwire configuration dialog, the status bar had two hardcoded labels, and the fetch signal was a fixed(int, int)tuple. Several review comments on early revisions of this PR pointed at the same root cause: the runtime needed a generic interface, and the UI + persistence needed to move with it.Approach
Runtime split into
MapperSource(abstract) andMapperRegistry(iterates sources, isolates per-source failures, aggregates results).Navigation.setup_mappers()loops over a list ofMapperConfigdataclasses and dispatches through atype → buildermap, so multiple Tripwire instances and mixed types work without special-casing.NavProcessor.finishedis nowSignal(dict)emitting{source_name: count}so the UI can render whatever is configured instead of two specific mappers.Persistence uses a single
MapperConfigsQSettings key holding a JSON list. This sidesteps the index-gap / per-field-migration problems of an array-of-keys layout and matches the cleanest approach found in the mogglemoss vibe-coded fork (other choices from that fork — plaintext token encryption, two redundant config dialogs, a UI-thread busy-wait on worker shutdown — were intentionally not copied).migrate_legacy()runs once on first load, reads either the historic flatMainWindow/tripwire_*layout or the groupedTripwire/*layout, rewrites them asMapperConfigs, and deletes the old keys;default_configs()seeds first-run installs.The UI surfaces the config list through a new
MappersDialog(QTableWidget with centered enable checkboxes, Add / Edit / Remove, works on a deep copy so Cancel actually discards). Tripwire instances are edited via the now-slimmedTripwireDialog(Eve Scout checkbox removed, Name field added, banner sized correctly so the form rows don't overlap it). Eve Scout instances use a simpleQInputDialogsince only the name is meaningfully user-editable. The three "Options" buttons (Log in / Fetch Mappers / Mappers…) stack vertically for legibility at the panel's narrow width.Trade-offs
Navigation.app_objstill holds the configuration reference. Moving it to a separate configuration object was considered; kept as-is becauseMainWindowis already the canonical owner of QSettings and adding a parallel configuration plumbing would be more churn than it's worth. Revisit if the coupling grows.validate_config()stays onMapperSource. Form-level validation (URL format, reachability probe) and per-instance "Test Connection" in the Mappers dialog are both tracked inTODO.md— they're additive on top of this data model and not blocking a functional multi-mapper experience.TODO.md).TODO.md.Testing
pipenv run pytest src/shortcircuit/model— 71 tests pass, including 9 new tests forMapperConfigJSON round-trip, legacy migration (both flat and grouped layouts, partial legacy, no-op case), and default seeding.MainWindowinstantiates cleanly, legacy migration runs once on real QSettings data,worker_thread_done({...})produces the expected dynamic status string, deep-copy semantics inMappersDialogprevent Cancel from leaking edits.Risks
Low. Existing installs migrate transparently on first launch (log line
Migrated legacy mapper settings into MapperConfigs (N entries)confirms the one-shot). Generated Qt files (view/gui_*.py) were regenerated viagenerate_gui.bat;gui_mappers.uiis now included in bothgenerate_gui.{bat,sh}..claude/settings.local.jsonis gitignored so per-developer permission overrides don't leak.