e978cf8a840a3ef45d8781ecfd5b925d7e128f02
96
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e978cf8a84 |
[feat](trx-frontend-http): separate transmit permission
CI / frontend (pull_request) Successful in 5m21s
CI / lint (pull_request) Successful in 2m24s
CI / test (pull_request) Successful in 9m11s
CI / test (push) Successful in 8m16s
CI / frontend (push) Successful in 4m24s
CI / reuse (push) Successful in 5s
CI / reuse (pull_request) Successful in 5s
CI / lint (push) Successful in 2m25s
Assisted-By: Codex (GPT-5) Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
44870bc941 |
[feat](trx-frontend-http): add restricted Guest role
Assisted-By: Codex (GPT-5) Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
5e9dae02c7 |
Complete managed account lifecycle
CI / test (push) Successful in 8m12s
CI / frontend (push) Successful in 4m15s
CI / reuse (push) Successful in 5s
CI / lint (pull_request) Successful in 2m24s
CI / test (pull_request) Successful in 9m6s
CI / frontend (pull_request) Successful in 5m17s
CI / reuse (pull_request) Successful in 5s
CI / lint (push) Successful in 2m26s
|
||
|
|
34507ffa17 |
Add composable HTTP access roles
CI / lint (pull_request) Successful in 2m24s
CI / test (pull_request) Successful in 9m24s
CI / frontend (pull_request) Successful in 5m12s
CI / reuse (pull_request) Successful in 6s
CI / lint (push) Successful in 2m24s
CI / test (push) Successful in 8m8s
CI / frontend (push) Successful in 4m15s
CI / reuse (push) Successful in 5s
|
||
|
|
36c1e56efa | Protect the final administrator | ||
|
|
e4cce9a004 | Add Users settings tab | ||
|
|
3c3fc69542 |
Refactor HTTP account system
CI / frontend (pull_request) Successful in 5m13s
CI / reuse (pull_request) Successful in 29s
CI / frontend (push) Successful in 4m15s
CI / reuse (push) Successful in 5s
CI / lint (pull_request) Successful in 2m25s
CI / test (pull_request) Successful in 9m24s
CI / lint (push) Successful in 2m23s
CI / test (push) Successful in 8m19s
|
||
|
|
ee185b98bc |
[feat](trx-logbook): work a contest, and record what came back
Phase 5 of the logbook: the exchange, the entry sponsors take, and the confirmations an award counts. Contest fields go on the contact — the contest, the serials both ways as numbers and as words, and the zones — because an exchange is not always a number: a zone, a section or a name goes in as written. The serial sent and the contest stay between contacts, since they belong to the session and not to the contact just logged, and the serial counts on by itself rather than being retyped forty times an hour. Cabrillo 3.0 is written because ADIF cannot do this job: sponsors take Cabrillo and reject everything else. Its shape is not ADIF's either — the frequency is kilohertz below 30 MHz and a band designator above it, the modes are CW, PH, FM, RY and DG, and the contacts go oldest first, as a contest log is read. The header cannot be derived from a log — how many operators, how much power, what the score is claimed to be — so it comes from the operator, with single-op, low power, all bands and mixed behind it. QSL, LoTW and eQSL states are held as ADIF's single letters, and anything else is refused rather than written: a log that grew states of its own would be one no other program could read. A contact is confirmed when any one of the three says so — an award wants a card or an electronic match, not one of each, and counting them separately would tell the operator they were short of what they have. The bands report counts contacts, distinct stations and confirmations per band, ordered by wavelength as a band plan reads. Closes #54 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
18b2d0efe6 |
[feat](trx-rs): keep a station log, and a layout to work the bands from
The logbook of issue #54, in the shape the proposal settled on. A new crate, trx-logbook, holds the contact, the ADIF reader and writer, the file, and the rules for telling one contact from two. ADIF because it is the only thing the ecosystem reads: LoTW, eQSL, Club Log, QRZ and every other logger take it and nothing else, so a log that cannot write .adi cannot be uploaded, confirmed or moved. The reader is forgiving in the ways real files are irregular -- lowercase tags, CRLF, missing header, unknown fields, a declared length that is the only thing ending a value -- and carries what it does not model through to the export, so a round trip does not strip what another program wrote. The file is JSON Lines, appended one line per contact. A log is the one thing here that cannot be regenerated, and the bookmark store's whole-file dump would rewrite megabytes to log one contact and lose all of them if the power went halfway; an append costs the record being written and no more, which a test tears a line in half to prove. Edits append revisions, deletes append tombstones, and the file compacts when the superseded outnumber the live. The panel is its own tab and stands in every layout. An entry opens with six fields and no more -- frequency, mode, rig name, time, and the callsign and locator of whatever decode it was started from. A report stays empty: an FT8 SNR is not what was sent. Times come from the server, because the browser may be a phone in another timezone, and the panel says so when the two disagree by more than a second. Worked-before answers as a callsign is typed. A decode is not a contact, so the Log button on an FT8 or APRS row opens an entry and logs nothing by itself. The ham layout is the fifth operator layout, opening on the logbook with the radio controls around it, offered only where the rig can transmit. Two bugs found on the way, both in code written here: a frequency of a whole number of megahertz ending in a zero rendered as a tenth of itself, in Rust and in TypeScript alike, because trimming trailing zeros from "20.000000" walks back through the point. The API also sits under /api/logbook rather than /logbook, so it cannot shadow its own page the way /bookmarks does. Closes #54 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
a19633e81f |
[fix](trx-frontend-http): rebuild the background decode panel around one list
The panel listed the same bookmarks twice: a status card at the top naming
the selected ones and what each was doing, and a checklist below naming all
of them with the checkboxes. Neither list said which row in the other it
meant, so choosing what to decode and reading what was being decoded were two
passes over the same names. The checklist itself sat squeezed against the
right-hand edge, because the row it lives in sets `align-items: flex-end` and
an inline `flex-direction: column` turned that into "push everything right".
It is one list now. A row carries its checkbox, its name, its frequency,
mode and decoder, and its own state, so picking and watching happen in the
same place. What the rig can hear moves up beside the switch, where it
explains why a selected bookmark reads out of span, and the selection adds
itself up under the list. The states lose the ✓/△/✗ they carried next to a
dot that already said as much, and say what they mean: "Out of span",
"Scheduler has it", "Nobody listening", each with the reason on hover. The
empty list now says which of the two reasons it is empty for, and what to do.
Save was live from the moment the panel opened, with nothing to save; it now
offers itself only when something has changed.
Two races behind it, both of which left the panel useless rather than ugly:
* The rig was whatever the panel was handed at load. Loading before the rig
list arrived handed it null, and the next telling only came when the
operator switched rigs — so the panel stayed empty and silent. Every rig
list refresh now passes the rig on, and both panels ignore one they have.
* The settings panels are wired once the session is up, but their modules
import asynchronously and the wiring skipped whichever had not arrived.
A panel that missed it kept dead buttons for the rest of the session: no
filter, no Select All, no Save. Wiring runs again when the modules land.
Closes #52
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7
Signed-off-by: Stan Grams <sjg@haxx.space>
|
||
|
|
e41c13917d |
[fix](trx-frontend-http): put HF APRS on the map, under its own source
The HF APRS list never plotted anything. Its plugin ships in the map plugin group and its packets carry positions, but nothing ever handed one to the map, so a station heard on 30 m appeared in the panel and nowhere else — and unlike the replay gaps around it, no reload brought it back. Plot it, and not as more VHF APRS. HF is a different band and a different path, and lumping the two together would leave no way to tell them apart or to look at one without the other, so it goes on as a source of its own: its own colour, its own chip in the map's Show filter, its own entry in the source legend, and its own clear. Station entries are keyed by source and callsign rather than callsign alone, so a station worked on both bands keeps a marker for each while the popups, the search text and the tracks still show the callsign as heard. decode-flow feeds an HF beacon alongside the VHF one and checks all of it: both reach the map under their own sources, the Show row offers HF APRS next to APRS, and turning that chip off takes the HF station off the map while the VHF one stays. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
ae7df31d91 |
[fix](trx-frontend-http): replay what arrived before a lazy view loaded
Opening the Map or Statistics page showed only what had been decoded since the moment it was opened, and a reload — landing straight on the tab, so its module loads at startup ahead of the history — was the only way to see the rest. Two things were being thrown away. The decode log the Statistics page counts lives in the map module, which is lazy. Recording into a module that is not loaded yet is a no-op, and unlike the map markers nothing replayed the log when it finally arrived, so every decode heard before the first visit was simply never counted. Hold those records in the client and hand them over when the module attaches. The map's own replay covered APRS, AIS and VDES, whose plugins implement syncMap, but not the grid squares: the FTx family and WSPR plotted locators as decodes arrived and had no replay at all, so everything they heard before the map loaded was lost, and the unique-grid counter with it. Both plot through a helper now, which their syncMap replays oldest first. A replayed WSPR spot carries the frequency it was heard on rather than one worked out against wherever the dial has moved to since. Pinned in decode-flow, whose history fixture gains FT8 and WSPR spots: after a first visit the statistics count every stored decode and every grid square, which before this change were 0 and 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
bf3bcc8a84 |
[fix](trx-frontend-http): show one rig at a time on the digital modes page
CI / lint (pull_request) Successful in 2m24s
CI / test (pull_request) Successful in 8m34s
CI / test (push) Successful in 7m47s
CI / frontend (pull_request) Successful in 4m28s
CI / reuse (pull_request) Successful in 5s
CI / lint (push) Successful in 2m21s
CI / frontend (push) Successful in 3m37s
CI / reuse (push) Successful in 6s
A client connected to several rigs decodes all of them at once, and the decode stream carries every rig's traffic to every browser. The decoder panels listed all of it: a station a background rig copied on another band appeared in the APRS list next to the selected rig's, the vessel counts and the "latest seen" lines counted both, the status lines said "Receiving" because some other rig was, and the CW pane interleaved two rigs into one stream of text that read as neither. The page is about the rig the operator selected — the one whose spectrum is on screen and whose audio is playing — so each panel now shows what that rig heard: rows, counts, latest-seen, status, the live picture a WEFAX or SSTV frame is painting, and the CW pane. Nothing is dropped on the way in. The map is the whole station's view, has its own rig filter, and would empty out if the plugins stopped feeding it, so the histories still hold every rig and the map still plots them. That also means a switch loses nothing: the runtime gained a rerender hook, which the rig switch calls, and switching back brings the other rig's traffic up again. The CW pane is the exception — a running stream of text cannot be unpicked after the fact — so it starts empty on the rig switched to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
15ff686542 |
[fix](trx-frontend-http): keep the mini views to the rig on screen
The decode SSE stream and the history behind it are not rig-scoped: every rig's decodes reach the browser, each carrying the rig that heard it. The panels on the decoder tabs want that — they aggregate the whole station — but the mini views over the waterfall caption the spectrum underneath, and they were reading the same unfiltered histories. A background rig copying APRS on another band put its frames over the active rig's waterfall. The mode gate did not help: it reads the mode of the rig on screen, so those frames appeared whenever that rig happened to be in PKT. Filter each overlay on the rig it belongs to, through one shared predicate that compares a decode's rig_id with the per-tab active rig already driving the spectrum and the audio. A decode that names no rig, and a session that has not learnt its rig list yet, still show everything. The FTx normalizer was dropping rig_id on the floor, so it now keeps it. CW needed more than a filter: its lines accumulate character by character, so two rigs copying at once braided their text into one unreadable line. Lines in progress are now kept per rig. The bar repaints in render() move into refreshDecodeBars(), which the rig switch calls as well — otherwise the outgoing rig's frames stayed on screen until the next state update — and which finally includes the CW bar. Closes #49 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
f396e8f235 |
[fix](trx-frontend-http): give satellite passes their own page
Pass predictions were a third view inside the Weather Satellite Decoder card, under Digital modes — a planning tool filed behind a decoder toggle, beside the FT8 and WEFAX panels it has nothing to do with. Nothing about knowing when a bird comes over belongs there. Move them to /satellites, reached from Tools alongside Statistics, Recorder, Settings and About: occasional destinations that live behind that menu rather than taking a slot in the operating strip. Adding a sixth strip button wrapped the phone nav onto two rows and cost the desktop strip its labels at 1280px, so the tab is hidden from the strip exactly the way its four peers already are — the nav is byte-for-byte what it was. The prediction code moves out of sat.ts into its own plugin that loads with the page, so the decoder card no longer carries it. Countdowns stop when the page is hidden and each visit reloads, since passes go stale while it is closed. The server grows a /satellites index route so a deep link or a refresh serves the SPA shell rather than a 404. Closes #47 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
18107ce07e |
[feat](trx-rs): receive SSTV pictures end to end
CI / lint (pull_request) Successful in 2m16s
CI / frontend (pull_request) Successful in 4m12s
CI / reuse (pull_request) Successful in 2s
CI / lint (push) Successful in 2m15s
CI / test (pull_request) Successful in 9m37s
CI / test (push) Successful in 7m36s
CI / frontend (push) Failing after 31s
CI / reuse (push) Successful in 3s
Wires the SSTV decoder into the stack, from the audio the server already has to a panel in the browser that shows the picture arriving. Server: a decoder task alongside the WEFAX one, running whenever the decoder is enabled and the rig is in a mode SSTV is sent in. A finished picture is written to the cache as a PNG and sent on as a message; the rows are sent as they decode, so a client can watch two minutes of Martin M1 fill in rather than waiting for it. Pictures join the decode history, are replayed to a client that connects later, and survive a restart. Protocol: SetSstvDecodeEnabled and ResetSstvDecoder, a sstv_decode _enabled flag in the rig state, two audio message types, and Sstv and SstvProgress on DecodedMessage. The history stores the message without its base64 payload -- the picture is already on disk, and a megabyte per entry is not what a history is for. Client: pictures land in their own history, and the PNG the server sent is written to the local cache so /sstv-images/ can serve it back. That endpoint and the WEFAX one now share their filename checks rather than each carrying a copy: no separators, no parent references, .png only. Web UI: an SSTV sub-tab beside WEFAX, with a live canvas the rows paint into at the line number they carry, a card for the last picture, and a filterable history with links to the files. Rows below the one arriving are grey rather than black -- not yet received is a different thing from received as black. A picture is not a spot, so neither pictures nor their progress updates reach the decode statistics; that exclusion list had grown by hand for LRPT and WEFAX and is now one named set. The decoder crate gains what the server needed to hand a picture on: to_png, to_png_base64 and save_png, with file names stamped in UTC so they sort. Panel behaviour is tested with the plugin runtime: rows painting at their own line numbers rather than in arrival order, a completed picture linked by file name alone with no server path in the page, a cut-off picture reported as partial, clearing, and the toggle following the rig state. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
a2c630a92b |
[feat](trx-frontend-http): put the tuned frequency in the address bar
CI / frontend (pull_request) Successful in 4m3s
CI / reuse (pull_request) Successful in 2s
CI / lint (push) Successful in 2m16s
CI / lint (pull_request) Successful in 2m16s
CI / test (pull_request) Successful in 8m11s
CI / test (push) Successful in 7m20s
CI / frontend (push) Successful in 3m10s
CI / reuse (push) Successful in 2s
A receiver spreads by being linked to, and there was nothing to link to: the routes carried the tab and nothing else, so "listen to this" could only ever mean a screenshot and a frequency typed out in a message. The query string now carries the dial -- rig, frequency, mode and bandwidth -- in both directions. Opening a link selects the rig, sets the mode, tunes, then applies the bandwidth: a mode change brings its own default bandwidth with it, so an explicit bw has to land after it. Frequencies are read the way someone writes them by hand (7074k, 14.074M) and written back as whole Hz, so what comes out of the address bar is the same link in canonical form. After that the address bar keeps up with the dial, which is what makes it copyable at any moment rather than only at load. It is rewritten with replaceState -- tuning is not navigation, and a swept dial would otherwise bury the back button. A link button in the top bar copies the current link; it folds into the overflow menu when the bar is tight. Applying a link changes the radio, so an rx session says so instead of failing control calls one at a time. A tab listening to a virtual channel leaves the address alone rather than publishing a frequency the rig is not on, and bw is skipped in both directions on rigs without filter control, which would only refuse it. The fixture pinned every state frame to 100 MHz plus jitter to keep frames distinct, so no test could observe tuning at all. The jitter moves to the S-meter and the fixture echoes set_freq/set_mode/ set_bandwidth, as it already did for squelch. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
09634eb851 |
[fix](trx-frontend-http): tidy up the map's filter bar
CI / lint (pull_request) Successful in 2m21s
CI / test (pull_request) Successful in 8m7s
CI / frontend (push) Successful in 2m57s
CI / reuse (push) Successful in 3s
CI / frontend (pull_request) Successful in 3m47s
CI / reuse (pull_request) Successful in 3s
CI / lint (push) Successful in 2m16s
CI / test (push) Successful in 7m19s
The bar explained itself in prose: "All bands visible by default" sat between the chips and the next group, taking width the bar could not spare and reading as a stray line of text. An "All" chip says the same thing in a chip's width and gives the selection somewhere to be undone. Band chips also came up dimmed at the very moment every band was on the map -- an empty selection is no filter at all, so nothing is dimmed until something is picked. The path toggles drop their "On"/"Off" suffix, which cost most of a row and only repeated what their own highlight already said; state moves to aria-pressed and the tooltip. The rest is alignment. The rule dividing the buttons from the filters is drawn on the button block's edge, and a centred block left it floating as a stub beside a two-row bar; stacked, it lay down the left of a block that sits underneath. The labels sat at their natural widths, so each row's first control started somewhere different, and the two pairs of phase buttons differed in width, so the groups after them missed each other by four pixels. One gutter for every label, one width for both pairs, and the search field moved last where it can take the room the fixed-width groups leave. The map layout test now covers the chips, the divider's height and the rows' shared start. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
88d04253ca |
[fix](trx-frontend-http): move the map's fullscreen and filter toggles into the bar
Fullscreen and Hide Filters floated in their own block over the map's top-right corner, separate from the filter bar they sit beside. Put them at the right-hand end of the bar, behind a separator. What made this awkward before is that Hide Filters cannot live inside the thing it hides, so the collapse now applies to the filters alone: the bar keeps its two controls and shrinks to them at the map's right edge, leaving the whole map visible and the way back one click away. Fixes #38 Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
026f816ddb |
[fix](trx-frontend-http): replay stored decodes onto the map when it loads
The map module is lazy: it arrives when the Map tab is first opened, which is normally long after startup restored the decode history. Until then aprsMapAddStation, aisMapAddVessel and vdesMapAddPoint are undefined, and the decoders' `if (lat != null && ... && fn)` guards quietly dropped every restored position. Nothing replayed them once the module did arrive, so the map came up empty and filled in only from decodes heard afterwards -- a station heard once was never plotted at all. A second reload appeared to fix it because the cached module then loaded early enough to win the race against the history fetch. Give DecoderPlugin an optional syncMap(), implement it for APRS, AIS and VDES over the history each already retains, and have map-core call trxPluginRuntime.syncMapAll() as it attaches. The add functions are keyed by callsign, MMSI and point, so replaying updates in place and cannot duplicate a marker; the replay runs oldest-first so tracks are rebuilt in the order they happened. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
39f551c914 |
[fix](trx-frontend-http): hold the APRS symbol column open for frames without one
renderLocalAprsSymbol() returns nothing when a packet carries no symbol table or code, so those rows lost the icon's 24px slot and every column after it -- callsign, type badge, summary -- slid left against the rows around them. Frames that do carry a symbol then read as indented. Render an empty slot of the same size instead, so a list mixing position reports with messages and telemetry still lines up. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
6d25ecdc11 |
[feat](trx-frontend-http): give the AIS list the same log shape
A message was three stacked lines — time and name, then MMSI and route, then motion, distance, position and age — so a screen held eight of them. It is one line now: time, vessel, message type, and what the message says, opening in place for the MMSI, the channel frequency, the route, the age, the fix and a jump to the map. Twenty-two fit where eight did. What a message says depends on what it is. Position reports give the fix and the motion; the static and voyage reports that carry no fix give the callsign and where the vessel is bound. Both fall back to whatever fields are present rather than showing nothing. The row vocabulary the APRS list introduced is no longer APRS-specific — the classes are decode-line and decode-expanded now, shared by both, and identity sits in fixed columns so the summaries line up down the list instead of starting wherever the callsign happens to end. The three summary cards above the list go the way of the APRS ones. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
37987b2779 |
[feat](trx-frontend-http): make the APRS list a log, and read the payloads
A frame was a card five lines tall — timestamp, a meta line, the information field as it arrived on the air, three buttons, and a Details panel repeating the four things already on the row — so five frames filled the panel and the payload was left to be decoded by eye. A frame is one line now: time, station, type, and what the frame says. It opens in place for the path, the CRC, the raw field, its bytes and the actions. Twenty-one frames fit where five did. And the information field is read rather than echoed. Weather reports give temperature, wind, humidity and pressure; telemetry gives its sequence and channels; a message gives its addressee and text; a position gives the fix, course and speed, and the comment the station wrote. Anything that cannot be summarised falls back to the raw field, which is in the expanded view either way. HF APRS had a copy of the same forty lines of markup, differing by one badge, so both now build their rows from one function in the shared module — the CSS is shared between them and this would have broken it otherwise. Both headers lose their three summary cards for a line of counts beside the filters, which frees another fifth of the panel. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
b48cc23d6e |
[fix](trx-frontend-http): keep the rig names through the state stream
The picker and the header showed each rig's lowercase id instead of its configured name. applyRigList takes the names as a parameter defaulted to an empty map, and the state-update path passes only the rig ids — names come from /rigs, not from a state frame — so that call landed on the default and the body, which treats "an object" as "here are the names", cleared them. One frame after load the names were gone for the rest of the session. Omitted now means no news rather than no names. The fixture is why this was invisible: it pushed an identical status payload every tick and the client skips a frame equal to the last, so render never ran and neither did the call that did the damage. Its event stream varies between frames now, as a real one does. Which immediately caught a second fault: state frames arrive continuously, and one sent before the server applied a new squelch threshold snapped the line back to where it had just been dragged from. A local change outranks the echo for two seconds, the same idea as the optimistic frequency guard beside it. The fixture also records what /set_sdr_squelch sets and reports it back afterwards — the drag test had been passing against a server that ignored the write. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
c1899229a0 |
[fix](trx-frontend-http): stop the decode history replay giving up at 20s
Reloading a second time sometimes showed history the first load did not, and the safety valve is why: it called one function that both released the buffered live decodes and tore the history worker down, so any load where the replay had not finished inside twenty seconds — a large backlog, a cold cache, a slow link — dropped whatever had not arrived, without a word. A reload got another go at it, and the second one is faster because everything is cached by then. Those are two separate things now. At the timeout the live decodes are released so the panels are not held back, the replay carries on, and the progress says so. The fallback's error path retries once and then says "Decode history unavailable" rather than leaving the operator to guess whether there was anything to see. The progress is no longer a scrim. It was fixed to the whole viewport with a wash over the page — the waterfall, the decode panels, all of it — for the length of the replay, which is exactly when there is something worth watching. It is a corner card with a bar: indeterminate while the payload is on the wire, then filling as N of M messages replay. None of this was reachable from a test. /decode/history answers in CBOR and the worker reads the body as CBOR unconditionally, but the fixture served JSON, so every browser run had been exercising the client's retry path and never its history path. It encodes CBOR now, including the 64-bit form the millisecond timestamps need, and decode-flow serves 1200 records and holds the client to restoring all of them on the first load, showing progress while it does, and never covering the page with it. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
84a99a3636 |
[fix](trx-frontend-http): load the decoders that own the digital modes panels
AIS, VDES and both APRS decoders were listed under the map plugin group alone, so opening Digital modes and clicking AIS or APRS gave an empty panel reading "Connected, listening for packets" while the decodes piled up unprocessed in the plugin runtime. They appeared only if something had opened the Map tab first, which flushed the queue. There is also a map-data group naming exactly those four that nothing loads: the loader is called with tab names and no tab is called map-data. They load with the tab whose panels they fill now. map-core stays lazy, since their calls into it are optional and the Map tab can go on paying for Leaflet by itself. tests/decode-flow.mjs follows a decode from the wire to the map: an AIS vessel and an APRS beacon arrive on /decode, and it asserts both panels fill with the map module confirmed absent, the mini view names the vessel and offers a pin, following that pin lands on /map centred on the vessel, and both decoders leave a marker. Nothing exercised any of this before — the fixture served an empty decode stream, which is how the map links came to be broken for every decoder at once. The fixture stamps decodes as it sends them, since the client prunes anything outside the retention window, and repeats them, since the views collapse by vessel and need more than one frame to behave. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
84bdf2593c |
[fix](trx-frontend-http): measure whether the tab bar fits, and watch for it
CI put the tab strip 9px into the controls at 1440px with no scaling at all, on a bar that had every degradation step available to it and used none of them. It used none because nothing thought anything was wrong: the fit test was an arithmetic estimate — identity + nav.scrollWidth + actions.scrollWidth + a 48px allowance for the gaps — and on a platform whose fonts run wider than the one it was written on, that allowance no longer covered what it stands for. An estimate that says "fits" stops the ladder before its first rung. It reads the geometry now: the controls have to stay inside the bar, and no tab may reach them. That is the same measurement the test makes, so the two cannot disagree about any platform's metrics. The tabs are the subject rather than the nav's box because the nav shrinks below its content — the box gets smaller while the tabs keep their width and slide underneath the controls. A second fault turned up while probing this: the strip only reflowed on window resize. The rig name arriving from the server, the style picker filling in, a font swapping in wider metrics — each changes what fits without touching the window, and the bar sat there as it was through all of them. A ResizeObserver on the bar and the controls covers those, and document.fonts.ready covers the swap. The guard sweeps text scales and adds a station name too long for the bar, but it should be said plainly: it passes against the old code too. Nothing here reproduces on this machine — a 4px viewport sweep from 1080 to 1500, three wide font stacks and scales from 1.0 to 3.0 all failed to make the old estimate lie. What is fixed is the mechanism that could. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
2c56d82a81 |
[feat](trx-frontend-http): make the SQL label the squelch switch
Clicking SQL turns the squelch on and off. The label and the button beside it said the same thing twice — one naming the control, the other reading "On" or "Off" — where the name itself is the obvious target, and the dot already carries the state: grey when off, green while the gate passes, amber while it holds. The pressed state is on the label, so the switch reads the same to a screen reader as it looks. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
0fc2115973 |
[fix](trx-frontend-http): make map links work before the map has loaded
The AIS and APRS mini views link each position to the map, and neither did anything: the map module installs itself lazily, and it was the one defining window.navigateToAprsMap, so until something had opened the Map tab the global did not exist. AIS calls it inline from onclick and threw "not a function"; APRS guards the call and so failed silently. The grid links on FT8, FT4, FT2 and WSPR rows went the same way through navigateToMapLocator. The app owns both globals now, installed at startup. They record the target, switch tabs through navigateToTab — the only path that materialises the panel from its template, loads the module and updates the history entry, none of which the module's own hand-rolled tab switch did — and the target is applied once the module reports ready. The module keeps the focusing, which is its job, and exposes it as focusMapPosition and focusMapLocator. The smoke test now calls the link from a cold page, asserting the map module is not loaded first so the check cannot pass by accident. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
2c1df75d19 |
[fix](trx-frontend-http): measure auto squelch from the meter
Auto took the spectrum's noise floor and added 6 dB, but the threshold is compared against the channel level the meter reports, and the two sit a long way apart: the gap is set by the FFT size and window, the channel bandwidth, the decimation, and peak-versus-mean statistics. Measured on white noise it runs +22.1 dB at 48k/8k/3k, +18.7 dB at 240k/24k/12k and -1.2 dB at 1.92M/24k/12k — a 23 dB swing across ordinary configurations. Only the last of those is anywhere near right, so on a narrow span Auto set the gate some 20 dB below the noise and it never closed. It now reads the same number the DSP compares: the 20th percentile of the meter over the last ten seconds, plus 5 dB. The percentile keeps a burst of traffic inside the window from dragging the estimate up, and 5 dB clears the meter's own jitter, which measured 0.9-1.6 dB. Nothing in it converts between scales, so no part of the signal chain can put it out again. With no history yet — a fresh connection, a rig switch — it listens for a moment rather than refusing. The fixture gained a streaming /meter, without which there is nothing to measure, and the spectrum test pins auto to the meter it serves. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
aefd36c4b1 |
[feat](trx-frontend-http): set the squelch on the spectrum, in dB
The threshold is in dB, and since the squelch fix that is the scale the spectrum axis and the S-meter are labelled in — so the control belongs on the plot, at the level it gates. A dashed line spans the spectrum at its threshold with a grip that reads it out, dragged like the bandwidth edges, green while the signal is above it and amber while it gates. Arrow keys move it a dB at a time for anyone not using a mouse. The audio row keeps a compact version: the dB, an indicator lit from the same meter the DSP compares against, Auto, and an enable toggle that no longer doubles as the level. The slider ran 0-100% over that dB range, which gave the operator a number with nothing on screen to relate it to, and zero meant "disabled", so turning the squelch off to listen threw the threshold away. Auto now says which level it picked. Two things the browser could only show once it was on the plot: the grip landed underneath the split control at the right edge, which swallowed its pointer, and dragging to the foot of the axis hid the line — and the grip with it — instead of pinning it where it could be dragged back. The fixture could not exercise any of this: /audio answered 404, which hides the audio row and the control inside it, and the status carried no filter block, which is what tells the client the rig has a squelch at all. Both now look like an SDR, and the spectrum test drives the line. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
4fa191e9b6 |
[fix](trx-frontend-http): serve the band plan to every session
/bandplan.json needed the control role. Route access is decided by suffix for static assets — .js, .css, .png and so on — and ".json" is not among them, so the band plan matched nothing and fell through to the catch-all. It is compiled into the binary and identical for every user, so it is public now, like the rest of them. Two things followed from that. Read-only sessions never saw a band plan at all. And since the page asks for it during startup, the request can land before the session is established: that 401 was swallowed by an empty catch and never retried, which is why the allocations sometimes only appeared after a manual reload. So the client no longer hides the failure, retries once the auth gate clears — which is exactly when a startup 401 becomes fixable — and schedules a draw when the data lands, since the strip is painted from the spectrum draw and a rig sitting between frames would stay blank. The fixture can now refuse the first request the way the server did, and the spectrum layout test holds the client to recovering from it. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
c073d03ffb |
[feat](trx-frontend-http): reorder the controls tray sections
The radio's own settings now come first, then audio, then the scheduler: Advanced radio controls, Audio controls, Scheduler controls. The advanced section is not in the markup — ui-core builds it at runtime and gathers the SDR settings, virtual channel and TX limit rows into it, appending the result, which put it last however the markup was ordered. It is inserted ahead of the audio section instead. The signal readout and the TX meters stay where they are, between the controls and the sections: they are readouts rather than a section, and on an SDR the spectrum covers them anyway. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
730f129404 |
[fix](trx-frontend-http): let the header identity give way before the tabs
CI still put the tab strip into the controls, now at 1100px — the narrowest bar in the app, since the bookmark gutters take 9.5rem a side above that width and leave 756px against 871px at 900px. With the controls already in the overflow menu and the tabs already down to icons, nothing else could give, and what gives by default is the strip: it is the one item allowed to shrink below its content, so its tabs keep full width and slide under the controls, out of reach. The identity block takes the squeeze instead, ellipsised. A clipped station name is still readable; a destination hidden underneath the controls is not. The guard that was supposed to catch this scaled only the tabs and the controls, not the title and subtitles — which is exactly what runs out of room — and skipped 900px. It now scales every piece of text in the bar and checks all four widths. Measured across text scales from 1.0 to 3.0 at each width, the bar keeps its 16px allowance everywhere; before this, 1.6 and above overlapped at 1100px. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
f95f3d0104 |
[feat](trx-frontend-http): centre the radio controls, split off the mode row
The controls every rig has — mode, wheel, tune step, transmit — now sit as a centred block rather than packed against the left edge. What the current mode adds moves out from among them: WFM's six controls stretched the row sideways whenever it was active, pushing the wheel and the step pickers off centre, and SAM did the same on a smaller scale. They get a row of their own below a divider, which appears and leaves with them — an empty one would still take a track and a gap in the tray and draw its divider under controls it has nothing to do with. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
e70e82c8c0 |
[feat](trx-frontend-http): rebuild the general radio controls row
Mode was a full-width select: 483px of the row to display "FM". The modes are three or four characters and there are at most twelve, so they become a segmented group like the Unit and Step Scale pickers beside them — a third of the width, and one click instead of two. The <select> stays as the mode's value. A dozen call sites and several plugins read #mode.value, so replacing it outright would have reached much further than a layout change should; it is hidden from sight and from assistive tech, the buttons write to it, and everything downstream runs unchanged. Every writer re-syncs the buttons, the plugins through a new trxCore.syncModePicker. The row itself was a grid with a track per column, but the WFM, SAM and transmit columns are hidden on most rigs, so it ended in some 500px of hole. It packs left now. Same fault one level down: the power buttons sat in three fixed tracks, so a rig with neither transmit nor lock kept two empty ones and left its label chip stranded at the far edge. Unit and Step Scale move out of the frequency row and in beside the wheel and the +/- they modify, which were some 600px away. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
23cc0db1fa |
[fix](trx-frontend-http): drop the tabs to icons when the bar runs out of room
CI put the tab strip 9px into the controls at 1440px on a run that passed locally: its system font is wider, and the bar had no move left to make. Controls are moved into the overflow menu until the bar fits, but once all of them were in the menu nothing else gave — the nav may shrink below its content, so the tabs kept full width and ran under the controls, leaving the destinations nearest them unclickable. Labels dropping to icons was the other half of the answer, but it hung off a max-width:1360px media query and so was unavailable at 1440px. That class now goes on by measurement, as the last step after the menu is exhausted, which is the same reasoning the controls' own fit test already uses: how much fits depends on the rig name and on how wide the platform draws the labels, not on the viewport. The class is cleared before measuring so the decision cannot ratchet, and icon widths are fixed, so it always buys back the labels' width. Labels now stay put between 1100px and 1360px while they fit, with the style picker and theme toggle behind the overflow menu instead. The suite could not have caught this: it passed on the fonts of the machine that wrote it. The layout section now repeats its fit check with the bar's text scaled up, which reproduces a wider system font anywhere — with this fix reverted it fails on macOS too. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
657f952a2c |
[fix](trx-frontend-http): hold the strips' place above the spectrum
Tuning across a band edge moved the whole page under the cursor: the band plan strip is in flow, so a range with no allocations collapsed it from 18px to nothing and dragged every element below it up (measured at 1600x950: overview top 118 to 100, footer 1026 to 1008). It now keeps its height whenever a band plan could be drawn at all, and only gives it back when the feature is off, has no data, or there is no spectrum. The bookmark rail gets the same treatment for consistency, though it never moved anything — it is absolutely positioned over the top of the overview. It stays up and blank rather than vanishing. Which exposes something the rail was already doing wrong: it covers the top of the plot, and a bare div still takes pointer events, so whenever bookmarks were in range that band of the overview could not be dragged or scrolled. Only the chips are targets now. tests/spectrum-layout.mjs covers this: it streams spectrum frames, tunes between a band with bookmarks and allocations and one with neither, and asserts nothing moves and that the rail lets clicks through. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
b2fbcb318d |
[fix](trx-frontend-http): stop Tools lighting up on every refresh
navigateToTab marked the Tools button by asking whether the destination tab was displayed, which is the right question at the wrong moment: the first route navigation runs while the card is still behind the loading state, where every tab computes to display:none. Refreshing or deep linking to any page therefore lit Tools alongside the real destination, and nothing re-evaluated it once the page appeared. Membership of the Tools menu answers the same question without needing anything laid out, and still reads the grouping ui-core installs rather than a second copy of it. The smoke fixture now serves the SPA shell for route paths the way the server's per-tab index handlers do, so a deep link no longer 404s and the case is testable at all; two of them are asserted. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
44721b579c |
[fix](trx-frontend-http): fill the map column down to the footer
The windowed map was capped at 75% of the viewport height and at a width-derived aspect ratio, which left a dead band under it: 69px at 1600x950, and on a 420px-wide phone a 270px map on an 800px screen. Neither cap was doing useful work now that the stage spans the full width, so the map fills the column down to the footer instead. Growing into the footer needs a bound: once the column is tall enough to push the footer below the fold, using its position would push it further on every pass, so the bottom edge is clamped to the viewport. Growth then consumes the column's spare height and settles in one pass. Also drops three mapIsFullscreen() branches in the windowed path that could never be taken — the fullscreen case returns above them. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
6f53592041 |
[feat](trx-frontend-http): rework the page footer
The footer floated in space below the content with no rule to close the page, its two clusters sat on a text baseline that left the source pill hanging, and the status hint was a plain line of text a size larger than the attribution beside it. Now a hairline closes the page the way .tab-bar opens it, the clusters centre on one line, and the attribution drops the opacity it stacked on top of --text-muted, which had put it below a readable contrast ratio. The status hint becomes a pill with a state dot: green when ready, amber while a command is in flight, red on connection loss. The colour comes from a data-state attribute, so every hint now goes through setPowerHint instead of assigning textContent directly. --status-ok carries the indicator green; .about-status-on picks it up too, which darkens it on light themes where the old value was barely legible. Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
5b7dd493d4 |
[feat](trx-frontend-http): draw APRS symbols from the sprite sheets
CI / lint (pull_request) Successful in 2m16s
CI / test (pull_request) Successful in 8m12s
CI / frontend (pull_request) Successful in 3m2s
CI / reuse (pull_request) Successful in 3s
CI / lint (push) Successful in 2m24s
CI / test (push) Successful in 7m28s
CI / frontend (push) Successful in 2m11s
CI / reuse (push) Successful in 3s
Resolve a table/code pair to a sprite cell in aprs-shared, and use it from both the packet lists and the map markers, which had each been printing the raw symbol character in a bordered box. A table identifier of / or \ selects the primary or alternate sheet directly. Anything else is an overlay character, which the APRS spec draws on top of the alternate symbol -- so those stack the overlay sheet over the alternate one rather than picking a sheet. Codes outside 0x21..0x7E have no cell and keep the old character box. The sheet URLs stay in the stylesheet so a min-resolution query can swap in the retina sheets; only the cell offset is computed and set inline. Map markers share the helper through the plugin chunk, so the map stays free of any remote symbol fetch. Verified in a browser against the real stylesheet and sheets: /> is a car, /_ a WX circle, /& an igate diamond, \n a red triangle, and the overlays S> and 7# carry their character on the alternate symbol. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018huL1ELyr86yVqfAabtioA Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
863a6d8fd4 |
[fix](trx-frontend-http): restore decode history from the stored records
CI / lint (pull_request) Successful in 2m21s
CI / test (pull_request) Successful in 8m19s
CI / frontend (pull_request) Successful in 3m1s
CI / reuse (pull_request) Successful in 2s
CI / lint (push) Successful in 2m16s
CI / test (push) Successful in 7m26s
CI / frontend (push) Successful in 2m11s
CI / reuse (push) Successful in 2s
Replay required every restored record to carry a string `type`, and stored records do not have one: an AIS entry holds mmsi, lat, lon, crc_ok and its decoder's own fields, nothing more. The filter therefore discarded all of them, and did it silently — the fetch returned its full payload, the worker decoded it, and no error was logged, so the history simply never appeared. That field identifies live SSE frames, which do carry it, which is why only replay was affected. History arrives already grouped and the group's kind is delivered alongside the messages, so `type` was never needed to route them. Require only that a record is an object. Confirmed against a live server: the first restored group is AIS, and its records expose their decoder fields with `type` undefined. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
92697b11c5 |
[feat](trx-frontend-http): mark Tools active for its destinations
CI / test (pull_request) Successful in 8m9s
CI / test (push) Successful in 7m22s
CI / lint (pull_request) Successful in 2m15s
CI / frontend (pull_request) Successful in 3m1s
CI / reuse (pull_request) Successful in 2s
CI / lint (push) Successful in 2m16s
CI / frontend (push) Successful in 2m10s
CI / reuse (push) Successful in 2s
Grouping Statistics, Recorder, Settings and About behind Tools left the tab strip looking identical on all four: the destination's own button carries the active class, but the strip hides that button, so nothing was marked. The page titles named the page without saying how you got there. Mark the Tools button when the active destination is one the strip hides. That state is read from the button's computed display rather than from a second copy of the grouping, so the two cannot drift: whatever ui-core puts in the menu lights up Tools, and a destination promoted back into the strip stops doing so with no further change. Tools already carries the tab class, so the existing active styling applies unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
41ddecf5a7 |
[fix](trx-frontend-http): label the overflow tab Tools and hide its glyph
CI / test (pull_request) Successful in 8m17s
CI / lint (push) Successful in 2m18s
CI / lint (pull_request) Successful in 2m16s
CI / frontend (pull_request) Successful in 3m6s
CI / reuse (pull_request) Successful in 3s
CI / test (push) Successful in 7m25s
CI / frontend (push) Successful in 2m11s
CI / reuse (push) Successful in 3s
The button rendered as "•••More": the dots span carried no styling at all, so the glyph sat flush against the label instead of behaving like the icon it is. Every other tab hides its icon while labels are shown and swaps to it when they are not; the dots now follow the same rule, so the button reads "Tools" beside the other labels and becomes the glyph alone in the icon band. "More" also said nothing about the destinations behind it. The menu holds Statistics, Recorder, Settings and About, so name it Tools and give the button an aria-label that spells that out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
b06c37affa |
[fix](trx-frontend-http): lift the header menus out of the header
CI / lint (pull_request) Successful in 2m24s
CI / test (pull_request) Successful in 8m21s
CI / frontend (pull_request) Successful in 3m5s
CI / reuse (pull_request) Successful in 3s
CI / lint (push) Successful in 2m17s
CI / test (push) Successful in 7m36s
CI / frontend (push) Successful in 2m9s
CI / reuse (push) Successful in 3s
Fixed positioning escaped the clipping, but not the stacking: the header carries `z-index: 2`, which makes it a stacking context, so whatever z-index a menu inside it carries is confined below level 2. The spectrum overlays paint as high as 9600, so both dropdowns opened underneath them. Reparent each menu to the body when it opens. Leaving that subtree is the only way out of an ancestor's stacking context, and the menus are already positioned in viewport coordinates, so nothing else about them changes. The outside-click test now considers the menu as well as its wrapper, since the two are no longer nested. Verified by hit testing rather than by inspecting z-index: elementFromPoint at the open menu's centre returns the menu. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
b4912f5879 |
[fix](trx-frontend-http): render the header menus above the page
CI / lint (push) Successful in 2m17s
CI / test (push) Successful in 7m23s
CI / lint (pull_request) Successful in 2m16s
CI / test (pull_request) Successful in 8m13s
CI / frontend (pull_request) Successful in 2m59s
CI / reuse (pull_request) Successful in 3s
CI / frontend (push) Successful in 2m10s
CI / reuse (push) Successful in 3s
Both header dropdowns were laid out inside the bar rather than over the page. The navigation menu opened as an 18px sliver positioned above its own button, and the overflow menu did not appear at all. Two causes. The tab strip kept `overflow-x: auto` from when it scrolled, which clips an absolutely positioned descendant — and the strip is what the navigation menu anchors to. The strip no longer scrolls, since the occasional destinations moved behind More, so the property and the edge fade that went with it are both gone. Anchoring in fixed coordinates at open time addresses the general case: an absolutely positioned menu is clipped by any scrolling ancestor and trapped inside whatever stacking context its ancestors create, so it can be squashed inside the bar or painted underneath page content. Fixed coordinates answer to the viewport, and the menu flips above its button near the bottom edge. Clearing `right` when setting `left` keeps the menus at their natural width: the stylesheet pins them to the right of their anchor, and leaving that in place stretched them across the bar — 845px for a four-item list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
3a9bf1b7ce |
[fix](trx-frontend-http): drop the rig description from the top bar
The header repeated the active rig's hardware string and mode list beside the rig picker. With a real SDR that reads SoapySDR driver=airspyhf,serial=c852eb5dd23539f8 · RX · SDR filters · LSB · USB · CW · CWR · AM · +7 modes which is longer than every other control in the bar combined, and it is already on the About tab in full, split across its Rig, Active rig, Connection, Modes and VFO rows. Remove the element and the builder behind it. Rig switching keeps its feedback through the existing hint channel rather than by briefly rewriting a permanent label, and the identity that belongs in a header — the rig's display name — stays in the left subtitle. The freed width is not spent: the tab strip now reaches Settings before it needs to scroll, where it previously faded out during Statistics. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
eee3630f04 |
[feat](trx-frontend-http): compact single-row top bar
The header's height depended on the viewport width, and not even monotonically: 112px at 1440, 169px at 1100, 131px at 900, 246px at 720. Both control groups wrapped, so every width produced a different ragged block — eight page tabs across four rows at 1100px, and action controls across three. Four different control heights (32, 34, 45 and 54px) sat in the same row, the 54px one being the rig picker with its summary stacked underneath, and on narrow viewports the icon buttons stretched to fill half the row, rendering a play triangle centred in a 249px box. Lay both groups out as one row that never wraps. Controls are a uniform 2rem and no longer stretch, the rig summary sits inline beside its select, and the page tabs scroll instead of wrapping. Secondary controls — layout, style and theme — move into an overflow menu when the bar cannot hold them, leaving audio, record and the rig picker inline. Deciding when they no longer fit needs natural widths, not rendered ones: the nav has min-width 0 and scrolls, so it always shrinks to the leftover space and always reports scrolling, and the bar reports overflow even when nothing is clipped. scrollWidth on the scroll container is its unconstrained content width, which is what the fit test compares against the space available. Measured after the change: 72px at 1440, 1280, 1100, 900 and 480, every control 32px, nothing clipped at any width. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz Signed-off-by: Stan Grams <sjg@haxx.space> |
||
|
|
b31790ff48 |
[fix](trx-frontend-http): keep layout sections togglable
CI / lint (pull_request) Successful in 2m21s
CI / frontend (pull_request) Successful in 3m3s
CI / reuse (pull_request) Successful in 3s
CI / test (pull_request) Successful in 8m32s
CI / lint (push) Successful in 2m30s
CI / test (push) Successful in 8m36s
CI / frontend (push) Successful in 2m13s
CI / reuse (push) Successful in 4s
A layout seeds the collapsible sections; it should not hold them there. applyLayout writes the disclosure state of the advanced, audio and scheduler sections, and it runs far more often than a layout change: render() calls applyRigList() for every SSE frame carrying `remotes`, which calls setActiveRig() unconditionally, which re-applies the layout. An operator who expanded a section that the selected layout collapses by default therefore had it shut again within about a second, which read as the section being locked by the layout — most visibly the scheduler under Compact. Write the section state only when the layout actually changes, or the first time each section appears in the DOM, since the advanced controls are constructed after the first applyLayout call. Switching layout still reseeds every section, so choosing a layout keeps meaning "give me these defaults". Verified in Chromium: with Compact selected, activating the scheduler summary opens the section and it survives both a rig-state refresh and a repeated applyLayout, while selecting Full still reseeds it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz Signed-off-by: Stan Grams <sjg@haxx.space> |