Skip to content

Romule 0.3.0 — the library you handle

Choose a tag to compare

@Devstored Devstored released this 03 Sep 07:04
· 151 commits to main since this release

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.
    setSystem used 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_groupes says 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 of urllib, 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
    doctor output.
  • 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, so docker logs romule showed almost nothing. It now
    writes to all three. debug adds 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/job in 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 passwd resets 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-expanded carries
    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 on ghcr.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 pathlib in 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
    with os.walk and strings, /api/scan went 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.
    A nlib or 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).3ds has europe and 3ds among 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 its display:flex rule 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 passwd printed
    "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 no nsz.
  • The documentation home page rendered its own markdown as text. The card
    grid is written inside a <div>, and without the md_in_html extension
    everything in an HTML block is left verbatim. --strict cannot 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 on docs/**: adding
    a setting without touching the docs never ran them. They moved to the
    unfiltered CI job, and lancer_tests.py now 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.md said 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-title pointing 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.