Skip to content

Add pluggable mapper architecture and Mappers management UI#42

Merged
secondfry merged 16 commits intomasterfrom
copilot/explore-api-consumption
Apr 15, 2026
Merged

Add pluggable mapper architecture and Mappers management UI#42
secondfry merged 16 commits intomasterfrom
copilot/explore-api-consumption

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 14, 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.

Copilot AI and others added 5 commits February 14, 2026 14:41
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
Copilot AI requested a review from secondfry February 14, 2026 14:50
Copy link
Copy Markdown
Owner

@secondfry secondfry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot it is kinda far from done.

Comment thread examples/example_multiple_mappers.py Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread docs/MAPPER_MODULES.md
Comment thread IMPLEMENTATION_SUMMARY.md Outdated
Comment thread src/shortcircuit/model/tripwire.py Outdated
Comment thread src/shortcircuit/model/mapper_registry.py
Co-authored-by: secondfry <400605+secondfry@users.noreply.github.com>
Comment thread src/shortcircuit/model/mapper_registry.py
Comment thread src/shortcircuit/model/tripwire.py Outdated
Co-authored-by: secondfry <400605+secondfry@users.noreply.github.com>
Comment thread src/shortcircuit/model/navigation.py Outdated
Comment thread src/shortcircuit/model/navigation.py Outdated
Comment thread src/shortcircuit/model/navigation.py Outdated
Comment thread src/shortcircuit/model/navprocessor.py Outdated
Comment thread src/shortcircuit/model/navigation.py Outdated
Comment thread src/shortcircuit/model/tripwire.py Outdated
Comment thread src/shortcircuit/model/mapper_template.py Outdated
Copilot AI and others added 2 commits February 14, 2026 15:15
…prove integration

Co-authored-by: secondfry <400605+secondfry@users.noreply.github.com>
Co-authored-by: secondfry <400605+secondfry@users.noreply.github.com>
Comment thread src/shortcircuit/model/evescout.py Outdated
Comment thread src/shortcircuit/model/evescout.py Outdated
Comment thread docs/MODULE_ARCHITECTURE.md
Comment thread docs/MODULE_ARCHITECTURE.md Outdated
Comment thread docs/MODULE_ARCHITECTURE.md
Comment thread src/shortcircuit/model/navigation.py Outdated
Comment thread src/shortcircuit/model/navprocessor.py Outdated
Comment thread src/shortcircuit/model/tripwire.py Outdated
Comment thread src/shortcircuit/model/tripwire.py
Comment thread src/shortcircuit/model/tripwire.py
…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 secondfry changed the title Add modular mapper architecture for multiple data sources Add pluggable mapper architecture and Mappers management UI Apr 15, 2026
@secondfry secondfry marked this pull request as ready for review April 15, 2026 19:29
Copilot AI review requested due to automatic review settings April 15, 2026 19:29
Copy link
Copy Markdown
Owner

@secondfry secondfry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@secondfry secondfry merged commit c4849da into master Apr 15, 2026
11 checks passed
@secondfry secondfry deleted the copilot/explore-api-consumption branch April 15, 2026 19:35
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.
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.

2 participants