Repository navigation
Releases: romule-app/romule
Release list
Romule 0.4.0
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|closeflips 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 connectexits 0 on links that sitoffline), 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=fropts 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
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_PROXIESwithout a genuinely local client, through the proxy 200 403 an outside client forging X-Forwarded-For: 127.0.0.1403 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_PROXIESaccepts CIDR —172.16.0.0/12beside
127.0.0.1. Exact-string matching was fine while you wrote127.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
everydocker compose upand 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 string10.0.0.0/8was 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 thelan_accesssetting; in a container
Romule listens on0.0.0.0and 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 tagsv0.3.0and 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
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 t...