Skip to content

Releases: ptitzgeg-on-git/vauxtra

v1.7.0

Choose a tag to compare

@github-actions github-actions released this 01 Oct 08:46
6bec671

Docker Image

docker pull ghcr.io/ptitzgeg-on-git/vauxtra:1.7.0

Verified pullable on both linux/amd64 and linux/arm64 before this page was created.

Changelog

Changed

  • A new visual identity. A petrol-teal palette, flat surfaces and a new logo replace the
    neon-on-black look. The README shows the interface with a banner and screenshots taken on
    a fresh demo instance.

  • Dropdowns, checkboxes and chips belong to the design. Every <select> is now a
    themed listbox that keeps the native keyboard (arrows, Home/End, type-ahead, Escape that
    closes the list without closing the dialog around it). The domain field of the service
    form suggests the known zones in the same list instead of the browser's grey <datalist>.
    Tags and environments are checkable chips.

  • One word for what the operator creates: "service". The interface said service,
    endpoint, route or host for the same thing depending on the screen. 112 strings changed in
    the eight languages, and the locale guard now pins the word in each of them. The menu
    group that holds services, tunnels and certificates is called "Publishing".

  • The installation and the add forms no longer contradict themselves. The wizard, the
    integration form and the service form were reworded step by step: buttons say what they
    do, the integration name is asked first in the guided flow, and "Settings" is no longer
    listed twice.

Fixed

  • Setting a password in the setup wizard ended the wizard. The server stops answering
    "setup required" as soon as a password is set, and the application showed the wizard only
    while it did: the recommended path landed on an empty dashboard after the second screen,
    and a reload after the first integration did the same. The wizard now marks itself started
    in the tab and stays until it is finished. Its restore screen, which asks for no typed
    confirmation, is still offered only while the server calls the instance unconfigured.

  • Every push to Nginx Proxy Manager removed a host's own certificate. The certificate was
    looked up by the service's zone, example.com, instead of its name, vault.example.com,
    so a certificate issued for the host itself -- NPM's default, one per host -- was never
    found, and the update wrote the host without one: HTTPS was gone after every push, edit or
    reconcile. The update also wrote the whole host, resetting the aliases, custom locations,
    HSTS, exploit blocking and meta the operator had set in NPM. The lookup now takes the full
    name, and the update reads the host and sends only what Vauxtra owns: the forward target,
    websockets, the service's name and a certificate that covers it. A certificate the lookup
    cannot judge is kept; one that does not cover a renamed host is removed. Zoraxy used the
    same lookup and is fixed with it. A wildcard is no longer offered for the zone's own name,
    which it does not cover.

Security

What's Changed

Other changes

Full Changelog: v1.6.1...v1.7.0

v1.6.1

Choose a tag to compare

@github-actions github-actions released this 23 Sep 10:18
2c78e38

Docker Image

docker pull ghcr.io/ptitzgeg-on-git/vauxtra:1.6.1

Verified pullable on both linux/amd64 and linux/arm64 before this page was created.

Changelog

Fixed

  • A Cloudflare DNS record came back grey, a CNAME to a tunnel included. Every write
    through the Cloudflare DNS integration took the orange cloud from the integration's
    setting, which is off by default, and every CNAME was written grey whatever it pointed at.
    A CNAME to <tunnel id>.cfargotunnel.com only answers when it is proxied. Measured on a
    production instance: a test name Vauxtra had just created toward a tunnel answered a CNAME
    and no address through two public resolvers, while the proxied record beside it, same
    target, answered two. Creating, renaming, re-enabling or correcting the drift of such a
    name cut it off. A proxied A record whose address changed came back grey without a word,
    and the origin's address went into public DNS: the scheduler's address update rewrites a
    record in place, the push corrects a drift by removing it and adding it back, a rename
    creates the new name, and each wrote the default over the operator's choice. A record
    already there now keeps its flag when its address changes, a rename carries the old name's
    flag, and a record the push removes and adds back gets its own flag back. A CNAME to a
    tunnel is created proxied, and one found grey is turned orange. Any other CNAME is still
    created grey, since a proxied one pointing at a name on another Cloudflare account answers
    error 1014, and for the same reason an address's flag never crosses to a CNAME. The
    Cloudflare Tunnel integration writes its own CNAME proxied and was not affected. Known
    limit: disabling a service and enabling it again removes the record in one request and
    creates it in another, hours or days apart, and nothing keeps the flag in between, so a
    proxied A record comes back with the integration's default. A CNAME to a tunnel comes back
    proxied.

  • After an upgrade, the browser could show the previous build. index.html names the
    hashed bundles of its build under a name that never changes, and it was sent with a
    Last-Modified and no Cache-Control, so a browser could reuse its copy without asking
    for a fraction of its age (heuristic freshness). Measured on a production instance: a page
    loaded just after an upgrade came out of the browser's cache with the previous build's
    bundle while /api/health already answered the new version, and the screen showed a
    defect the running version had fixed. Every file of that route, the page and the icons at
    the root of the build, is now sent with Cache-Control: no-cache: the browser keeps its
    copy and asks again before using it. The hashed bundles under /assets do not go through
    that route and are unchanged.

  • Every move to another proxy came back with an error about the host it had just
    removed.
    A regression of 1.6.0. The edit deleted the previous proxy host in its proxy
    block, then again with the former targets, whose failures 1.6.0 had just started to put in
    errors. NPM answers the second deletion of an id with a 404, so the save reported
    "Former target: Failed to delete proxy host" about a host that was gone. The previous host
    is now deleted once, with the former targets. Found by reading the code and reproduced by
    tests.

  • An edit that moved a service did not say when the previous route stayed up. Moving a
    service to another mode, integration, name or tunnel publishes the new route, then
    withdraws the old one. Those withdrawals dropped the provider's answer, and an exception
    reached the journal only: a provider that refused left the old route serving the name
    behind a 200 with an empty errors, while the screen showed the service moved. One of
    them already read the answer, the tunnel route of a service disabled in the same request
    that changed its name or its tunnel. A bulk enable had the mirror defect: it wrote "DNS
    re-added" to the journal whatever the DNS server answered. A refused withdrawal is now
    reported, in the response and in the journal, once it has been read against the
    provider's listing: NPM answers a 404 for an id it no longer holds and the Cloudflare DNS
    integration answers false when nothing matches, and neither is a route left behind. A
    listing that cannot be read does not clear the refusal. Cloudflare Tunnel is read against
    nothing: its ingress half answers true for a rule already gone, so a false from it is
    always a failure, possibly of its DNS half, which the rule listing does not show. A
    read-only integration is no longer called, and a proxy that files its hosts by name is
    asked about the service's name rather than what an earlier rename left in the database. A
    bulk enable reads the DNS server's answer and reports a refusal, and an exception raised
    while an edit switches a proxy host on or off lands in errors, not only in the journal.
    A disabled service no longer reports, on every edit and on its deletion, a DNS record its
    disable had already withdrawn. Every provider exception that reaches errors is masked,
    as the journal already was. No integration shipped today lets a credential through there:
    the two that authenticate in the query string catch their requests errors on writes and
    mask their listing refusals, and the masking closes the path for the next one. Found by
    reading the code and reproduced by tests, not observed on a running instance. Known
    limits: when the DNS server and the address change together, one refused record can be
    reported twice, by the withdrawal from the old server and by the one from the former
    targets; and deleting a service does not yet read its proxy's listing, so a host removed
    by hand is reported as a failure.

What's Changed

Other changes

Full Changelog: v1.6.0...v1.6.1

v1.6.0

Choose a tag to compare

@github-actions github-actions released this 23 Sep 08:04
9586cd3

Docker Image

docker pull ghcr.io/ptitzgeg-on-git/vauxtra:1.6.0

Verified pullable on both linux/amd64 and linux/arm64 before this page was created.

Changelog

Fixed

  • The provider scan offered zones nobody had declared, and never said which rows sat in
    them.
    Measured on a production instance with one DNS domain declared: a first scan listed
    91 routes, 59 of them in twelve zones the operator had never declared, behind a "Quick
    Import (91 new)" button that writes them all after a single confirmation. A second scan,
    minutes later, with 32 services imported in between, listed 32. That looked like a
    declared-domain filter that only engaged once a service existed. There was no such filter
    anywhere: the scan returned whatever each integration could read. The instance's journal,
    read afterwards, rules out a failed listing: none failed on either pass, no integration was
    edited in between, and the Zone ID field was empty. By elimination, what changed is what
    the token could read: its zone resources were narrowed on the Cloudflare side between the
    two scans, and it now reads the declared zone only. That part is not a code defect. What
    the code did get wrong is that its answer could show none of it. No row said which zone it
    belonged to or whether that zone was declared, integrations were read in no set order, and
    an integration whose listing raised was written to the journal and nowhere else, so a
    failed listing would have looked the same: fewer routes, without a word. The scan now files
    each row under its zone (the longest declared domain it sits under, then the zone the
    provider reports, then its first dot), says whether that zone is declared, returns the
    declared domains and one line per integration with how many entries it listed or why it
    could not, and reads the integrations in the order they were added. Settings > Data opens
    on the declared domains, says how many routes that hides, marks an undeclared zone on its
    row, filters by zone, and names an integration it could not read, with a link to the
    integrations page.

  • The import split a name at its first dot. nas.maison.example.org became nas under
    maison.example.org, whatever zone it came from. The check for a service already tracked
    under that name used the same split, so a service stored as nas.maison under
    example.org was not found: the import created it a second time and declared
    maison.example.org as a domain on the way. The import now splits at the zone the scan
    filed the row under, finds a tracked service by its whole name and by its tunnel hostname,
    and refuses the apex of a zone it knows, declared or reported by the integration, with the
    reason on the row, as it already refused a name with no dot. The apex used to go through
    the same split as any name: example.net became a service example under a domain net,
    declared on the way. A name whose zone neither the declared domains nor the integration
    give still falls back to its first dot, apex included.

  • Linking a DNS record overwrote the DNS half a service already had. When a scanned
    record matched a tracked service, the import set the service's DNS integration and address
    to the record's, whatever the service already had: a service published through one DNS
    integration was moved to another, and a tunnel service was given a DNS integration it does
    not use. A link now fills only the half that is missing, and a service that already has a
    DNS integration, or is published through a tunnel, is set aside with the reason. When two
    integrations answer for one name, the message says the other record is left as it is and
    not tracked: expected with split-horizon DNS, a stale record otherwise.

  • "Quick Import" sent the whole scan back. Rows already tracked and rows in zones nobody
    had declared went with it, and its confirmation counted them without a word about zones.
    A tracked row sent back is what let a DNS record re-link a service, as described above. It
    now sends the importable rows on screen and nothing else, and of a row that links a tracked
    service, only its DNS record. Its confirmation says how many tracked services will be
    linked, how many routes sit in zones not declared, how many names several DNS records
    answer for, how many use a local TLD, and how many importable routes the filters leave
    out. Both import confirmations now open on Cancel. The setup wizard's import step shows
    one row per name and, since importing a service declares its domain, names the domains an
    import is about to declare, above the button that does it.

  • An Enter held down could answer a confirmation. A dialog of the info variant opens
    with the focus on its confirm button, and a browser keeps firing an Enter that is held
    down: the Enter that asked the question came back as a repeat on the confirm button and
    answered yes. A repeat is now ignored inside the dialog, on its buttons and in its typed
    confirmation alike, and a caller can ask for the focus to open on Cancel (initialFocus).
    The report of an import confirmed without its button being clicked was not reproduced, and
    this is one way it can happen, not a proof that it did. The other ways the report suggested
    a question could end are now pinned by tests: the focus leaving the dialog or the window
    answers nothing, the page that asked going away answers no, and a second question answers
    the first one no.

  • "Test all" on the Integrations page could sit through a whole health round before testing
    anything.
    It refetched four queries before sending a single test, one of them the health
    round that tested every integration one after the other. A refetch joins a first load still
    in flight, and cancels and restarts any later round, so the click waited for a whole round
    either way, the button busy and disabled all along. That explains a click that sent no test
    for a while, not by itself the report's three clicks that sent no request at all: only a
    refresh already running before them would silence those, and nothing in the report says one
    was. It was not reproduced against the instance the report measured. The tests now go out
    at once: each card spins until its own answer comes back, the button counts them, and with
    no integration enabled the page says there is nothing to test instead of reporting a
    refresh. The health round itself now tests up to eight integrations at a time.

  • "Scan providers" said nothing about the domains it files names under. The report
    measured a button that sent no request until a DNS domain was declared. That was not
    reproduced: with no domain declared the button sends its request, and no code path holds it
    back. What was missing is the reason a declared domain matters. The panel now says, before
    the scan, that it files names under the declared domains and that none is declared, with a
    link to Settings > DNS Domains; it does not claim so while the list of domains could not be
    read, and the declared-only filter stands aside while there is nothing to filter on.

  • The provider wizard called an empty form complete, and asked for the name last. "Name
    the integration" folded the guided steps away and showed "Everything is filled in. Validate
    the connection to continue." above an empty address, username and password. The name is
    now asked first, in every mode. "Next" waits for the required fields of its step, the step
    dots cannot jump past the first step still missing something, and the last step lists what
    is missing, each field asked on an earlier step linking back to it, and says everything is
    filled in only once it is. Behind the banner, the forms disagreed on what is required: the
    guided steps marked the NPM e-mail required, the expert form of the same dialog called it
    optional, the dialog's "Next" and step dots checked nothing, and the e-mail could be left
    blank all the way to a failed login. One rule now serves both forms and the setup wizard: a
    field is required when the local rules say so, or when a guided step asks for it without
    marking it optional. That adds the username of NPM, AdGuard Home and Technitium, whose
    login cannot go without one, and an edit is held to it too: only the secret may stay blank
    there, to keep the stored one. An optional field is marked once, where five labels also
    said "(optional)". In the setup wizard, picking another type gives the form that type's
    name and an empty address instead of the previous type's, a connection verdict is dropped
    by the next edit so "Connect" never answers for values nobody tested, and a hosted API's
    address field now shows in the expert form, optional, saying what a blank one does.

  • A DNS record with no port was probed on port 80 and reported down. The import gave a
    DNS record port 80, so a name published in DNS alone, an A record for a UDP service or a
    CNAME to a tunnel, was probed over TCP on a port nothing listens on and turned red: on the
    instance measured, two of the three services reported down were exactly that. A service's
    port can now be 0, meaning none. The import writes 0 for a DNS record, the "DNS only" form
    accepts an empty port and no longer refills it with 80 when the field loses focus, and
    nothing probes such a service: not the scheduler, not "Check all services", which counts
    it apart from the tunnels in its summary, and not "Check now", which still resolves the
    name and says there is no port to test. The server refuses 0 where a proxy or a tunnel
    forwards to the port, and saving a service without a port resets its status to unknown.
    Monitoring writes "No port: not testable" on its row and explains it in the drawer, and the
    latency card counts only the latencies actually measured, where it used to read "Measured
    on 1 service this session" under a dash after a check that measured nothing. A service
    impor...

Read more

v1.5.2

Choose a tag to compare

@github-actions github-actions released this 22 Sep 08:19
9f89b77

Docker Image

docker pull ghcr.io/ptitzgeg-on-git/vauxtra:1.5.2

Verified pullable on both linux/amd64 and linux/arm64 before this page was created.

Changelog

Fixed

  • A diagnostic check with no detail announced itself with a generic word. When a
    provider validation check returned no detail sentence, both screens that show it fell back
    to "Verification" — the same word for every check in the list. The name of the check was
    in hand the whole time and was never used. The check names are now translated in all eight
    locales, and a folding helper resolves them, so the fallback reads "Token verification"
    where it used to read "Check". Counted, not assumed: the providers emit 29 check names,
    which fold to 28 keys because dns_write and DNS write are the same check spelled two
    ways, and all 28 exist in every locale, with no key left over for a check nobody emits. A
    name this build does not know — what an API newer than the build sends — shows through as
    itself, which tells a reader more than a generic word would. The generic word is what
    remains for a check carrying no name at all.

  • The log footer counted rows the screen was not showing. The level filter is applied by
    the server and the text search only sifts the page already in hand, and both fed the same
    footer sentence. The count shown was therefore the server's, never the screen's. The footer
    now tells the two cases apart and says how many rows match when a text search narrows the
    view; the empty state no longer blames the API on a branch only reached after a request
    succeeded, and reads "no filter in effect" instead. The counted key the sentence needs,
    settings.logs.total_matching, did not exist and arrives here in all eight locales, with
    the plural categories each language actually has.

  • French set a breakable space before its punctuation. French puts a no-break space
    before ? ! ; and around its quotation marks, and the browser is the reason: a plain space
    there lets the ? wrap onto a line of its own, under the sentence it belongs to. A narrow
    no-break space was substituted for one breakable space in 221 values — one of which also
    had a word corrected — and folding both files back to plain spaces returns them byte for
    byte identical, which is the proof that only spaces moved. The pass was applied by a script
    that masks {placeholder} tokens and technical runs such as host:8443 and http://…
    before it matches. That mask now lives in scripts/locale-typography.mjs, out of the guard
    so it can be imported at all, and is pinned by twelve witnesses: five sites it must take,
    five it must refuse, two on the mask itself. Each escape hatch was watched going red with
    the hatch removed. Measured while writing them: not one French value in the file needs that
    mask today. It is there for the sentence written next month, which will carry a port or a
    placeholder.

  • A log level badge read "D'accord". The severity labels on a log line had been
    translated as ordinary words instead of as levels: OK read "D'accord" in French, "bien"
    in Spanish, "Okay" in German and "Oke" in Dutch, and the Spanish error level read "error"
    in lower case while every other level was capitalised. These are badges on a journal entry,
    not an answer to a question. All four now read OK, which is what the level is called in
    each of those languages, and the Spanish error level matches the case of its neighbours.

  • French settings pages said "providers". Eight French values had kept the English word
    where the rest of the interface says "fournisseurs": the migration panel, the webhook scope
    picker and the credentials export help among them. A ninth, the journal's empty state, had
    lost the accents from "deconnectee". All nine now read as French.

  • The container could still regain a privilege it did not start with. no-new-privileges
    had been left off both Vauxtra services by reflex, because the entrypoint starts as root.
    That is the wrong reflex: the flag forbids REGAINING a privilege, not dropping one, and
    what the entrypoint does is the drop. Measured on the image with the flag set: groupadd,
    usermod, chown -R and gosu appuser all four go through, and the Docker socket's
    secondary group arrives on the other side. Deliberately no further: read_only: true and
    cap_drop: ALL both break this entrypoint, which writes /etc/group and /etc/passwd and
    chowns /app/data. That negative knowledge is written beside the flag, so the next reader
    does not set them and find the breakage at the following restart.

Added

  • The locale guard now refuses French written without its accents. The list is narrow on
    purpose: a word only enters it when its unaccented spelling is never correct French. That
    is why a, ou, sur and expose are absent — they are real words, and listing them
    would make the guard noisy enough to be switched off. It reads \p{L} rather than \w,
    because JavaScript's \w stays ASCII even under /u, which would cut "Parametres" into
    "Param" and "tres", and "tres" is on the list.

  • The locale guard now refuses a breakable space before French punctuation. It runs on
    fr.json alone, and that is the point: this is a French typographic rule, and applying it
    to the other seven locales would flag correct text. The message names the punctuation whose
    space is wrong rather than a column number, so it says what to repair. scripts/ carried no
    test at all before this; vitest now collects scripts/**/*.test.mjs as well.

  • The locale guard now pins three words that are only wrong in one language. Prestations
    is what a caterer sells, Balises are the tags in a markup document, Dienstleistungen is
    the commercial sense of "services", and Provider was English sitting in a Chinese file.
    Each was the only string in its file using that word while the rest of the file said
    otherwise. The per-locale table and the global one are concatenated rather than spread: both
    are keyed by locale key and hold a list of words, so { ...global, ...perLocale } would let
    a per-locale list REPLACE the global one for that key instead of adding to it, and the
    global ban would disappear without a word. No key sits in both maps today; that line is what
    keeps it harmless when one does.

  • Four assertions on the sentence the expose assistant prints at the end. CI already
    prevents the cause — a translation call that fills none of its parameters fails the build
    — but nothing looked at the render. Putting a spare <span>{host}</span> back printed the
    host twice on screen with every guard green. The four checks say: the host is written once,
    there is a single monospace region whose text is exactly the host, the translator's word
    order is honoured when the parameter moves, and the case of a translation that has lost its
    parameter, described as dev behaves. The negative witness was played: with the defect put
    back, the four new tests fall and the nine existing ones stay green.

What's Changed

Other changes

Full Changelog: v1.5.1...v1.5.2

v1.5.1

Choose a tag to compare

@github-actions github-actions released this 20 Sep 12:06
8750b81

Docker Image

docker pull ghcr.io/ptitzgeg-on-git/vauxtra:1.5.1

Verified pullable on both linux/amd64 and linux/arm64 before this page was created.

Changelog

Fixed

  • Two different schemas declared themselves under the same number. 1.5.0 added an
    eighteenth migration, service_templates.environment_ids_json, without touching
    SCHEMA_VERSION, although the comment above the constant asks for exactly that.
    Measured on the production instance right after the upgrade: settings.schema_version
    read 11 and the column was there. A 1.4.0 database and a 1.5.0 database therefore
    answered the same 11, and the one marker that tells two schemas apart after the fact
    told nothing apart. The constant is now 12, and the guard meant to make that omission
    visible no longer forbids it: it froze assertEqual(11, SCHEMA_VERSION), so bumping
    the constant broke the suite. It checks the pair (number of migrations, version) now,
    and says what to do when it falls.

  • A real HIGH finding was reported as a scanner that had not run. The grype exit
    codes in the security workflow were read backwards. 2 is
    ErrAboveSeverityThreshold, which is the tool succeeding and finding something; 1 is
    the tool failing. So a genuine finding came out as a broken run, and a genuine crash
    came out as a clean report. The codes are now taken from the source.

  • The release tag was validated by a glob. v[0-9]*.[0-9]*.[0-9]* accepts
    v1.5.0-rc1 and v1.5.0+meta, which the error message directly below it promises to
    refuse. And because the shell's * matches a newline, a two-line value passed too:
    the two echo lines that follow write to $GITHUB_OUTPUT, which GitHub reads line by
    line, so the second line published an output nobody wrote. version is precisely the
    string the release page hands the reader to paste into docker pull. An anchored
    regular expression refuses all three shapes.

  • The release wait could not reach its own deadline. The job ceiling was 20 minutes
    for a wait that can legitimately last 30, so the two ::error:: lines that name the
    missing manifest were unreachable code. The ceiling is 40 now, and the wait reads its
    budget from PULL_WAIT_MINUTES, written once.

  • Two publication runs that overwrite each other were filed apart, then all of them
    were filed together.
    The concurrency group ended in github.ref, which put the two
    runs that collide into different groups: pushing main and then tagging that same
    commit is two refs and one commit, and both write latest. Measured on v1.5.0: four
    minutes of overlap, and it was the second to finish that placed latest. The first
    repair put every publication into one group, which was too wide: GitHub cancels a
    group's queued run as soon as a newer one queues behind it, whatever
    cancel-in-progress says, so a push to dev arriving while a release build waited
    killed that build, and the release then spent its whole wait on an image nobody was
    building. main and a release tag now share one queue; dev, which moves only dev,
    has its own.

  • The sha- tag was written from every ref, although the images differ. APP_VERSION
    is latest, dev or the version number depending on the ref, so the second run
    repointed the crumb and left the first image nameless, handed to the weekly cleanup's
    delete-untagged along with its signature and its attestation. It is written from
    main only.

  • The GHCR cleanup never verified dev. It protects the tag from deletion, because a
    staging instance pulls it on every restart, and then asked "does this tag still
    resolve?" of latest and the version numbers only. The one moving tag with a machine
    behind it was the one nobody checked. The contract holds 13 tags now instead of 12.

  • Two progress logs never arrived in time. The wait for the image, up to thirty
    minutes, and the walk of the registry, some fifty manifests, run under python3 -u
    now. Python buffers its output whenever it is not a terminal, and under Actions it
    never is: measured here, three lines written over 1.2 s arrive at the far end of the
    pipe within 91 ms, all at process exit. The log worth reading is the one from a run cut
    short by its own ceiling, and that was exactly the run that wrote nothing.

  • The hygiene gate swallowed the files it could not read. Its conclusion is about the
    whole repository; a file it failed to decode is a file it did not look at, and
    Repo hygiene check passed came out with the same confidence. Such a file is named
    now. Measured: all 400 tracked files decode today, which is the reason to close the
    hole today rather than the reason not to.

  • The version gate read two declarations out of four. It pinned
    vauxtra_mcp/__init__.py against frontend/package.json and against what the MCP
    handshake actually answers, and stopped there. The release number is also written twice
    in frontend/package-lock.json and once at the top of this file, and neither was
    watched. Both were found by grep while cutting this very release, not by a failing
    test, which is the defect family that gate exists to catch. A stale lock does not build
    a wrong image, it fails npm ci with a message about being out of sync, a long way
    from the character that needs changing; a stale changelog publishes a release page for
    a build nobody made. Three tests now read the lock by key, the newest released heading,
    and the comparison link that turns that heading into something clickable. Reading by
    key is not a style choice: css.escape in that lock is at 1.5.1 and lz-string at
    1.5.0, so a text search for either number finds theirs first. CONTRIBUTING.md names
    the four files too: its release instructions went straight from "when dev has proven
    itself" to "tag main", with the cut that has to happen in between written down
    nowhere.

Removed

  • The UNRECOVERABLE exclusion set in the GHCR cleanup. Its six tags have been taken out
    of the registry, read anonymously on 2026-09-20: not one of the six is there, so it
    excluded nothing and its ::notice:: no longer printed. A silent exception list that
    matches nothing is worse than none, because it would wave through the next breakage
    that reused one of those names.

Changed

  • Dependencies: uvicorn[standard] >= 0.53.0, apprise >= 1.13.1, pytest >= 9.1.1
    for development, four GitHub Actions and fifteen npm packages in the minor group.

What's Changed

Other changes

Full Changelog: v1.5.0...v1.5.1

v1.5.0

Choose a tag to compare

@github-actions github-actions released this 20 Sep 06:41

Docker Image

docker pull ghcr.io/ptitzgeg-on-git/vauxtra:1.5.0

Verified pullable on both linux/amd64 and linux/arm64 before this page was created.

Changelog

This version's changelog section is 287183 characters, more than a release page can hold. Read it in full in CHANGELOG.md.

What's Changed

Other changes

Full Changelog: v1.4.0...v1.5.0

v1.4.0

Choose a tag to compare

@github-actions github-actions released this 11 Sep 08:28

Docker Image

docker pull ghcr.io/ptitzgeg-on-git/vauxtra:1.4.0

Full Changelog: v1.3.0...v1.4.0

v1.0.0

Choose a tag to compare

@ptitzgeg-on-git ptitzgeg-on-git released this 11 Sep 13:43

ℹ️ This version was never shipped. The tag was created on 2026-09-11 so the numbering has
no hole in it, and it points at the same commit as v1.0.1.

PR #20 was squashed on merge, so app/security.py, app/cache.py, app/errors.py and the
operator guides all reached main in a single tree, which went out as 1.0.1 two days later.
There is no state of the repository that corresponds to 1.0.0 alone, and no 1.0.0 image was
ever built or pushed.

Use v1.0.1 if you are looking for this code. Its release notes explain why its image cannot
be pulled either.

What was in it

Added

  • app/security.py — CORS origin validation, domain sanitization, password strength enforcement
  • app/cache.py — request-scoped caching with TTL expiration, removing N+1 provider calls
  • app/errors.py — unified error handling with standardized codes across every endpoint
  • Operator documentation: docs/HOWTO.md, docs/DEPLOYMENT.md, docs/TROUBLESHOOTING.md

Changed

  • CORS validation: strict origin checking, no wildcards, port and scheme enforcement
  • Error responses: standardized codes and shapes across all API endpoints
  • app/main.py — CORS validation and request cache middleware

Fixed

  • 8 security issues (CORS, domain injection, password policy, session handling)

Docker Image

None. This number was never built. The nearest published image is 1.1.0, which is the oldest
one that still pulls:

docker pull ghcr.io/ptitzgeg-on-git/vauxtra:1.1.0

Full Changelog: v0.1.0...v1.0.0

v1.3.0

Choose a tag to compare

@github-actions github-actions released this 10 Sep 22:05

Docker Image

docker pull ghcr.io/ptitzgeg-on-git/vauxtra:1.3.0

What's Changed

Full Changelog: v1.2.0...v1.3.0

v1.2.0

Choose a tag to compare

@github-actions github-actions released this 10 Sep 19:05

Docker Image

docker pull ghcr.io/ptitzgeg-on-git/vauxtra:1.2.0

What's Changed

Full Changelog: v1.1.0...v1.2.0