The Flasher's saved configurations started as [] with no loading
flag, and the Saved Firmwares tab is the default tab, so every load
painted "No saved configurations yet." until the fetch returned. The
api helper returned [] on a non-OK response too, so a failed fetch
read as an empty list.
useStatistics used useState() with no initial value, so the
Statistics page's first render saw undefined and flashed "Failed to
fetch statistics" for one frame before the effect set {} and the
spinner appeared.
Invariants now enforced and tested:
* null = pending, undefined = fetch failed, [] / object = loaded. The
api helper returns undefined (and toasts) on a non-OK response.
* The Saved Firmwares tab shows a placeholder while pending, an error
on failure, and "No saved configurations yet." only once the server
confirmed there are none. Only the newest configs request may write
state, since save and delete refetch.
* The Statistics page shows its loader while pending and its error
only for a failed fetch.
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* bookmarks: user bookmarks in the navigation bar, stored in bookmarks.yaml
A "Bookmarks" tab between Calculators and Flasher opens on hover or click and
lists the user's bookmarks, with directories as flyout submenus (Menu.Sub).
The mobile hamburger shows directories as labelled groups instead. The tab
always shows, with an "Edit bookmarks" item, so the feature can be found
before any bookmark exists.
Bookmarks live in a new config-only bookmarks.yaml: a nested tree where a
node with `children` is a directory and a node with `url` is a bookmark.
Every node carries an integer id so the API can rename, move and delete
without relying on names. A URL may be a path on this WROLPi, a port on this
host (`:8096/` or `http://:8096/`, resolved against whatever hostname the
browser used, so it works over LAN IP, mDNS and the hotspot alike), or an
absolute URL. Each bookmark chooses whether to open in a new tab.
The editor at /more/bookmarks follows the playlist items table: numbered rows,
up/down/edit/delete, and inline "Add a bookmark" / "Add a directory" forms
with a Location picker. Deleting a directory asks first, since its contents
go with it. The menu and the editor are in the component gallery.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* bookmarks: mark More current when the editor is folded inside it; rem indents in the mobile menu
Found on the QA Pi at a width where Bookmarks overflows into More: the More
button carried no mark on /more/bookmarks, because the bookmarks link has no
`to` for isLinkActive to match. isLinkActive now knows the bookmarks link owns
its editor page, and the Bookmarks tab uses the same rule.
The mobile menu's directory labels sat a few pixels left of the labels around
them: the indent was in em, and a Menu.Label is set smaller than a Menu.Item.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* bookmarks: parse URLs instead of matching a prefix; validate the whole tree on every write
The URL contract was checked by prefix on both sides, and a prefix is not a
form: `//evil.com` starts like a path and `:8096@evil.com` starts like a port,
and a browser takes both off this host. A path is now a single leading slash
that resolves to this origin, a port form is an optional scheme, `:`, digits
and then only a path, query or fragment (no `@`, no backslash, no control
characters), and anything else must parse as http(s). The same rules live in
wrolpi/bookmarks.py and BookmarksContext.js, with the same bypass strings in
both test suites.
The config's dataclass validator only saw that `bookmarks` was a list, so a
hand-edited file or the generic config API could store a `javascript:` URL
that the bookmarks API would refuse, and the nav rendered it. The config now
validates the tree itself on every write and import: URL forms, exactly one of
url/children, unique integer ids, boolean new_tab, and no cycles (a YAML
anchor can build one). An invalid file is not imported and, since it was never
successfully imported, is never overwritten.
On the page, a stored URL that does not resolve is rendered as text, never as
an href. A refused add keeps what was typed instead of clearing the form, and
an edit whose move is refused still refreshes the nav (the rename was
committed) and keeps the modal open. An IPv6 host is bracketed before a port.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* help: document Bookmarks
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
The dashboard threw on arrival from any other page:
ReferenceError: Cannot access 'useCalculators' before initialization
at Module.useCalculators
at DashboardCalculators
`Calculators.js` exports `useCalculators` as a `const` and imports the ten
calculators it renders. Five of those reached back up to `Apps.js` for
`ColoredInput`, and `Apps.js` imports `CalculatorsPage` from
`Calculators.js` -- so the hub was in a cycle with its own leaves:
Calculators.js -> calculators/ElectricalCalculators.js
-> Apps.js -> Calculators.js
Webpack compiles a `const` export to a live getter, and a getter read
before the module body reaches the declaration THROWS rather than
returning undefined. That is the whole mechanism behind this particular
error, and it names the const it could not reach.
`ColoredInput` is a text field with a Label welded to one edge -- the
calculators' unit markers. It has no business in the module that ROUTES
to the calculators, so it moved to `components/ui/Inputs.tsx`. Nothing in
`components/ui` imports from `components/`, so a control kept there cannot
close a loop like this again. Gallery entry added, per the standing rule.
`src/import-cycles.test.js` is the guard: a small import-graph walker,
scoped to hubs that export a `const` into their own subtree. It fails
with the cycle printed, and it self-checks against a known cycle so it
cannot pass by finding nothing. Verified red with the cycle planted back.
HONEST LIMIT: the guard is static because I could not reproduce the
RUNTIME error in a test. TDZ needs a module to be mid-evaluation when the
getter is read, and by the time any spec body runs every module has
finished; babel also papers over it, handing back undefined where webpack
throws. I tried an e2e that navigates to the dashboard and collects
window errors -- it passes with the cycle present, so it would have been a
test that cannot fail for the reason it claims, and I deleted it rather
than ship it. A react-refresh trigger did not reproduce it either. What
is checkable, and what is genuinely the defect, is the cycle.
1080 jest / 60 suites, 158 in ui-layout.cy.js, clean build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Phase 2, part 3. Baseline 63 -> 50.
Common.js is the one that mattered: 51 files import it, so its Semantic usage
was propping up the rest of the migration. Everything in it now comes from the
library -- Paginator, the two search inputs, DirectorySearch, TabLinks, the
toggles, the messages, APIButton, Breadcrumbs, SimpleAccordion, IframeViewer,
CardPoster, FileIcon, LoadStatistic, the theme menu, and the hotspot icons.
Also migrated: Placeholder, Apps, ArchivePreview, CbzViewer, AddToPlaylist,
TaggedDeleteConfirmModal, the collection edit/tag forms, and four inventory
components.
New in the library, all in the gallery and tested:
SearchBox Semantic's `<Search category>` with its grouped results shape
kept, because four call sites already build it. A real
combobox rather than a div that listens for clicks.
Pagination Semantic's prop names; onPageChange gets the page number.
TabBar the strip only -- routing stays with the caller.
IconStack Semantic's IconGroup, named once for assistive technology.
Table.HeaderCell sorted/onSort -- a real button carrying aria-sort.
Button/IconButton translate Semantic's size names.
That last one is defensive: unmigrated callers still pass size='tiny', and
Mantine drops a size it does not recognise without a word, so the button
quietly renders at the wrong size. Mapping here keeps the mixed state honest.
Three real bugs found and fixed on the way, none of them cosmetic:
* `fileSuffixIconName` returned fourteen icon names the map did not have, so
opening a folder with a PDF, a Word document or an archive in it logged an
error and drew a fallback glyph. All fourteen mapped, with a test that
walks the mapper's own return values, and another asserting every mapped
name resolves to a Tabler component that exists.
* `Statistic` relied on Semantic's `Statistic.Group` for its spacing, so on
the dashboard the load averages ran together into "1 MIN. LOAD5 MIN.
LOAD15 MIN. LOAD". It carries its own spacing now.
* Semantic's inverted stylesheet colors every descendant of an inverted
segment, so Toggle labels inherited #dddddd -- white pixels in night mode,
on the Settings page, which is exactly where someone adjusts their WROLPi
in the dark. Restated from tokens.
The baseline ratchet gained a third assertion: a migrated file may still lean
on Semantic's CSS classes without importing the library, which the import check
cannot see and which would collapse silently when Phase 4 deletes the
stylesheet. There is one such bridge today and it is now declared.
* Move One-Time Pad encryption to the browser; delete backend OTP module
OTP encryption/decryption is now performed entirely in the browser so
messages, keys, and generated pads never leave the originating device.
- Add app/src/components/otp.js: a JS port of the former modules/otp/lib.py
(encrypt/decrypt, message formatting, validation, and secure pad
generation via crypto.getRandomValues with rejection sampling).
- Encrypt/Decrypt now run live as the user types or pastes, with inline
error messages instead of submit buttons.
- Generate New Pad builds the printable HTML locally (blob URL) instead of
hitting /api/otp/html.
- Migrate all backend OTP tests to app/src/components/otp.test.js with
identical data.
- Delete the modules/otp backend module and its blueprint registration.
* Revoke the One-Time Pad blob URL after the new tab loads
Addresses a minor blob-handle leak: handleGenerateNewPad created an object
URL on every click but never released it.
* Let users choose the One-Time Pad character set
Add an "Advanced" accordion under the buttons with three options: A-Z,
A-Z/0-9 (default, unchanged), and a custom character set. The chosen set is
shared by encrypt, decrypt, and pad generation.
- otp.js: parameterize encrypt/decrypt/generate on the alphabet (default
OTP_CHARS for back-compat); add OTP_CHARS_ALPHA and validateCharset; input
is auto-uppercased only when the set is all-uppercase, so custom sets with
lowercase are case-sensitive; the printable pad is labeled with its set.
- Apps.js: lift charset state into OTP(); gate encrypt/decrypt/generate on a
valid set with inline validation errors.
- otp.test.js: add A-Z, custom round-trip, auto-uppercase, validateCharset,
and pad-label tests.
* Scope inverted-accordion dark background to direct children
The rule forcing #171616 on .ui.inverted.accordion .title/.content used a
descendant selector, so it leaked onto any nested element using the `content`
class — e.g. an ErrorMessage's Message.Content rendered inside SimpleAccordion
got a dark band behind its text. Scope to direct children so the accordion
still styles its own title/content but not nested components.
* Add optional error-detection checksum to the One-Time Pad
Adapts the position-weighted checksum from otp-checksum.md to the
frontend-only OTP. checksum = sum(position * value) mod N, parameterized on
the selected character set (N = chars.length), so it follows whichever pad
alphabet is chosen. Position weighting catches transposition errors.
- otp.js: add calculateChecksum/appendChecksum/verifyChecksum.
- Apps.js: opt-in "Append error-detection checksum" toggle in Advanced
(default off). When on, encrypt appends a checksum char; decrypt verifies
it, warns on mismatch, and still shows the decrypted text.
- otp.test.js: checksum unit tests, charset-variation, and end-to-end vectors.
* Show compact checksum status instead of a large warning box
Replace the bulky semantic-ui WarningMessage on decrypt with a small inline
status line: a green check + "Checksum is valid" or a red x + "Checksum is
invalid", shown whenever the checksum option is on and a message is being
decrypted.
* Fix checksum-mode decrypt truncating messages without a checksum
When the "Append error-detection checksum" option was on, Decrypt always
stripped the last ciphertext character before decrypting, so a message that
carried no trailing checksum (wrong sender setting, body pasted alone, etc.)
lost its final character and decrypted to garbage.
Add stripChecksum(received, chars), which removes the trailing character only
when it actually verifies as the checksum of the preceding characters; a
message without a valid checksum is returned intact and flagged invalid. The
decrypt UI now uses it.
Also broaden test coverage:
- stripChecksum tests, including a regression for the truncation bug.
- validation error classes (InvalidOTP/Plaintext/Ciphertext) and the
longer-than-OTP length guards.
- deterministic encrypt/decrypt roundtrip fuzz across several character sets.
- generateMessage character-count and charset-membership checks.
- end-to-end checksummed encrypt -> format -> strip -> decrypt flow.
Use a dedicated InvalidMessage error for the checksum helpers instead of the
misleading InvalidPlaintext.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Handling all combinations of toggle in light and dark.
Changing CPUUsageProgress colors to be more readable in dark mode.
Moving theme handling into Theme.js. Importing element wrappers from Theme.js to handle element theming.
Changing video menu to dark mode.
Creating ThemeContext to share Dark Mode setting.
Removing duplicate code for search functions.
Displaying X when downloads are disabled.
Using react context to share settings across components.
Refusing to start workers when WROL Mode is enabled.
Changing warning to debug.
Queuing recurring downloads first.
Adding Video.url index.
Adding special handling for youtube shorts to videos.
Reworking Downloads to have one worker per process.
Testing creating downloads and getting downloaders.
Testing video validation and poster generation. Testing video statistics.
Increasing inventory test coverage.
Adding pytest.ini to automatically detect async test.
Adding more archive test coverage.
Testing Archive CRUD.
Catching and logging when encountering error during indexing.
Using FileIcon when Archive has no screenshot.
A Video will be assigned to a Channel by its info json first, then to its directory.
Including suffix in File json.
Adding index for File.suffix.
Gathering suffix during indexing.
Passing entire file down to FileIcon.
Moving logging up.
Returning File.associated in file json.
Associated files are last in file search.
Removing duplicate refreshing code by creating new _refresh_directory function.
Changing File.idempotency to a datetime to avoid having to clear idempotency.
Truncating indexes to avoid error with large files.
Don't overwrite video data when info json is empty.
Using already_downloaded for channel videos.
Awaiting refresh in Channel.refresh.
Testing that videos already downloaded aren't downloaded again.
Adding indexes for video/archive referenced paths.
Changing grid width for mobile or computer.
Adding button to refresh specific directories.
Restoring Channel edit link.
Fixing caption path in video_modeler.
Adding ZIP files to mimetype colors.
Using model instead of first file's model.
Fetching File models in batches to avoid issuing a SELECT for every File with a model.
Flushing generated poster file.
Getting Channel in VideoPage, relying on Video.channl_id, falling back to URL's channel_id.
Attaching Video to it's Channel during download.
Using ExternalCardLink for ArchivePage singlefile link.
Validating and indexing videos after download.
Handling glob when file name contains matching characters.
Reworking and testing split_path_stem_and_suffix and glob_shared_stem.
Adding associated video files to DB during upsert.
Adding pending downloads to dashboard.
Hiding status when download is open.
Handling when load is empty.
Adding load statistics to Dashboard.
Limiting concurrent after_refresh functions.
Handling when no onSelect is declared.
Adding ImageRow for FileTable.
Moving image date into Card.Meta.
Using ExternalLink for FileCard.
Using ExternalLink for ImageCard.
Handling when error occurs during refresh of a directory. Committing during refresh to avoid losing all files if an error occurs.
Removing api health check to avoid spamming logs.
Creating ImageCard to display image posters when they aren't too large.
Removing archive.domain_id not null.
Reworking file refresh to process all files in a single directory.
Bumping version for major file rework.
No longer checking for video corruption.
Adding duration by default to speed up tests.
Using register_indexer for all indexers.
Adding CPU usage progress bar in Welcome.
Modifying TextIndexer to ignore large text files.
Counting all files during refresh.
Adding refresh_logger to clarify logging. Checking for wrol_mode before refresh functions.
Removing unused idempotency from video/channel tables.
Using magic module to speed up mimetype identification.
All tests pass.
Archive tests pass.
Changing video get endpoint to return 'file' to avoid ambiguity.
Video tests pass.
Simplifying regex filtering.
Using regex to ignore case when filtering.
Converting all channel classes to functions.
Restoring channel refresh and download buttons.
Matching format of Archive and Video pages.
Adding sorting to Archives search.
Reworking video orders for file search. Using SearchInput for videos search.
Archives can be searched on the Archives page. Domains can be filtered.
Reworking Archive search to use Files. Videos, Archives and Global search use the FilesView.
Videos are claimed by their Channel based on the Channel's directory. Prev and Next video cards are displayed on the VideoPage.
Creating useVideo for reworked VideoWrapper and VideoPage.
Adding Paginator to FilesView.
Moving file refresh button out of manage tab. Removing Files page tabs. Limiting refresh to one at a time.
Adding class for card icon to match poster height.
Video and Archives always re-claim their files on refresh.
Archive files are indexed and claimed.
FileView table is compact and striped.
Videos page uses FileView.
Updated navlinks to router v6.
Removing ! character from file names.
Disabling select button when cards are visible.
Adding show/hide button for FileTable selection.
The Archive Page is displayed.
Adding mimetype dropdown.
Upgrading to React Router v6 for useSearchParams.
Validating video during modeling. Removing screenshot during corruption check because it was very slow. Updating app to use video files as search results.
Updating video search to return Files.
Video and WROLPi tests pass.
Creating function to refresh files within a channel's directory.
Removing troubleshooting print. Updating result order in test.
Reworking video_factory to handle a specific video file location.
Returning video model on file search.
Getting session using video_file.
Screenshotting when checking for video corruption.
Reducing ffmpeg calls by gathering duration during poster generation.
Moving video file matching into model.
VideoIndexer is moved into video module. Video files are validated.
Moving files module into wrolpi in preparation to use it in modules.
Title and contents are indexed for text file.
Files are indexed on refresh by using their indexer.
Zip files are indexed using their contents.
Testing multiple zip files.
Adding unique to file.path. Checking that the correct indexer is used.
Adding index to file.modification_datetime and file.size.
Handling file info gathering in File.do_index. Only reading contents of file if file.indexed is False.
Video model is found during file refresh. A Video record is created that has the associated files.
Videos without associated files are found.
Videos are found using registered video_modeler() rather than in refresh code.
Removing old video columns.
Handling video info json files.
Trying to import each module individually. Will continue to import modules even if one fails.
Reworking refresh so it performs less DB calls.
Converting file.path to primary key.
file.path is always a pathlib.Path.
Updating video_factory to use new file system, testing it.
Bump sanic from 21.9.1 to 21.12.2 in /docker/archive
Bumps [sanic](https://github.com/sanic-org/sanic) from 21.9.1 to 21.12.2.
- [Release notes](https://github.com/sanic-org/sanic/releases)
- [Changelog](https://github.com/sanic-org/sanic/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/sanic-org/sanic/compare/v21.9.1...v21.12.2)
---
updated-dependencies:
- dependency-name: sanic
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>