Repository navigation
Romule 0.3.0 — the library you handle
The release about the parts you touch. 0.2.0 made Romule publishable — public
repository, frozen API, strict CSP. This one is about the library itself: how
fast it answers, how it behaves on a phone and on a handheld, who is allowed to
do what, and what happens when something goes wrong at three in the morning
with no browser in reach.
Added
- The library opens on every platform at once, and switching is instant.
setSystemused to empty the grid then wait for a round trip: the content
collapsed, the page jumped back to the top, and everything reappeared. The
current view now holds until its replacement arrives, and each platform is
kept in memory — going back is free. The cache is dropped on the only three
events that change the inventory: a task ending, Refresh, and a file drop.
A cache you cannot invalidate is a display bug on a timer. - Search that keeps up with typing. Measured before, on 5 000 titles: 20.4 ms
per keystroke, above the frame budget — the field felt sticky. The unified
list only changes when the data, the platform or the sort order change, so it
is memoised on exactly those; typing no longer rebuilds and re-sorts the whole
library. The field is sticky, answers/and⌘K, and searches all
platforms in the all-platforms view. - Saved filter views, kept on the server. Search, status chip and advanced
filters combine, and the combination can be named and found again — on your
phone as on your desk. Sort order and tile size stay in the browser: those
belong to the screen in front of you, not to the set of games. - One click clears every filter. Three mechanisms filtered the library and
nothing said how many were active; you could hunt for a while wondering why
the grid was empty because of a chip set the day before. The button now
carries the count of all three, and Clear all appears only when there is
something to clear. - Two roles, SSO included. The model existed by halves: the first account
was administrator and 27 routes were reserved server-side, but the interface
hid nothing, and an SSO session could never administer.oidc_groupes
says who may enter;oidc_admin_groupessays who may administer. Empty means
nobody — an empty setting must never mean everyone. - It behaves like an application, not a web page.
100dvh,
overscroll-behavior, no tap highlight, safe-area insets, no long-press
selection on chrome. The three gestures that gave away the web page are gone. - The D-pad walks the grid. Retro handhelds are Android devices with a
browser, a short screen and buttons rather than a precise finger. Arrow keys
move card to card, Enter opens, Escape closes, and the focus ring is finally
visible. - Ten device profiles in the responsive audit, five of them handhelds:
Anbernic RG35XX, Retroid Pocket 5, AYN Odin 2 / Thor, the Thor's second
screen (360 × 413 — the only viewport taller than wide), and a Steam Deck. - An update notice. A quiet pill in the header when a newer version exists,
opening the release notes. It is an invitation, not an alarm: it appears only
when there is something to read, blocks nothing, and can be ignored for
weeks. One request to GitHub a day, and a setting to switch it off — the only
time Romule reaches the internet unasked. - Notifications to Discord, Slack, Telegram, ntfy, Gotify — or any webhook.
Paste an address in Settings → Access; the service is worked out from it.
Comparable tools reach for Apprise, which is a dependency; Romule has none,
so the five families that cover most self-hosted setups are implemented in
about a hundred lines ofurllib, plus a generic webhook for the rest.
The address is a bearer secret and is never sent back — the interface
shows only the host, and it appears in no log, no API response and no
doctoroutput. ROMULE_LOG— five terminal log styles.quiet,normal,verbose,
debug,json.JobRunner.log()wrote to a file and an in-memory buffer
and never to stdout, sodocker logs romuleshowed almost nothing. It now
writes to all three.debugadds each HTTP request with its status and
duration, the module, the thread and seconds since startup;verbose
deliberately stops short of it, because the interface polls/api/jobin a
loop and those lines would bury what you came to read.- A startup banner naming the service and the facts you would otherwise
spend half an hour looking for: version and Python, where settings live as
opposed to games, who may connect, external tools found, the log file path. - Terminal commands for when the interface is not the answer —
romule doctor,romule user list|passwd|admin|totp-off|rm,
romule config list|get|set.user passwdresets a password without the old
one, invalidates that account's sessions and clears the lockout counter. They
exist only on the command line: whoever can run them already has the
service's files, so they grant nothing new — they only make it doable without
mistakes. - Add a platform Romule does not know. Twenty-three are recognised out of
the box; a display name, a folder and a list of extensions adds any other.
The feature existed and a button carried it — nothing said so. - A bilingual documentation site, twelve pages in English and French, with
CI checks that the settings reference, the environment variables, the OpenAPI
specification, the numbers cited in prose and the two translation catalogues
all still match the code.
Changed
- Plurals are real plurals. 98 occurrences of
file(s)— the most visible
flaw in the interface, and on nearly every screen. A single rule would have
traded one mistake for another: French puts 0 and 1 in the singular, English
only 1. The template carries both forms,{singular|plural}, and the
language picks — one catalogue key per sentence, and a translator writes the
two forms of their language without needing to know French. - Undo instead of confirm. The trash is the undo. Asking "are you sure?"
charged the price of a mistake that costs nothing. The action happens, and
the toast offers Undo for eight seconds. What genuinely cannot be undone —
emptying the trash, clearing the log, revoking a key — still asks first. - On a phone, the content comes before the settings. Nineteen controls used
to sit between the top of the screen and the first cover. Filter and sort
rows fold behind one button below 700 px; search stays reachable. - The library toolbar is aligned, and measured. Three vertical axes in the
same row — 245, 249 and 250 px — and three heights.aria-expandedcarries
the state and the CSS reads that, so there is one source of truth rather
than a class to keep in step with what screen readers announce. - Installing no longer starts with
git clone. The container image is
public onghcr.io/romule-app/romule—latest,0.3,0.3.0, multi-arch,
no authentication — so the recommended path is a Compose file you paste, with
one line to change. - IGDB is now a second source for cover art. SteamGridDB is a community
artwork database: rich on what gets played with a keyboard, thin on
handheld console catalogues. It does not have Crazy Construction — a real
3DS game — while IGDB does, with its cover. Romule already queried IGDB for
summaries and simply never asked it for images. - The library scan no longer uses
pathlibin its hot loop. Answering
"should this be in SQLite?" honestly meant profiling it, and the profile
said the bottleneck was never storage: on 20 000 titles,Path.relative_to
was 39 % of the time and JSON serialisation did not appear at all. Rewritten
withos.walkand strings,/api/scanwent from 1 759 ms to 1 170 ms
and startup from 949 ms to 457 ms. The rewrite was checked by replaying
the old loop verbatim and comparing every field of 4 433 entries: zero
differences. Written up in
Storage and performance.
Fixed
- A wrong entry is worse than a missing one.
covers.sgdb_infos()took
found["data"][0]— SteamGridDB's first autocomplete result, unchecked. On
Crazy Construction it returns a game called Crazy, and since that title
is then the pivot for the IGDB lookup, the card inherited both the name and
the summary of a different game. One unchecked[0]produced both defects.
Both sources now go through the same rule: a candidate must cover two thirds
of the distinctive words of the title. - A cover source is consulted when there is no image, not when there is no
address. Anlibor SteamGridDB URL that answers 404 is still a URL; the
fallback now runs after every candidate has been downloaded and rejected. - The title, not the file name, is sent to IGDB.
Crazy Construction (Europe) (En,Fr,De).3dshaseuropeand3dsamong its distinctive words,
so the matching rule rightly rejected the correct game. The fallback
searched, found, and refused. - Two stray braces disabled half the stylesheet. A lone
}raises nothing
visible: the browser resynchronises and silently drops everything up to the
next safe point. The settings bar rendered as a vertical list on a 1500 px
screen because itsdisplay:flexrule was simply no longer applied. Nothing
caught it — not the tests, not CI, not me. I saw it by looking at a
screenshot. - Commands exited 0 when they refused.
romule user passwdprinted
"Refused: ..." and reported success, so a script could not tell a refusal
from a completed job. Found by the new tests — six perfectly worded refusals,
all announced as successes. - Startup notices went to stdout.
nsz absent — ...was printed before
every command, including ones whose output is meant to be read by a program:
VALUE=$(romule config get trash_days)captured the notice along with the
value. They go to stderr now. Found by CI, on a machine that has nonsz. - The documentation home page rendered its own markdown as text. The card
grid is written inside a<div>, and without themd_in_htmlextension
everything in an HTML block is left verbatim.--strictcannot see it: it is
not a MkDocs warning, it is a page that reads badly. - Numbers cited in the documentation are checked against the code. The
README claimed 37 settings when there were 41, and the roles page listed five
families of admin-only routes when there are seven. - Two checks that guard against drift could not see it. The settings
reference and numbers checks lived in workflows filtered ondocs/**: adding
a setting without touching the docs never ran them. They moved to the
unfiltered CI job, andlancer_tests.pynow runs them locally too. - The access token and the activity log were documented in the wrong
folder — they live in the service data folder, not next to your games. beta.mdsaid the audit report and login pages were "English-only" two
lines above stating they are in French. They are in French, and so is the
command line.- The contributing page still taught
'%d game(s) found', the notation
this release replaced with{singular|plural}. - Anchors are validated at build time. A renamed heading used to leave
page.md#old-titlepointing nowhere, silently.
Docker — docker pull ghcr.io/romule-app/romule:0.3.0 (also 0.3 and latest, amd64 and arm64)
Documentation — https://romule-app.github.io/romule/ · en français
Upgrading — your state lives in the /data volume and survives. docker compose pull && docker compose up -d.