Add APRS bulletins#345
Open
Russell-KV4S wants to merge 1193 commits into
Open
Conversation
…y Pi Adds goarch:arm goarm:6 to goreleaser, an arm-unknown-linux-gnueabihf cross-rs target for the Rust modem, and a matching rust-build matrix entry. One ARMv6 build covers Pi 1, Pi 2, Pi Zero and Zero W -- Pi 2's Cortex-A7 is ARMv7 but runs ARMv6 binaries fine. No 32-bit ARM Docker image; ships as .deb (armhf) and tarball (linux_armv6l) only.
…gurePtt; drop gpio_pin android carrier
…-build build(release): add 32-bit ARM (armhf) Linux build for older Raspberry Pi
SendPacket enqueues onto the consumer goroutine, which persists and then submits the auto-ACK sequentially. The existing waitFor on store.List won the race in most runs, but the auto-ACK submit could still be pending when sink.list() was read, producing flaky auto-ACK count = 0 failures under -race in CI. Mirror the wait pattern used in the broadcast test (line ~575).
…-method-field feat(android): carry PTT transport in dedicated ptt_method field
Two related Android-focused design specs: - 2026-05-19-android-bluetooth-kiss-tnc-design.md: BT-paired KISS TNCs (Mobilinkd, Kenwood TH-D74/D75, etc.) via a Kotlin BluetoothSocket relay over the existing platform UDS, feeding the unchanged pkg/kiss serial path through an injected OpenFunc. Bonded-only picker; no in-app pairing. Desktop unchanged (existing rfcomm bind workflow on Linux, /dev/cu.* on macOS). - 2026-05-19-android-ptt-tab-design.md: undoes PR chrissnell#157's per-platform PTT divergence. One Ptt.svelte for both Android and desktop; per-channel PttConfig schema preserved (no migration); add/edit modal split into Change Method and Change Device dialogs; Android PTT block removed from ChannelEditModal.svelte.
…droid-bt-tnc-and-ptt-tab docs(specs): Android Bluetooth KISS TNC + unified PTT page
… drop dead writeJob handleSerialClose used to cancelAndJoin the read pump before closing the socket. BluetoothSocket.inputStream.read() is a blocking native JNI call that coroutine cancellation cannot interrupt -- only socket.close() unblocks it via IOException. Swap the order to match closeQuietly: close the socket first, then await the read job. Otherwise the launch hangs and the socket never closes. Test closeHandle_sendsClose_and_stopsPumps only verified that closing a non-existent handle is a no-op (the comment in the test body admitted it). Rename to closeNonexistentHandle_isNoOp so a future reader is not misled into thinking live open/close lifecycle is covered. HandleState.writeJob was declared, always constructed as null, and never read anywhere. Drop the field and the forward-looking comment.
…vice PlatformServer gains a nullable BtSerialAdapter field plus attachBtAdapter() to wire it in after construction, and a typed broadcastBt() that mirrors the existing broadcastGpsFix / broadcastGnssStatus style. serveClient now dispatches SERIAL_OPEN, SERIAL_DATA, SERIAL_CLOSE, and BONDED_BT_DEVICES_REQUEST directly to the adapter as fire-and-forget notifications (replies travel back asynchronously via broadcastBt). A missing adapter is treated as a no-op + DEBUG log so PlatformServerTest keeps working without one. GraywolfService constructs SystemBluetoothFacade + BtSerialAdapter after PlatformServer.start() and attaches it. onDestroy calls btSerialAdapter.shutdown() BEFORE platformServer.stop() so any final SerialClose frames make it through the still-open UDS.
…ded-list refresh Service-scoped BroadcastReceiver that watches BluetoothDevice.EXTRA_BOND_STATE transitions. On BOND_NONE it tells BtSerialAdapter.onBondLost(mac) to tear down any open RFCOMM handles for that device and push a refreshed bonded list. On BOND_BONDED it just calls handleBondedRequest() so the Go side sees a freshly paired device without the operator having to refresh. Registration mirrors stopReceiver: RECEIVER_NOT_EXPORTED on API 33+, unflagged registerReceiver below. Unregister in onDestroy wraps IllegalArgumentException for idempotency. Receiving the broadcast does not require BLUETOOTH_CONNECT (that permission gates direct API reads, not intent reception), so this works on devices where the operator has not yet granted the permission -- the bonded list will just be empty until they do.
Adds a one-shot Bluetooth bonded-device query to the Go platformsvc client. Wraps BondedBtDevicesRequest in roundTrip and registers BondedBtDevicesResponse on the dispatch allow-list so the strict-ordered respCh delivers the reply. BondedBtDevice is the typed Go view of BondedBtDevicesResponse.Device (MAC + Name). Order matches the platform's view at the moment of the request; live bond changes flow through the bond-state broadcast added in phase 2B, not this RPC. Test: hermetic fake-server pair verifies a two-device response decodes into the expected BondedBtDevice slice.
…teCloser Adds an RFCOMM-SPP serial open path that returns a standard io.ReadWriteCloser. Each open handle multiplexes onto the existing UDS connection: a uint32 handle ID (atomic-allocated) keys per-stream inbound channels, and the dispatch loop fans SerialOpenAck / SerialData / SerialClose / SerialError frames into the matching channel. Reasons not to reuse roundTrip: - BtSerialOpen waits for an ack but should not hold requestMu while the platform service is performing an RFCOMM connect (seconds, not milliseconds), which would block all other clients of the client. - SerialData read/write traffic is fully asynchronous; the strict- ordered request/response model does not apply. Per-handle delivery is non-blocking (256-frame buffer; dropped on overflow). Write chunks the caller's bytes into 4 KiB SerialData frames; Close is idempotent via sync.Once and emits a final SerialClose to the server. SerialErrorErr surfaces out-of-band stream failures (bond_lost, rfcomm_closed, etc.) distinct from io.EOF so callers can decide whether to retry or surface to the operator. writeMu serializes writeFrame calls so the new async writers (BtSerial Write, Close, plus the cancel-emit in BtSerialOpen) don't interleave their 4-byte length prefix with another writer's payload. roundTrip now also takes writeMu for its single writeFrame call. Tests (hermetic net.Pipe-driven fake server): - TestBtSerialOpen_roundTrip: open + write + server-echo + read - TestBtSerialOpen_serverClose_returnsEOF: server SerialClose -> io.EOF - TestBtSerialOpen_serialError_returnsTypedError: SerialError surfaces as *SerialErrorErr with code and detail intact
…erialOpen cleanups Without this, when the UDS dies or the client shuts down, every open btReadWriteCloser.Read blocks forever on its per-handle channel because handleDisconnect and Close only touched respCh and conn. Add drainBtHandles() that snapshots and clears the btHandles map under btHandlesMu, then closes each channel outside the lock (avoiding any re-entry into dispatch paths that also take the mutex). Call it from both handleDisconnect (after respCh close) and Close (after closeCh close, before conn.Close). Safe when btHandles is nil (initial state). Two new tests cover the fix: - TestBtSerialOpen_clientClose_unblocksRead asserts Read returns io.EOF within 500 ms after the underlying client.Close(). - TestBtSerialOpen_serverDisconnect_unblocksRead asserts the same for the handleDisconnect path (server closes the pipe). Minor cleanups in btserial.go: - chunked Write uses the Go 1.21+ min() builtin instead of the manual bounds check. - Read overflow path drops the unnecessary defensive copy into a fresh []byte; append() already copies data[n:] into its own backing array. Minor cleanup in btserial_test.go: the btTestServer.stopCh field was created and closed but never read -- the read loop exits because s.conn.Close() causes readFrame to error. Delete the dead field and its close() call.
…through platformsvc
- Expose kiss.OpenFunc as a type alias of SerialConfig.OpenFunc so
out-of-package factories can return it without duplicating the
signature.
- Add build-tagged factories in pkg/app:
- kiss_openfunc_default.go: non-Android returns nil so the
SerialSupervisor falls back to defaultSerialOpen.
- kiss_openfunc_android.go: routes MAC-style device strings
(colon or hyphen MAC-48) to platformsvc.BtSerialOpen and
rejects raw device paths with errNotSupportedOnAndroid.
- Add App.kissSerialOpenFunc() accessor on both tags so wiring.go
can call it tag-free.
- Unit tests cover MAC routing, non-MAC rejection, nil-client
no-op, and the non-Android stub.
…ith OpenFunc The KISS dispatch loop previously skipped the bluetooth interface type. Add an explicit KissTypeBluetooth case that hands the MAC (stored in ki.Device) to Manager.StartSerial with BaudRate=0 and Mode=ModeTnc forced regardless of DTO input -- RFCOMM has no baud and Bluetooth SPP TNCs always own the modem. Wire OpenFunc on both the serial and bluetooth cases via the new App.kissSerialOpenFunc() accessor: - Desktop: returns nil so the supervisor falls back to its default go.bug.st/serial opener. - Android: returns an OpenFunc that routes MAC-style device strings through platformsvc.BtSerialOpen and rejects raw device paths. Same SerialSupervisor implementation serves both transports.
Add TestSerialSupervisor_BluetoothShapedConfig_RxRoundTrip which drives the supervisor with the exact shape pkg/app/wiring.go uses for KissTypeBluetooth: MAC-style device string, BaudRate=0, and Mode=ModeTnc. The injected OpenFunc returns a net.Pipe end the way platformsvc.BtSerialOpen returns an RFCOMM-multiplexed io.ReadWriteCloser on Android; the supervisor doesn't care which is which. Asserts that: - OpenFunc receives the MAC and a zero baud (RFCOMM has no baud). - A KISS frame written to the pipe round-trips through ServeTransport. - OnFrameIngress fires with ModeTnc (BT TNCs own the modem). This is the integration proof that the new dispatch case in wiring.go wires the BT path through unchanged supervisor code.
…no baud)
The original predicate rejected BaudRate==0 for both KissTypeSerial and
KissTypeBluetooth, but wiring.go hardcodes BaudRate=0 on the bluetooth
path because RFCOMM has no baud rate. Net effect was that valid POST/PUT
requests for bluetooth KISS interfaces were rejected at the API boundary.
Split the predicate: require BaudRate>0 only for real serial devices.
Bluetooth requests with BaudRate==0 are now accepted. Updated the
existing baud-rate test to assert the new bluetooth-accepted case and
the corrected error message ("serial interfaces" instead of
"serial/bluetooth interfaces").
Adds the bonded-Bluetooth-devices REST endpoint that the Svelte UI's
Bluetooth KISS form (Phase 5) reads to populate the bonded-device
dropdown. On Android the handler enumerates bonds through the
platformsvc client (via a build-tagged App.btSourceForWebapi adapter);
on desktop builds the source is nil and the handler returns 501.
Wire shape: GET returns {"devices": [{"mac": "...", "name": "..."}]}.
Empty bond sets serialize as [], never null, so the UI's iteration code
doesn't need to special-case null.
…nt on btsource adapter
The 501 (non-Android) and 500 (source error) responses from
GET /api/kiss/bonded-bt-devices used http.Error, producing
text/plain bodies that contradicted the Swagger doc-comment
(which declared webtypes.ErrorResponse for both). The 500 path
also echoed the raw underlying error string to the client,
risking leakage of driver/stack details the way the rest of the
webapi explicitly avoids (see pkg/webapi/errors.go:48-51).
Switch both paths to the in-tree convention:
- 501 -> writeJSON(w, http.StatusNotImplemented, webtypes.ErrorResponse{...})
(matches the precedent at pkg/webapi/ptt.go:243-244)
- 500 -> s.internalError(w, r, op, err)
(logs the real error server-side, returns sanitized JSON)
Update TestGetBondedBtDevices_SourceError_Returns500 to drop the
old assertion that the response body contained the raw 'boom'
text (internalError deliberately strips it) and add positive
assertions: application/json content-type, JSON-shaped
webtypes.ErrorResponse body with a non-empty Error field, and a
regression guard that the raw underlying error is NOT echoed.
Extend TestGetBondedBtDevices_NonAndroid_Returns501 with the same
JSON-shape checks so the 501 body contract is also pinned.
Also reword the btsource_android.go header comment, which
claimed the per-call platformClient read matched 'the pattern
used by the other webapi setters wired in pkg/app/wiring.go'.
That's not true -- kissSerialOpenFunc in
kiss_openfunc_android.go captures the client once at
construction. The honest justification is that per-call keeps
the door open for a future late SetPlatformClient or
reconnect-swap without surprising the UI's polling loop, at
one extra pointer load per request.
…ell#247) Enrich the /api/packets DTO with channel_name, resolved from the numeric channel ID against the configured channel inventory, and render it in the PacketLogViewer (relabel the column Ch -> Channel). Falls back to the raw ID when the channel maps to no configured channel (e.g. channel 0, used for non-RF / APRS-IS arrivals). Regenerates the OpenAPI spec and the frontend API types. Co-authored-by: graywolf-agent <agent@multica.local> Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: multica-agent <github@multica.ai> (cherry picked from commit 6a9b6e3)
Issue chrissnell#231: graywolf-modem SIGSEGV crash-loops on 32-bit Raspberry Pi OS (Pi Zero/armv6) because libasound2t64 writes a 16-byte struct timespec into alsa-rs's 8-byte get_htstamp buffer. The shipped fix (RUST_LIBC_UNSTABLE_GNU_TIME_BITS=64) cannot link against our Bionic (glibc 2.27) cross toolchain (undefined __ioctl_time64) and cascaded into four crate forks. Replace it with a single alsa-rs patch that reads the htstamp accessors into a 16-byte buffer, so the C call cannot overflow without building for time64. The binary stays time32: it links on the current toolchain and runs on both pre-t64 and t64 systems. - Cargo.toml: [patch.crates-io] alsa -> fork with the 16-byte-buffer fix. - release.yml / Cross.toml: remove RUST_LIBC_UNSTABLE_GNU_TIME_BITS. - nix/cpal/gpiocdev stay stock (no forks); no time64 means they compile fine. Design: docs/plans/2026-06-11-armhf-t64-alsa-htstamp-fix.md Co-authored-by: multica-agent <github@multica.ai>
…#231) Runs a C canary in a 32-bit ARM t64 userland under QEMU and asserts that an 8-byte get_htstamp buffer overflows while a 16-byte one does not -- the exact premise the alsa-rs htstamp buffer fix relies on. Fails closed if the base image is ever not a t64 32-bit userland. Co-authored-by: multica-agent <github@multica.ai>
* Warn at startup when the system clock is not synced An undisciplined clock skews packet ages and the map's Time Range filter, which can silently hide stations (graywolf#234). Add a pkg/clocksync adjtimex(2) check on Linux (Unknown/no-warn elsewhere) and emit a one-time WARN from the startup banner. Co-authored-by: multica-agent <github@multica.ai> * clocksync: key sync state on STA_UNSYNC bit alone Drop the redundant adjtimex return-state (TIME_ERROR) disjunct, which could over-fire on leap-second/clock-error states. The kernel sets STA_UNSYNC at boot and clears it only once a time source disciplines the clock, so the bit alone covers the no-daemon and not-yet-converged cases while leaving a synced clock with a pending leap second reported as synced. Factor the decision into a pure classify() helper and table-test the boundaries. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: graywolf-agent <agent@graywolf.local> Co-authored-by: multica-agent <github@multica.ai>
…ell#253) Resolve each packet's channel id to its operator-given name via the shared channels store and emit it as a Channel column in packets.csv. Falls back to the raw id when the channel was deleted. CSV cells are now RFC 4180-quoted so channel names with commas or quotes can't corrupt rows. Closes chrissnell#233 Co-authored-by: graywolf-agent <agent@graywolf.local> Co-authored-by: multica-agent <github@multica.ai>
…hrissnell#252) * Scale dashboard uptime to days/weeks/months above thresholds The Uptime stat rendered only as hours and minutes, which became hard to read for long-running nodes. Scale the display to the largest meaningful unit (days, weeks, months) once each threshold is met, showing at most two units so the value stays on one line in the stat card. Sub-day uptimes keep the existing hours/minutes format. Closes chrissnell#248 Co-authored-by: multica-agent <github@multica.ai> * dashboard: keep stat values on one line (nowrap) Code review on chrissnell#248 flagged that long uptime strings (and existing 7-8 digit packet counts) could wrap at the minimum stat-card width since .stat-value had no white-space rule. Add nowrap so the value always stays on a single line. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Graywolf Agent <agent@multica.local> Co-authored-by: multica-agent <github@multica.ai>
…r-path clear() on map teardown Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- esc(): add double-quote escaping to close HTML attribute injection (affects existing data-callsign attrs too, so the fix is overdue) - Message link: skip for APRS objects (is_object), which cannot receive APRS messages; use encodeURIComponent for the thread ID value (consistent with openDm() in Messages.svelte) and toUpperCase() to match the dm: thread key convention - CSS: collapse stn-msg-link + stn-ext-link into shared stn-link class - vite.config.js: correct dev proxy target to 127.0.0.1:8080 (matches the backend default in pkg/app/config.go; was pointing at 8081) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…elpers Documents the child-before-parent Svelte onDestroy ordering and MapLibre v5's map.remove() -> delete this.style behavior. Any layer helper with a clear() or similar method called outside onDestroy must guard getSource() with try/catch, same as destroy(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a full APRS bulletin-board subsystem: outbound compose/transmit
with per-slot retransmit scheduling per APRS101, inbound ingestion and
upsert via the message router, REST API, and a Bulletins UI page.
- pkg/bulletins: Store, Sender, Scheduler, Service with full test suite
(store_test, service_test, scheduler_test). Scheduler fires every
20 min for BLN0-9 (max 12 sends) and every 1 h for BLNA-Z (max 96).
- Outbound packets use the operator's MessagePreferences.DefaultPath
(WIDE1-1,WIDE2-1 default) so bulletins are digipeated like beacons.
When an iGate is wired the bulletin is also sent directly to APRS-IS
via TNC2/TCPIP* so it appears on aprs.fi; ErrNotEnabled is silently
swallowed for RF-only operators.
- Inbound bulletins are routed by the message router to a BulletinSink
interface; the router does not persist them as directed messages.
- SQLite partial-index upsert workaround: ON CONFLICT cannot target
partial indexes, so UpsertInbound uses select-then-update/insert.
- REST: GET/POST /api/bulletins, DELETE /api/bulletins/{id},
POST /api/bulletins/{id}/read, POST /api/bulletins/read-all.
All handlers return 503 until the service is wired (safe startup).
- Frontend: Bulletins.svelte page with compose panel, slot selector
(optgroup for bulletins vs announcements), 67-char counter, Received/
Sent tabs with unread badge, mark-read, delete, 30-s auto-refresh.
Sidebar icon uses inline SVG (chonky-ui allowlist does not include rss).
- Windows exe icon: goversioninfo embeds graywolf.ico into the binary
via resource_windows.syso; bump-point and bump-minor now update
versioninfo.json and regenerate the .syso on each release.
- Wiki: new bulletins.md; README and code-map updated.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bulletin scheduler: per APRS protocol the first sends after a bulletin is created should fire rapidly to survive packet collisions, then settle into the standard Net Cycle Time rate. Now sends 3 times at 30-second intervals on creation, then switches to the 20-minute stable rate. Poll interval reduced from 60s to 15s so the 30s burst windows are actually caught. Added TestScheduler_BurstThenStableRate to verify the transition from burst to stable interval. Messaging settings: expose MessagePreferences.DefaultPath in the Messaging settings page as a "Digipeater path" text field. This field already existed in the DB and was used by both messages and bulletins but had no UI. Operators can now set WIDE1-1, WIDE1-1,WIDE2-1, or empty (direct) from the settings page. Added getter/setter to messagesPreferencesState store. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Slot taxonomy table now shows initial burst column (3x30s) vs stable interval (20 min) separately. - Scheduler section corrected: poll is 15s not 20 min; burst-then-stable interval logic documented with the actual constants. - Frontend section: note that rss icon is replaced by inline SVG; add note that digipeater path is now configurable in Messaging settings. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Operators can now set the stable bulletin retransmit interval from the Messaging settings page. 0 = burst-only (3 sends at 30s, then stop); 1-20 = minutes between retransmits after the burst phase. Default is 20 min per the APRS Net Cycle Time spec for 2-hop stations. - migration 27 adds bulletin_interval_mins to messages_preferences, backfills existing rows to 20 - Scheduler.intervalMins is an atomic uint32 updated at runtime via SetIntervalMins; burst-only clears NextSendAt so rows go dormant - ServiceConfig.BulletinIntervalMins wired through app/wiring.go - DTO Validate enforces 0-20 range; FromModel and buildPayload both include the field - Frontend: Bulletins box in MessagesSettings.svelte with a 0-20 number input; store getter/setter follow existing pattern - Tests: BurstOnly_StopsAfterBurst and CustomInterval added Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The retransmit interval is now set per-bulletin at compose time rather than as a global Messages preference. Each bulletin carries its own interval_mins (0=burst-only, 1-20=stable rate) so BLN0 can retransmit every 20 min while BLN1 goes every 10 min, for example. - migration 28 adds interval_mins to the bulletins table (SQL DEFAULT 20, backfills any pre-existing rows); the GORM struct tag intentionally omits the default so zero (burst-only) is stored as-is - Scheduler reads b.IntervalMins per row; no more global intervalMins atomic on Scheduler, no SetIntervalMins/SetBulletinIntervalMins methods - SendRequest carries IntervalMins; service.Send() stores it on the row; webapi handler passes it through from the DTO - BulletinResponse now includes interval_mins for the UI to display - Bulletins.svelte compose form: "Every N min" input (0-20, default 20), hidden for announcements which are always 1 hr; sendStatus() shows the per-bulletin rate - Remove BulletinIntervalMins from MessagePreferences, messages DTO, store, and MessagesSettings.svelte Bulletins box Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- Burst-only bulletins (interval_mins=0) now set max_sends=BulletinBurstCount
so the row exhausts naturally after 3 sends and the UI shows "Complete"
instead of "3/12 sent * burst only" indefinitely
- Add TestSend_BurstOnly_MaxSends and TestSend_IntervalMins_Stored
- Add TestSendBulletin_IntervalMins_PassedThrough and _OutOfRange
- Fix TestSend_Valid to use IntervalMins:20 (zero value now means burst-only)
Frontend:
- Remove unused ALL_SLOTS constant from Bulletins.svelte
- Fix indentation on {#if !isAnnouncement} interval block
- MessagesSettings: strengthen digipeater path hint to explain it is a
station-level setting covering all outbound APRS including bulletins
Wiki:
- Fix migrations heading: 26-27 -> 26-28
- Add interval_mins to DB column table with correct max_sends note
- Update POST /api/bulletins body to include interval_mins
- Update slot taxonomy: note stable interval is configurable (default 20 min)
- Explain global path rationale in the frontend section
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Russell-KV4S
force-pushed
the
feature/aprs-bulletin-board
branch
from
June 25, 2026 14:10
e1e9e4b to
a7ffad8
Compare
- code-map: add messages RetryIntervalSecs/RetryMaxAttempts to messages entry (migration 26 adds retry_interval_secs; 0 stored = use default 30s/4 attempts; settable in MessagesSettings Retry box) - code-map: fix bulletins table migration number 26 -> 27 - code-map: note migration 28 (bulletin_interval_mins on messages_preferences) is a dead column superseded by migration 29's per-row interval_mins - code-map: add migration 26-29 summary to configstore entry - bulletins.md: replace step-by-step scheduler internals with topology summary - bulletins.md: replace full column table with migration note (component- internal detail belongs in the code per CLAUDE.md) - bulletins.md: replace REST endpoint table with pointer (REST reference belongs in the handbook per CLAUDE.md) - bulletins.md: fix section header "migrations 26-28" -> "migrations 27, 29" - bulletins.md: add dead-column warning for migration 28 - README.md: update bulletins.md description to match current content Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- scheduler: announcement interval (BLNA uses 1h, not burst/stable) - scheduler: soft-deleted row is not re-sent after kick - webapi/bulletins: verify direction+unread_only query params are propagated to the service Filter (existing test ignored the filter) - messages/router: bulletin sink error drops packet gracefully and router continues processing subsequent packets without panic Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Upstream added invariants 53 (map-viewport any-trail-position), 54 (stationcache trail order/dedup), and 55 (SmartBeaconing tracker gate). Our fork had a conflicting invariant 53 (MapLibre map.remove teardown guard) -- renumbered to 56. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n, .syso) build-pipelines.md: add a row for resource_windows.syso explaining that goversioninfo generates it from versioninfo.json + graywolf.ico, that the .syso is committed so plain go build works anywhere, and how to regenerate it manually. code-map.md: add a "Go binary entry point (cmd/graywolf/)" section covering main.go, versioninfo.json, graywolf.ico, resource_windows.syso, authcli/, parentwatch.go, android_config.go, and flare.go. Consolidates the flare entry point row (previously floating in the flare CLI section) into the entry-point table. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…in-board # Conflicts: # docs/wiki/invariants.md # pkg/configstore/migrate.go # pkg/configstore/models.go # pkg/messages/service.go # pkg/webapi/server.go # web/src/routes/MessagesSettings.svelte
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
What changed
Backend
pkg/bulletins/-- new package: Store, Sender, Scheduler, Service with full test coveragepkg/configstore/models.go-- Bulletin model; four migrations (all post-AutoMigrate, raw SQL):retry_interval_secstomessages_preferencesbulletinstable with partial unique index on(from_call, slot)bulletin_interval_minstomessages_preferences-- orphaned; design moved to per-row column in migration 29 (noted in wiki; no Go code reads it)interval_minsper-row onbulletinstablepkg/messages/router.go-- BulletinSink interface and dispatch step; inbound BLN* packets route to the bulletin service rather than being dropped as not_for_uspkg/messages/retry.go-- RetryIntervalSecs and RetryMaxAttempts now read live from MessagePreferences; changes take effect without restartpkg/app/wiring.go-- wireBulletins, bulletinsComponent lifecyclepkg/webapi/bulletins.go-- REST endpoints: GET/POST /api/bulletins, DELETE /{id}, POST /{id}/read, POST /read-allFrontend
web/src/routes/Bulletins.svelte-- compose form (slot, text, interval), Received/Sent tabs, unread badge, per-row send status and deleteweb/src/routes/MessagesSettings.svelte-- Retry box (max retries + interval seconds); digipeater path field with station-level explanationweb/src/api/bulletins.js-- API clientDocs
docs/wiki/bulletins.md-- subsystem wiki page: wire format, slot taxonomy, ingest flow, outbound send topology, migration notes (including migration 28 dead-column warning), app wiring, frontend wiring. Internal scheduler logic and full schema table removed per wiki conventions (component-internal detail belongs in code).docs/wiki/code-map.md-- bulletins entry (corrected migration 26->27), messages retry settings added to messages entry, configstore entry notes migrations 26-29docs/wiki/README.md-- updated bulletins.md descriptionKnown issues (flagged in review, not blocking)
bulletin_interval_minswas added tomessages_preferencesbut no Go code reads it. Design was superseded by per-rowinterval_minson the bulletins table (migration 29). Flagged in wiki. Cleanup migration can follow separately.scheduler.go):SendCountis incremented in memory beforestore.Updatepersists it. A SQLite write failure leaves the row re-eligible on the next scheduler tick, potentially causing an extra RF send.SetTxChanneldormant data race: plainuint32field written without atomic; no call site exists post-Start today but inconsistent with themessages.Senderpattern (atomic.Uint32).parsePathduplication: copied verbatim frompkg/messages/sender.go; candidate for extraction to a shared package.Test plan
🤖 Generated with Claude Code