Skip to content

Releases: romule-app/romule

Romule 0.4.0

Choose a tag to compare

@Devstored Devstored released this 10 Sep 13:36

The release where first start stopped being a trap, and the wizard learned to
listen. The full detail — every entry with its why — is in
CHANGELOG.md.

First start, redone

  • Trust on first use, the model Jellyfin and the *arr suite use: an
    unclaimed installation opens on its wizard, and creating the first account
    claims it. The bootstrap token is gone — under Docker it made the first
    account impossible to create.
  • A mandatory access step: open on the local network (said out loud, in the
    terminal too, at every start) or protected by accounts/SSO. romule access open|close flips it from the terminal when the interface is out of reach.
  • Two-factor sign-in with a QR code — encoder written in stdlib, verified by
    its own decoder against the published reference vector.

The console, at last

  • Pairing works from a container. The connection is checked rather than
    taken on adb's word (adb connect exits 0 on links that sit offline), a
    flat refusal right after pairing is retried once, the paired address itself
    is tried first, and the connection port is swept for outward from the
    pairing port when everything faster failed.
  • The wizard asks how the console is linked — cable or Wi-Fi — and shows that
    path alone; once connected it shows the console's own answers: name,
    battery, Android version. Adding a console opens the same connect dialog on
    a blank entry, and the console names itself on first contact.
  • The ROMs root is found, by folder names (aliases included: PS1,
    Sega) and by contents when names say nothing — which is why every platform
    now shows its game count.

Interface

  • Folder navigation is one reusable modal; platform cards carry tinted
    silhouettes; provider settings hide until their provider is chosen; an
    « À propos » tab with version, release notes, source and tools.
  • The update pill can be dismissed, server-side, until the next version.
  • ~370 sentences moved into named, accented constants; the terminal speaks
    English (ROMULE_LANG=fr opts into French) and no longer mixes languages
    mid-stream.

Nets that now bite

Every defect above got a checker or a test proven to fail without its fix:
routes vs methods, duplicate DOM ids, sentences split by emphasis, sentences
hidden in ternaries, config keys vs docs vs README counts, and a port sweep
tested with 1100 file descriptors open — the condition that had silently
disabled it.

Upgrade: docker compose pull && docker compose up -d. Configurations
from 0.3.x are read as they are; going back keeps your pairing.

Romule 0.3.1 — behind a reverse proxy

Choose a tag to compare

@Devstored Devstored released this 03 Sep 17:23

A follow-up to 0.3.0: the reverse-proxy example the documentation kept
promising, and the four defects that writing it uncovered.

Added

  • exemples/caddy/ — a complete reverse-proxy stack, with Romule not
    published on the host: it is reachable only through Caddy. CI stands the
    stack up on every change and replays the matrix that matters:

    with ROMULE_TRUSTED_PROXIES without
    a genuinely local client, through the proxy 200 403
    an outside client forging X-Forwarded-For: 127.0.0.1 403 403

    The two rows differ by one header and give opposite results. This cannot be
    proven with a double: what is being checked is what a real Caddy writes
    into the chain, and a double would only prove what it wrote itself.

  • ROMULE_TRUSTED_PROXIES accepts CIDR — 172.16.0.0/12 beside
    127.0.0.1. Exact-string matching was fine while you wrote 127.0.0.1; it
    stopped being fine the moment there was a container, because Docker assigns
    the proxy's address dynamically. The setting the documentation recommends was
    impractical in the deployment it recommends: you had to read an address after
    every docker compose up and fix it when it changed.

Fixed

  • A network notation was compared as an address. With
    ROMULE_TRUSTED_PROXIES=10.0.0.0/8, the literal string 10.0.0.0/8 was in
    the exact-match set, so a chain link equal to it counted as a declared relay.
    No exploitation is claimed — behind a real proxy the attacker's address is
    appended on the right and the walk stops there — but a notation is not an
    address, and it is fixed as the comparison bug it is.

  • The startup banner said "Network: disabled" right above the address to
    connect to.
    The line followed the lan_access setting; in a container
    Romule listens on 0.0.0.0 and protects itself with a token, so the two
    disagreed in the most common case. It follows the socket now.

  • The update notice no longer reads "Version v0.3.0 available". GitHub
    names its tags v0.3.0 and the interface already writes the word Version
    in front. The prefix is stripped for display only — the comparison never read
    it. Noticed by running the notifier against the first real release.


Docker — docker pull ghcr.io/romule-app/romule:0.3.1 (also 0.3 and latest, amd64 and arm64)

Documentation — https://romule-app.github.io/romule/ · en français

Upgrading — docker compose pull && docker compose up -d. Your state lives in the /data volume and survives.

Romule 0.3.0 — the library you handle

Choose a tag to compare

@Devstored Devstored released this 03 Sep 07:04

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 t...
Read more