Last updated: 2026-08-01
Compiled from user feedback: AA1GM, KC1UIX, W1BKW, W1MTW, N1GSK, KC1JMH
Canonical location:
docs/ROADMAP.md.
Pruning policy: completed items are removed from this file, not struck through. The changelog is the record of what shipped; this file is the record of what has not. Before deleting an item, confirm its user-facing outcome is in
docs/CHANGELOG.mdand any convention or decision worth keeping has been moved todocs/DEVELOPMENT.mdordocs/DESIGN.md. This file does not keep a per-prune narrative of what was removed or why — that history lives indocs/CHANGELOG.md(user-facing outcome) and git history (implementation detail), not here.
Items are grouped by milestone tier, then by theme within each tier. Each item carries a type tag:
- 🐛 Bug — confirmed broken behavior
- 🔧 Improvement — working feature that needs polish
- ✨ Feature — new capability
- 🔬 Investigative — needs reproduction before scoping
Priority within each tier is roughly top-to-bottom. Items from conversations are attributed to their source where useful for context.
As of rev 25, each item carries a Model: line recommending which Claude model a sub-agent should use to implement it:
- Haiku — single-file, mechanical, precisely specified changes (delete dead code, fix a known line, add a badge column). Safe once the task is spelled out exactly.
- Sonnet — multi-file features and refactors that follow an established pattern with a clear spec. The workhorse tier for this codebase.
- Opus — architecture, security-sensitive design, data modeling, and anything touching auth/payments/time handling. Also used as a review gate on Sonnet work where noted.
Rule of thumb: Haiku and Sonnet can only maintain this codebase safely once files are small and patterns are extracted. That groundwork shipped with Milestone 0.4 (2026-07-06), so Milestone 1 items can now be assigned to smaller models as their Model: lines indicate.
The bulk of this milestone (0.1 confirmed bugs, 0.2 orphaned code, 0.3 guardrails, 0.4 the modularity and componentization program) completed between 2026-07-03 and 2026-07-06 and has been pruned. What it delivered: a test suite and CI pipeline, React error boundaries, WebSocket resilience, SMTP timeouts, IANA timezone validation, composite indexes, shared frontend hooks, app/permissions.py, the backend router facades, and the frontend page splits. The patterns those splits established are documented in docs/DEVELOPMENT.md ("Backend router-split (facade) pattern", "Frontend component-split pattern", "Post-split verification checklist").
Milestone 0 is complete as of 2026-07-29. Every section has shipped and been pruned. Section numbers are not reused, so commit messages and docs referencing "Milestone 0.4" or "Milestone 0.7" still resolve against the changelog. New codebase-health work should open a new section here rather than reopening a pruned one.
Meaningful new capabilities that don't require architectural changes.
✨ MFA / TOTP authenticator support (KC1JMH)
Model: Opus for the security design (secret at-rest encryption, recovery-code storage/hashing, login-flow changes); Sonnet for implementation against that design. Do not hand any part of this to Haiku — auth mistakes are silent until exploited.
Let users enroll a TOTP authenticator as a second factor. During enrollment, show both the QR code and the plain-text preshared secret (base32) with a copy button, so users can store the secret in a password vault (Bitwarden, 1Password) instead of a dedicated authenticator app if they prefer.
Requirements:
- New columns on
User:totp_secret(with at-rest encryption considerations),totp_enabled(bool), and storage for backup/recovery codes. - Enrollment flow: generate a secret, render the QR from an
otpauth://URI and display the secret string with a copy button, then verify a code before enabling. - Verification step at login (after magic-link / OAuth) when
totp_enabledis set. - Backup/recovery codes for account recovery, plus a disable-MFA flow.
- Suggested libraries:
pyotpfor TOTP,qrcodefor QR generation (or render theotpauth://URI to a QR client-side).
Downstream (was its own "Admin Tooling" roadmap section; folded in here 2026-07-30 since it's entirely blocked on this feature): once totp_enabled exists, add an MFA-enrolled badge to the Admin → Users list (Model: Haiku — a single read of the new column plus a badge column, no query design work) so the operator can see who can participate in authenticated nets. Surface it as a padlock badge consistent with the check-in authentication indicator (see Authenticated nets below). The NCS and changelog-subscriber indicators that shipped alongside this item 2026-07-30 are already live (see docs/CHANGELOG.md).
✨ Authenticated nets — station identity verification via TOTP (KC1JMH)
Model: Sonnet for implementation, with an Opus review gate on the expected-code endpoint (it deliberately reveals a station's current TOTP to NCS — the permission gating and never-send-the-secret rule must be airtight).
Builds directly on MFA. Adds a per-net "Authenticated net" toggle in the Edit Net settings, with subtext explaining that it lets check-ins prove their identity to net control. When enabled, a checked-in user can read the current code from their authenticator to NCS; an action button on the check-in row shows NCS the expected code for that station so they can confirm it matches and mark the station as identity-verified for the net.
Requirements / design questions:
- New
authenticated(bool) toggle on the net/template, with descriptive subtext in the Edit Net toggles section. - Action button on check-in rows (NCS-only) that displays the currently-valid expected TOTP for the station's account.
- Server endpoint: given the net and the check-in's user, return the currently-valid expected code(s) with a small skew window — permission-gated to NCS (and possibly Logger). The raw secret must never be sent to the client; only the computed code.
- Record an "identity verified" state on the check-in once NCS affirms the match.
- Authentication status indicator — each check-in row shows a padlock next to (or overlaid on) the station's profile icon: a closed padlock once identity is verified, an open padlock when it is not. This makes a station's authentication state readable at a glance for everyone viewing the net.
- Unenrolled stations — if a user checks into an authenticated net without having enrolled MFA, they simply show the open-padlock (unauthenticated) state. ECTLogger does not block or auto-reject them; it is left to NCS to decide how to respond. The indicator just makes the unauthenticated status visible.
- Verification is only offered on nets flagged
authenticated; on all other nets no padlock is shown. - Dependency: requires the MFA / TOTP support above to exist first.
✨ Optional Ko-fi supporter integration, admin-configured per deployment (sustainability)
Model: Phase 1 (config + support page) Sonnet; Phase 2 webhook handler Sonnet with Opus review (inbound payment webhooks are an attack surface — token verification, replay, refund handling); Phase 3 (progress bar/copy) Haiku; Phase 4 (About-modal credits) Sonnet.
Prerequisite: Help Menu and About modal shipped in rev 22. The subtle "Support" entry point lives inside the About modal.
A subtle, never-obtrusive, never-gated way for operators to help cover an instance's hosting and development costs. ECTLogger stays 100% free; support is a quiet opt-in side door, not a paywall, modal, or nag. Because ECTLogger is open source and self-hosted by others, this ships as a generic integration any operator can point at their own Ko-fi account from the Admin panel — never hardcoded to one account, and disabled by default so a fresh clone shows nothing until configured.
Platform decision — Ko-fi, single platform.
Ko-fi is the choice for US-based operators specifically: it lets a creator link both Stripe and PayPal side-by-side, while Buy Me a Coffee is Stripe-only and Stripe US cannot process PayPal — disqualifying it for the older, PayPal/Venmo-trusting ham audience. Ko-fi also takes 0% on one-time tips (only the ~2.9% + $0.30 processor fee) and 5% on recurring memberships unless the operator pays Ko-fi Gold ($6/mo). Running two platforms at once was explicitly rejected: it creates donor choice-paralysis, double webhook maintenance, and fragmented goal tracking. (Source: design conversation, June 2026.)
Deployment-configurable settings (open-source requirement).
New columns on the AppSettings singleton (see DEVELOPMENT.md "AppSettings singleton pattern"), all editable from a new Support / Funding section in the Admin panel (gated by the existing role != ADMIN → 403 check):
| Setting | Type / default | Public read? | Purpose |
|---|---|---|---|
kofi_enabled |
bool, default false |
yes | Master switch; gates the entire feature |
kofi_username |
string, nullable | yes | Builds the Ko-fi page / widget URL |
kofi_webhook_token |
string, nullable | no — write-only | Verifies inbound webhooks |
kofi_hosting_goal_amount |
int, nullable | yes | Monthly progress-bar target (per deployment) |
kofi_hosting_goal_currency |
string, default USD |
yes | Goal display currency |
kofi_support_message |
text, nullable (may be blank) | yes | Operator's own pitch; blank renders a sensible default |
Secret handling: the public GET /settings response (readable before login) must never return kofi_webhook_token. Expose a boolean kofi_webhook_configured instead; the raw token is settable only via the admin PUT. The Admin panel also displays this deployment's fixed webhook URL (https://<your-host>/api/webhooks/donations) for the operator to paste into their own Ko-fi dashboard.
Phase 1 — Config + subtle surface (not started)
- Migration: add the six
kofi_*columns toAppSettings - Extend
AppSettingsResponse/AppSettingsUpdateschemas and thePUT /settingshandler, enforcing the write-only rule forkofi_webhook_token - Admin panel "Support / Funding" section (set values; show the deployment webhook URL)
- "Support" link added inside the About modal — the single quiet entry point (no nags, no buttons beside action controls)
-
/supportview: renderskofi_support_message(or default copy) plus the Ko-fi widget; renders only whenkofi_enabled - Add
.github/FUNDING.ymlhere and across the other ham repos (hamalert-notifier,skywarn-activation-alerts,pktnet,radiomail.info, etc.) for the native GitHub Sponsor button
Phase 2 — Supporter recognition ("sparkle") (not started)
- Migration: add
users.is_supporter(bool) andusers.supporter_expires_at(nullable, tz-aware UTC) -
routers/donations.py:POST /api/webhooks/donations— verify the configuredkofi_webhook_token, match payloademailtoUser.email, set the flag (one-off tip → expires in 30 days; subscription → persists until a cancel/expiry event) - Daily expiry sweep that clears lapsed one-off sparkles (follows the existing
*_service.pybackground pattern) - Surface
is_supporterin the WebSocket user payload (online_users) and relevant user serializers -
UserAvatar.tsx: conditional supporter style — a subtle gold ring with a soft glow and a periodic shine sweep, kept lightweight for the real-time UI. One component change propagates to Chat, NetView, Navbar, Profile, and NCSStaffModal - Decide edge cases: donor email ≠ account email (offer a "link my donation" note on
/support); refunds/chargebacks strip the sparkle; anonymous tips count toward the goal but earn no sparkle
Phase 3 — Transparency (not started)
- Monthly progress bar driven by
kofi_hosting_goal_amount(use Ko-fi's native goal widget first; build custom only if styling demands it) - Honest cost framing in the default
/supportcopy: real monthly hosting figure, "always free" reassurance, and the "independent dev lab funds a suite of ham tools" ecosystem framing (the HamStudy / SignalStuff model) - Open Collective deferred — revisit only if donation volume ever justifies a public ledger
Phase 4 — Recognition in the About modal (not started)
Top financial supporters and community contributors are acknowledged directly in the About modal — visible to every user, no login required. Two categories:
- Top Supporters — operators who have contributed financially via Ko-fi, ordered by lifetime contribution amount. Shown as a short list (e.g., top 5) with callsign and a subtle supporter badge. A "View all supporters" link or secondary dialog lists the full roster.
- Honorable Mentions — operators who have contributed invaluable feedback in the form of bug reports and feature requests. This group already exists in the "Feedback Attribution" table at the bottom of this roadmap and should be seeded from that list when the feature ships.
Data model notes:
- Top Supporters: derived from the
is_supporter/ donation webhook data added in Phase 2. Lifetime totals require accumulating amounts on the webhook handler (newdonor_lifetime_totalcolumn, or a separatedonationslog table). Display requires explicit opt-in from the donor (ashow_in_creditsflag) to avoid surfacing anonymous tips. - Honorable Mentions: a lightweight
creditstable (or admin-managed JSON inAppSettings) with fields:callsign,name,note(optional),category(supporter | contributor). Admins add/remove entries from the Admin panel.
Implementation checklist (not started)
- Decide on data source for lifetime totals (accumulate on webhook vs. separate donations log)
- Add
show_in_creditsopt-in flag to user/donor flow on/supportpage - Admin panel: "Credits" section to manage the Honorable Mentions list
-
GET /api/creditspublic endpoint returning top supporters + honorable mentions - About modal: "Top Supporters" section (short list) + "Honorable Mentions" section, both collapsed behind a "View contributors" expandable or secondary dialog
- Seed Honorable Mentions from the Feedback Attribution table in this roadmap at launch
Trigger: implement after Phase 2 (donor tracking) is in place. Honorable Mentions can ship independently of Phase 2 since they are admin-curated.
Trigger: start after the Help Menu / About modal ships (done). Phase 1 alone satisfies the original goal (a subtle support link); Phases 2–4 are additive and carry no risk to core net logging.
✨ Net trivia support (back-burner, pending spec)
Model: Sonnet once a spec exists; the spec itself is a human/Opus conversation.
Load trivia questions from a CSV file or URL. During a net, NCS can click a trivia icon on a check-in row to pose a question to that station and log correct/incorrect. Include trivia results in the net log, PDF report, and email summaries. Needs detailed spec before development begins.
Items that require significant new infrastructure, platform expansion, or external integrations.
✨ Migrate from SQLite to PostgreSQL ahead of expected growth (KC1JMH)
Model: Opus — data migration with zero-loss requirements, plus an audit-found landmine: several columns store JSON as Text (e.g. User.callsigns) and enums via SQLAlchemy Enum — both need an explicit porting decision for Postgres. The schema-tooling question (Alembic or not) is its own prerequisite section below.
ECTLogger runs SQLite today, which is appropriate for a low-concurrency single-server deployment. SQLite serializes all writes; under concurrent net sessions and real-time check-ins from multiple NCS operators at once, this will become a bottleneck. The ORM layer (SQLAlchemy async with aiosqlite) already supports PostgreSQL via asyncpg — the DATABASE_URL env var is the primary code-level change.
Migration plan:
- Resolve the schema-tooling decision (see the prerequisite section below)
- Provision a PostgreSQL instance on the IONOS VPS (or use a managed instance)
- Bring the new database to the current schema (
alembic upgrade headif Alembic is adopted, otherwise create frommodels.py) - Write a one-time data migration script to export SQLite rows and import into Postgres (preserve all timestamps and IDs)
- Flip
DATABASE_URL, restart, smoke-test - Keep the SQLite file as a backup for 30 days post-migration
Trigger: migrate before the user base exceeds ~300 accounts or before any feature requiring high concurrent write throughput (e.g., simultaneous multi-net operation). The expected inbound migration from ham.live's closure makes this a near-term planning item rather than a back-burner one.
🔧 Decide whether to adopt Alembic before the Postgres cutover (formerly roadmap item 0.5, "Migration hygiene policy")
Model: Opus for the decision, Sonnet for execution.
Migrations today are hand-numbered Python scripts in backend/migrations/, each run individually against each deployment. That works for SQLite schema tweaks but leaves no versioning record, so a fresh Postgres database has no defined "current schema" to build from. Decide and execute one of two paths before the cutover:
- Adopt Alembic — autogenerate an initial revision from
models.py, stamp the existing production/beta databases as current so they are not re-run from scratch, and convert the run-each-script workflow (including the docs inbackend/migrations/README.md,docs/DEVELOPMENT.md, and the deployment steps in.github/copilot-instructions.md). - Skip Alembic — bootstrap the Postgres schema directly from
models.pyand keep the numbered-script convention for incremental changes.
Whichever path is taken, the PostgreSQL migration plan above must be rewritten to match: it currently assumes Alembic.
Already settled (2026-07-07), keep enforcing: migrations carry schema changes only, never instance-specific data fixes — a self-hoster must never inherit another deployment's roster seeding. The standing rule lives in the "Migration content guidelines" section of backend/migrations/README.md, and it constrains whichever tooling path is chosen. The 013_ numbering collision that prompted the rule is resolved.
🔧 Standardize on timezone-aware UTC datetimes end-to-end (KC1JMH)
Model: Sonnet for the mechanical sweep, Opus review before merge (naive/aware bugs pass tests that don't cross a DST boundary). Audit 2026-07-03 verified scope: 34 utcnow references across 8 files — auth.py, main.py, whats_new_service.py, ncs_reminder_service.py, routers/chat.py, routers/check_ins.py, routers/nets.py, routers/ncs_rotation.py. Frontend 'Z'-append workarounds live in Admin.tsx (6 sites), CreateNet.tsx, and NetView.tsx.
Background. The app already stores concrete net instants (Net.scheduled_start_time, started_at, closed_at, etc.) in UTC and renders them per-user in local time. The fragility is how UTC is represented in the code: today it is naive UTC by convention. On SQLite, DateTime(timezone=True) silently drops the offset, so a value written as "UTC" comes back as a naive datetime. The backend leans on this (boundary helpers return naive UTC; comparisons use the deprecated datetime.utcnow()), and the frontend papers over it by appending 'Z' before parsing (CreateNet.tsx, NetView.tsx). The June 2026 reminder bug was one symptom of this naive/aware ambiguity.
Why this blocks PostgreSQL. SQLite ignores timezone info; PostgreSQL does not. A DateTime(timezone=True) column maps to timestamptz, which stores a true instant and returns tz-aware datetimes. Under Postgres:
- Writing a naive datetime to
timestamptzmakes the driver (asyncpg) assume the server/session timezone — which may not be UTC — silently corrupting the stored instant. - Reading returns tz-aware values, so any lingering
naive == awarecomparison (e.g. againstdatetime.utcnow()) raisesTypeError: can't compare offset-naive and offset-aware datetimes.
In other words, the SQLite→Postgres migration will break time handling app-wide unless this is resolved first. This item is therefore a prerequisite, not a nice-to-have.
Target design (works on both SQLite and PostgreSQL):
- Introduce a single
UTCDateTimeSQLAlchemyTypeDecoratorused by every datetime column:- On bind (write): require/assume UTC, store as a tz-aware value on Postgres (
timestamptz) and as a normalized naive-UTC value on SQLite. - On result (read): re-attach
tzinfo=timezone.utcto values coming back naive from SQLite, so the rest of the app always receives tz-aware UTC regardless of backend.
- On bind (write): require/assume UTC, store as a tz-aware value on Postgres (
- Replace every
datetime.utcnow()withdatetime.now(timezone.utc)(also resolves the Python 3.12 deprecation). Grep targets:routers/*.py,ncs_reminder_service.py,whats_new_service.py,auth.py. - Keep
template_local_to_utc()as the conversion boundary for recurrence-rule projections — but have it return tz-aware UTC once the decorator is in place. - Serialize datetimes via
.isoformat()(yields+00:00) and remove the frontend'Z'-append workarounds; standardize parsing in oneparseUtc()helper on the client.
Implementation checklist (not started)
- Add
UTCDateTimeTypeDecorator inmodels.py(or aapp/types.py) and switch all datetime columns to it - Replace all
datetime.utcnow()calls withdatetime.now(timezone.utc) - Audit every
naive vs awarecomparison; remove the defensive naive/aware branches (e.g.routers/nets.pylobby logic) - Update
template_local_to_utc()and reminder service to produce/consume tz-aware UTC - Remove
'Z'-append hacks; add a singleparseUtc()client helper and route all scheduled-time parsing through it - Verify on SQLite (existing) and a scratch PostgreSQL instance: round-trip a scheduled net, a reminder projection, and an import/ICS-309 export
- Sequence this before flipping
DATABASE_URLin the PostgreSQL migration
Trigger: complete alongside (and ahead of) the PostgreSQL migration. Low user-visible risk if done carefully; high risk if deferred until after the Postgres cutover.
✨ Teams — ARES/SKYWARN team roster, training tracking, and ARRL Form 2 support (KC1JMH — back-burner)
Model: Opus for the module design (new domain model, role system, reporting); Sonnet for phased build-out once designed.
Full spec and design notes: docs/concepts/TEAM-MANAGEMENT-NOTES.md
Summary: a new Teams section (menu between Schedule and Stats) to replace spreadsheet-based ARES/SKYWARN team tracking with a role-controlled, self-service platform. Members manage their own profiles; team managers handle roster, approvals, and reporting. Net participation automatically rolls up to team records. Designed to facilitate ARES Form 2 and EMA hour reporting.
Also carries the Teams-dependent half of "can hear" station-to-station coverage logging (shipped 2026-08-02, see CHANGELOG.md) — named team locations (shelters, EOCs), location-to-location coverage, and Coverage Assessment reporting for team managers. See TEAM-MANAGEMENT-NOTES.md section 5.6; the per-net capture it builds on has already shipped and is not blocked by this module.
Blocked on: core web app stability, self-hosting, and Docker packaging being in good shape first.
✨ Keep logging a net when connectivity drops, and sync on reconnect (KC1JMH)
Model: Opus for the sync and conflict design — this is a distributed-state problem, and the conflict rule below has a real data-loss failure mode. Sonnet for implementation once the design is settled.
An NCS running a net from a field site, an EOC on generator, or a rural home should be able to keep logging check-ins through a connectivity outage, with queued changes replayed when the link returns. Other participants should see that the NCS has gone offline and that net updates will resume once connectivity is restored, rather than silently watching a frozen net.
Offline operation in the browser requires a service worker to cache the app shell, which makes this a PWA. This resolves the PWA-vs-native question previously flagged under Native Desktop Client below: the offline requirement is the strongest driver for that work, and a PWA satisfies it without maintaining three native packages. Treat the PWA as the path forward and the native desktop client as likely redundant.
Groundwork already in place. Every check-in mutation now applies the server's authoritative single-row response to local state rather than re-reading the whole list (frontend/src/components/netview/checkInActions.ts), and a status change paints optimistically and rolls back on failure. "Refetch everything after a write" cannot work offline — there is nothing to refetch from — so that single-row apply is the reconcile primitive this feature builds on. Optimistic update is the same mechanism with the round trip deferred from milliseconds to minutes.
Reconnect resilience also shipped separately: the net socket now reconnects indefinitely, reconnects immediately when the browser reports online, and resyncs everything on reconnect via the netResync event — check-ins, roles, stats, can-hear, chat, activity log, and traffic. See DEVELOPMENT.md "Reconnect and resync". That covers recovery from an outage; it does not cover working through one.
The PWA is the last piece, not the first. A service worker is strictly required for only two things: cold-starting the app with no network, and surviving a reload mid-outage. Everything else people actually want during a net works in a plain page with the tab already open — IndexedDB needs no service worker, and neither do online/offline detection or the resync above. The realistic ARES/SKYWARN failure is not "the NCS opens a laptop with no internet"; it is "the NCS is mid-net and the link drops for ten minutes with the tab already open." Stage the work accordingly: durable queue and conflict handling first, service worker and installability last. Doing it in the other order spends the expensive effort on the rarer case.
Prerequisites, in rough dependency order:
- Client-generated IDs. The server assigns
check_in.idtoday. A check-in created offline has no id, so a queue cannot reference it and it cannot be edited before reconnect. A client-side UUID has to be carried through the model and honored by the create endpoint — this is a schema and API change, not a frontend detail. - A durable queue. Optimistic state is in-memory and does not survive a refresh, tab close, or crash — precisely the conditions a field deployment hits. Needs IndexedDB with an explicit replay order.
- A conflict rule. This is the sharp edge, not a detail.
create_check_in(backend/app/routers/check_ins.py) rejects with 400 "already checked in" when a callsign's latest row is notCHECKED_OUT. Two NCS operating offline on the same net will both log the same callsigns, and on reconnect the second operator's queued creates get rejected wholesale. This needs a merge policy decided up front; retry-with-backoff makes it worse, not better. - Deferred-rollback UX. A rollback 200 ms after a click is invisible. A rollback twenty minutes later, after the operator has moved on and the net has advanced, needs a reconciliation review screen showing what could not be applied and why. A toast is useless at that timescale.
The offline-presence signal. backend/app/net_pause.py already computes "no NCS present" and broadcasts net_pause_change, with a banner surface on the net view. Reuse that pattern rather than inventing a parallel one — but note the trigger differs: it keys off check-in status, not connectivity, so an NCS whose link drops still reads as present. The new signal should be driven by socket liveness, which ConnectionManager (backend/app/main.py) already observes when a WebSocket drops.
Relationship to the TUI/packet client below: that item also specifies offline command queuing and replay. The conflict rule and queue semantics should be designed once and shared, not solved twice with different answers.
✨ Standalone NCS client application (Windows / macOS / Linux) (KC1JMH — back-burner)
Model: Opus (framework selection and packaging architecture). Decision recorded: the PWA question flagged here is resolved in favor of the PWA — see Offline-Capable Web Client above. Offline operation is the strongest driver for a dedicated client, a PWA satisfies it, and maintaining three native packages alongside it is hard to justify. Treat this section as likely superseded; revisit only if a concrete requirement emerges that a PWA genuinely cannot meet.
A packaged desktop GUI application for NCS operators connecting to a hosted or self-hosted ECTLogger instance. Intended for single-operator NCS use; not a server. Targets scenarios where a browser is impractical but a full GUI is available. Proposed repo layout: clients/windows/, clients/macos/, clients/linux/ with installable packages per release. Technology decision pending — evaluate Electron, Tauri, or native framework.
✨ Terminal-first NCS client for low-bandwidth and degraded-link operations (KC1JMH — back-burner)
Model: Opus (protocol design for the packet command mode is the hard part; the TUI itself is Sonnet work afterward).
Full spec and design notes: docs/concepts/TUI-PACKET-CLIENT.md
Summary: a terminal UI (TUI) client and packet-optimized command protocol for running nets over SSH, local console, or packet radio links (~1200 baud). Two command modes — full terminal and abbreviated packet — with offline command queuing and replay on reconnect. Future phase includes a Winlink gateway for form-based check-in submission. Distinct from the desktop GUI client above: this is the degraded-connectivity and emergency deployment path.
This is separate from the standalone desktop client above. Both are back-burner until the web app and self-hosting are stable.
Its offline command queuing and replay overlaps directly with the Offline-Capable Web Client above. Design the queue semantics and the conflict rule once and share them across both clients — solving the same problem twice invites two different answers to "what happens when two operators logged the same callsign offline."
✨ Docker image for self-hosters
Model: Sonnet. Note: a fresh Docker install must never execute another deployment's roster fixes — the schema-changes-only rule in backend/migrations/README.md ("Migration content guidelines") is what keeps that true, so verify the image's migration step honors it.
Official Dockerfile / docker-compose.yml for a one-command self-hosted deployment. Publish to Docker Hub alongside each release.
✨ Net template portability between hosted and self-hosted
Model: Opus design (export format, identity/attribution model), then Sonnet.
Allow net templates created on app.ectlogger.us to be copied to a self-hosted instance (and vice versa), preserving origin metadata for attribution. Opt-in sharing of logs and net stats between instances.
✨ Cross-instance user stats sync
Model: Opus — federated identity/token exchange is an architecture decision with security consequences.
Users who participate in nets on both hosted and self-hosted instances can opt in to aggregating their check-in stats across both. Requires a federated identity or token-exchange design.
✨ Resilience against hosted server unavailability
Model: Sonnet (mostly an audit task: verify no hard-coded dependencies on the hosted instance, then fix what's found).
Self-hosted instances should degrade gracefully if app.ectlogger.us is unreachable or permanently offline. No hard dependency on the hosted server for core net logging functionality.
Items that were raised but need clarification, reproduction steps, or a design decision before they can be scheduled.
| Item | Source | Blocker |
|---|---|---|
| ham.live closure — onboarding displaced users | KC1JMH | ham.live is shuttering. No action needed, but inbound user migration is expected. Infrastructure scaling items (DB indexes, PostgreSQL migration path) have been added to the roadmap in anticipation. Worth monitoring signup rate in coming weeks. |
| Item | Rationale |
|---|---|
| Disabling web self-check-in globally | Net managers can already configure this per-net if needed; a global kill switch is not warranted. |
| Mobile station sort removal | Confirmed intentional and appreciated; making it optional (Milestone 1) is sufficient. |
Well-specified but deprioritized — not blocked on information (see Parking Lot for those), just not worth building right now.
✨ Auto-check-in for subscribed stations on certain nets (field request, 2026-07-30)
Let a station be automatically checked in on nets they're subscribed to (surfaced via the existing subscription icon), for operators who reliably check into the same recurring net every week.
Design decision (2026-07-30, KC1JMH): if revisited, scope is belt-and-suspenders — the NCS/schedule owner must enable auto-check-in on the schedule and the individual station must opt in on their subscription. Neither side alone is sufficient.
Why backburnered: this request traces back to stations not realizing they needed to manually check in after opening the net view. That underlying discoverability problem has since been addressed directly — the command-bar rewrite, the flashing Check In button, and the check-in prompt dialog all now surface the action itself, which was the actual gap. Revisit only if reports of missed check-ins continue despite those fixes.
| Handle | Net role |
|---|---|
| AA1GM — Joel Huntress | Net manager, Maine Dirigo DMR Net |
| KC1UIX — David Lounsbury | YCECT multi-repeater SKYWARN |
| W1BKW — Brian Wall | Regular participant, ham.live nets |
| W1MTW — Mark Carlson | Net participant (mobile user) |
| N1GSK | Net participant (mobile user), Maine Dirigo Net |
| KC1JMH — Brad Brown | Developer / net manager / WSSM Club Secretary / Cumberland County ARES EC |