Skip to content

v0.9.5

Choose a tag to compare

@github-actions github-actions released this 16 Sep 16:43
· 91 commits to main since this release

There is no 0.9.4. The patch moved two, from 0.9.3, so that
stellar-compose can pin stellar-api:0.9.5 and stellar-ui:0.9.5 together and
tag that commit as a stack version — compose tags only commits where both pins
name the same release, and leaves an unequal pair untagged rather than giving it
an invented number. Per ADR-0004
the patch digit is this repo's own cadence, so the skip costs nothing here; the
next UI-only release is 0.9.6 against the same 0.9.5 contract.

Added

  • A release group panel on the release page
    (#317) — "this album,
    everywhere you can reach it": the group's canonical title and year, and the
    member releases in other communities, each linking to its own release page.
    The release being viewed is marked, not hidden, so the panel reads as the
    whole album rather than a list of elsewhere.

    Identity and membership arrive separately, and the split is an access boundary
    rather than a fetching convenience. release.group inlines with the release
    read — seeing a release entitles you to its group's identity — so the header
    renders with no request. The sibling list stays behind
    GET /release-groups/{id}, which returns only the members this viewer may
    see
    . That list can be shorter than the album really is and can differ
    between two viewers on the same page; both are correct, not gaps.

    The endpoint answers 404 for a group with no viewer-visible member, using
    the same status and message as a group that does not exist. It cannot arise
    from this page, so it renders as no panel rather than an error banner.

    The panel does not render for an ungrouped release, and fires no request for
    one — which is most of them, since grouping is opt-in and never backfilled.

  • A collage row that stands for two entries says so, and removing it removes
    the album
    (#319) —
    GET /collages/{id} now collapses entries that are the same album catalogued
    in two communities. The surviving row carries a copy-count chip, and expanding
    it lists the absorbed copies above the edition stack, each linking into its
    own
    community.

    × now means "remove the album", not "remove this row". Deleting only the
    representative left the absorbed copy behind, to resurface as its own row on
    the next load. The deletable copies are computed before anything is sent,
    from the same per-row permission the button itself uses — the collage owner,
    that row's own adder, or collage staff — so the confirm states the outcome up
    front ("you can remove 1 — the rest were added by other members and will
    stay") and no request is fired that is already known to be refused.

    Deletions run one at a time and stop on a 429 rather than firing the
    remainder into the same rate limit. The catch is kept regardless: 403 also
    covers a locked collage, which the client-side predicate cannot see.

    The absorbed copies are shown by title and linked by community id. The collage
    response carries no community name — for the entry or its copies — and
    naming them would cost a request per expanded row.

  • Search can collapse duplicates onto the album
    (#320) — a Show releases · albums toggle on the release browse page. In album mode the row
    is the group, so an album held in three communities is one result and
    total counts albums; each row lists the versions this viewer can reach.

    It is a separate endpoint rather than a flag because /search/releases
    paginates with skip/take: collapsing a fetched page there would leave
    total overstating and show a group straddling a page boundary twice.

    Every filter carries across untouched — the two endpoints accept identical
    ones — but the sort vocabularies differ, so consumers, contributors
    and random are reconciled to the group default on toggle rather than sent,
    since the group endpoint answers 400 for them. There is deliberately no
    member-count sort: the api can only count a group's whole relation, not the
    part the viewer may see.

    An empty album search names its own cause — grouping is opt-in and never
    backfilled, so on an uncurated catalogue every album query is empty — and
    links back to the same filters as releases. The shared "No releases found."
    would be wrong for the majority of queries rather than an edge case.

  • Staff can search the invite pool by email and cancel a pending invite
    (#329,
    api#636).

    • Search is a case-insensitive substring, sent 300ms after typing stops
      and only at 3 characters or more, the api's minimum. It combines with the
      status filter, and Clear resets both.
    • Cancel appears on pending rows for staff with invites_edit (or
      admin). A modal names the address and takes a required Reason (staff
      only, audit log) and an optional Message to inviter (a System PM; blank
      notifies nobody). The api refunds the inviter when the invite was spent.
      A 409 or 404, meaning the invite was accepted, lapsed or cancelled
      first, shows the api's msg and refreshes the list. On existing installs
      no rank carries invites_edit until an admin grants it.
    • A Sent column shows when each invite was last sent (createdAt).
  • Staff can revoke a member's invites and set their invite balance from the
    profile
    (#329,
    api#636). A new
    Invites panel sits under Change Rank in Staff Actions, showing the
    balance and whether invite privileges are allowed or revoked.

    • Revoke invites / Restore invites opens a dialog that says what the
      change does, then takes a required Reason (staff only, audit log) and an
      optional Message to member (a System PM; blank notifies nobody). The
      direction is fixed when the dialog opens.
    • Edit balance takes a new balance from 0 to 1000 and sends the balance on
      screen as expectedInviteCount. Save stays disabled while the value is
      unchanged. A 409 means the balance moved first: the dialog stays open with
      what staff typed, waits for the reloaded profile, and names the new balance.
      Nothing is retried automatically. The dialog reads the balance from its own
      subscription to the cached profile, so the notice, Current balance and
      the expectedInviteCount a save sends are always the same number, and they
      follow a background refresh.
    • The panel needs invites_edit and a profile that carries both invite
      fields. The api sends them only to staff it recognises by staff, admin,
      users_edit, users_warn or users_disable, so a rank holding
      invites_edit alone sees no panel. On existing installs no rank carries
      invites_edit until an admin grants it.
    • The profile sidebar marks a revoked member's balance (revoked), for the
      member and for staff. The balance is kept while revoked.
  • The invite page explains the rules, the gates, and what you have pending
    (#329,
    #331,
    api#637,
    api#640). /invite
    now reads rules, send, pending invites, then the invite tree.

    • The Golden Rules are quoted, not paraphrased. Rules 1.1, 2.1 and 2.2
      come from GET /rules/tree and render through utils/rulesText.tsx with
      the tree's variables, replacing two hard-coded paragraphs. They are
      selected by their stable code and numbered by position, so a reordered
      tree renumbers them rather than quoting the wrong rule.
    • A refused sender is told before typing. The page reads
      GET /profile/me/invites/eligibility and, when it refuses, shows the api's
      own msg in place of the form; a trailing site path in that message is
      rendered as a link. A send still surfaces its own 403, which remains the
      authority. If the eligibility read itself fails, the form is shown: the
      POST decides.
    • Pending invites lists what has not been accepted yet, soonest to lapse
      first, with the note, when it was sent and when it expires. Withdraw
      confirms first, saying the link stops working immediately and that the
      address cannot be invited again until the original expiry, then reports the
      api's msg — which is what says whether the invite came back to you.
    • The header count follows a send or a withdraw, which it did not before,
      and shows ∞ for a sender holding invites_unlimited (or admin). The
      old ∞ branch keyed on a null count, which the api never sends. An
      unlimited sender's page says "Invites: unlimited" instead of a balance.
    • ${irc} in rule 1.1 points at the public IRC guide rather than /irc, a
      route this app does not have
      (api#630).
  • A date in the future reads as the future. readableTime counted only
    backwards, so anything ahead fell into its "just now" branch. It now reads
    in 2d, which also fixes the staff invite pool's Expires column, where
    every pending invite claimed to expire "just now".

  • A disabled account is told where reactivation happens
    (#324,
    api#622). Signing in
    to a disabled account showed a four-word toast that then vanished. The login
    form now keeps a panel that names the IRC channel to ask in and links the
    public guide on connecting, both taken from the 403 itself
    (AccountDisabledResponse), so neither value is written into this repo.

    Reactivation is handled by staff on IRC, not in this app: the vendored
    /auth/reactivation-request and /auth/reactivation-confirm routes
    deliberately get no surface here. The panel stays until the next sign-in
    attempt, rather than disappearing on a timer while the member is reading a
    channel name.

    An api older than the 403 that carries those two fields still shows the
    toast it always did.

  • A full site says so, to the visitor and to staff
    (#327) — stellar-api
    0.9.5 enforces maxUsers as a count of enabled accounts, and reports it
    anonymously as registrationFull on GET /install. Two surfaces consume it.

    The register page renders a notice in place of the form: "Registration is
    full: the site has reached its member limit."
    The same words in invite mode,
    which is a deliberate omission — an invite's clock keeps running while the
    site is full, so this page cannot promise a key stays valid. The api's own
    refusal names the date the key runs out, and that reply is already surfaced
    verbatim on submit, which is where a key exists to be accurate about.

    Staff get an undismissable banner in the ModBar, not a setup-checklist item:
    checklist items dismiss permanently by id, so one would go silent the next
    time the site filled. It names both consequences — registration and invite
    sending are refused — and links to the settings page where maxUsers lives.

    The banner polls, alone among this query's readers. App.tsx holds a root
    subscription for the whole session and nothing in this app calls
    setupListeners, so an unpolled read is the boot-time snapshot: staff who
    opened a tab before the site filled would never see the banner at all.

    Known gap, tracked in stellar-api: registrationFull is forced false while
    registration is closed, but invite sending is refused on capacity alone. So
    closing registration on a full site keeps refusing sends while the banner goes
    dark. That needs a capacity signal independent of registration status.

  • Staff can opt a rank into the invite handout
    (#326) — the rank form
    gains inviteGrantPerPeriod and inviteCap, the two settings stellar-api
    0.9.5 added for the periodic class-based handout (ADR-0039). The api ships
    every rank at 0, so until now the only way to turn grants on was to write to
    the database by hand.

    Both say 0 = none, which is the opposite of the 0 = unlimited on
    Personal Collage Limit beside them. That split is the api's own — the schema
    documents assetLimit as the deliberate opposite of the collage and
    stylesheet limits — so the form states each field's meaning rather than
    assuming a house convention.

    A live warning covers the settings that save cleanly and then do nothing. The
    obvious one is a rate with a cap of 0. The one that does not announce itself
    is a rate above the cap: the api measures room against the full grant
    (balance > cap - perPeriod), so a member starting at 0 is already at a cap
    below the rate, and the rank advances its clock forever without ever granting.
    The sweep logs that as "at cap", which reads like a healthy member holding
    invites. A rate equal to the cap is fine and does not warn.

    A rate of 0 is stated plainly rather than flagged — that is the rank being
    switched off, not a mistake. One further line notes that grants come from a
    scheduled job, so values are saved but inert until an operator enables it;
    the mode is an environment variable exposed on no route, so the ui cannot say
    which state a given server is in.

    UserRankManager gains one Invites column showing rate / cap, or —
    when the rank earns none. One column rather than two, because neither number
    means anything alone — 5 / 2 is only legible as broken when both are shown.

Changed

  • The vendored contract catches up with stellar-api 0.9.4
    (#301) — UserRank
    gains two required fields, inviteGrantPerPeriod and inviteCap, and both
    rank request bodies accept them as optional non-negative integers. They arrived
    with api#282, the
    periodic class-based invite handout: a rank accrues inviteGrantPerPeriod
    invites every 14 days, up to inviteCap, and both default to 0.

    UserRankManager.test.tsx's makeRank fixture adds the two fields. It builds
    a complete UserRankRecord that mirrors what the API's formatRank sends, so
    the now-required fields failed typecheck until it did. No shipped code was
    affected. info.version moves to 0.9.4; the coupling stays at 0.9, so no
    parity cut is owed under
    ADR-0004.

    Nothing in the app consumes them yet. UserRankFormPage has no inputs for
    either field, so staff cannot opt a rank into the handout from the UI. Because
    every rank defaults to 0 and the api job ships INVITE_GRANT_MODE=off,
    nothing is lost in the meantime.

  • The vendored contract catches up with stellar-api 0.9.3
    (#301) — two paths, two
    schemas, one widened schema and two description rewrites, none of which this
    repo could see while typecheck asserted agreement with a contract the API had
    stopped serving.

    /auth/reactivation-request and /auth/reactivation-confirm, with
    ReactivationRequestBody and ReactivationConfirmBody, arrived with
    api#279 — the way back
    in for a disabled account. AbsorbedCollageEntry gains a required user
    ({ id, username }) from
    api#617, so a collapsed
    collage row can now name the adder whose copy it cannot remove rather than
    only counting them. Two 400 descriptions were rewritten at the source
    (api#600,
    api#603).

    The sync is purely additive — no path, schema or property was removed or
    narrowed, and typecheck is clean, so there was no drift damage to repair.
    info.version is 0.9.3 on both sides and the coupling stays at 0.9, so no
    parity cut is owed under
    ADR-0004.

    Nothing in the app consumes any of it yet. AbsorbedCopies still reads
    not yours to remove and removalConfirmMessage still says "added by other
    members", both of which user.username now makes nameable; and the
    reactivation routes have no surface at all, which is the larger of the two
    gaps.

  • The vendored contract picks up the whole ReleaseGroup surface — nine
    paths and seven schemas that stellar-api has shipped and this repo could not
    see. /release-groups/{id} and its merge/split/cover/log routes arrived with
    api#265;
    /search/release-groups and the additive fields arrived with
    api#605 and
    ADR-0037.

    Three existing schemas gain fields: Release gains group and
    releaseGroupId, CollageEntry gains group and groupedWith, and
    CollageDetail gains a required numVisibleEntries. releaseGroupId is the
    reason this was owed rather than merely late
    — the API has always emitted it,
    but the hand-authored Release schema did not list it, so the release-group
    panel was unbuildable against the contract rather than against the server.

    The sync is purely additive: no path, schema or property was removed or
    narrowed, and info.version is 0.9.3 on both sides, so no parity cut is owed
    under ADR-0004.

    Nothing in the app consumes any of it yet. That is
    #316,
    #317,
    #318,
    #319 and
    #320, which land next
    and all needed this first.

  • The collage entry-list header counts what the list actually holds
    (#316) — it reads
    numVisibleEntries rather than numEntries, because it sits directly above
    the list it describes. The Statistics panel keeps numEntries: a collage's
    size is a property of the collage, and it is the quantity the per-collage
    quota is enforced against. CollageBrowse and UserProfile are unchanged —
    neither has an entry list beside the number.

    The two now disagree for two independent reasons, and both are intended:
    entries whose release the viewer cannot reach are omitted (api ADR-0036), and
    entries sharing a release group are collapsed onto one row
    (api ADR-0037).
    A member outside one of a collage's communities can see "9 entries" above nine
    rows and a Statistics panel reading 12.

  • A grouped release shows its group's cover art
    (#318) — two
    communities' copies of one album routinely carry different artwork, often one
    good scan and one placeholder. releaseCover(group, release) prefers the
    group's canonical cover and falls back to the release's own, so the same album
    looks like the same album across the site.

    The fallback is the common path rather than a defensive one: group.image
    is null for any group with no cover art, and grouping is opt-in and never
    backfilled, so most releases have no group at all.

    Four render sites, in three files: the collage cover mosaic and entry-row
    thumbnail, the release page's Cover panel, and the community release list.
    That list is derived from the contract — the three schemas that carry a
    group are Release, CollageEntry and the /search/releases item — rather
    than from the site table in #318, which named ReleaseBrowsePage (it renders
    no cover art at all, only a text table) and omitted CommunityPage.

  • The vendored contract catches up with stellar-api 0.9.5
    (#328). The resync
    forces one type error: POST /ratio-policy/{userId}/override now requires
    reason, below. Everything else it vendors is new api surface, consumed
    by the invite, login, capacity and rank issues that follow it (#324, #326,
    #327, #329, #330, #331). /auth/reactivation-request and
    /auth/reactivation-confirm leave the contract, and nothing here called
    them.

  • The ratio policy override takes a reason, and a download disable says why
    (#332,
    api#646). Against api
    0.9.5 the panel's { status }-only request answered 400.

    • RatioPolicyPanel sends { status, reason, message? }. Reason is
      required, staff-only and recorded in the audit log; a whitespace-only
      reason is refused before any request. Message to member is optional,
      sent as a System PM, and the hint says a blank one notifies nobody. Both
      clear after a successful override, and a failure shows the api's msg.
    • The status options read "OK", "Ratio watch" and "Downloads disabled", and a
      line under the select says what the choice does. The disable line matters
      most: a staff disable does not lift itself, so it is no nudge that clears
      when the ratio recovers.
    • A disabled member's current state names the cause: Ratio (lifts
      automatically once the ratio recovers, checked daily) or Staff (only staff
      can lift it).
    • The panel moves onto the kit (PageShell, Panel, Field, Button,
      Badge, and field on the select and textareas). It was one of the last
      staff tools still painting gray utilities, illegible on a light theme.
    • RatioWatchPage reads "Download Disabled · Ratio" or "· Staff" in the
      status cell; a staff row's tooltip says only staff can lift it.
    • The member's banner in RatioStats splits by cause. A ratio disable
      says downloads come back automatically once the ratio meets its
      requirement, checked daily, linking the ratio rules. A staff disable says it
      does not lift on its own and links Staff PM. "Contact staff to appeal" is
      gone from both, since the site defines no appeal process. Not yet visible to
      members:
      nothing mounts RatioStats since 63d64cf; mounting the notice
      is #334.

Fixed

  • The login page no longer trails a separator when registration is closed.
    The · between "Forgot password?" and "Register" rendered unconditionally,
    so an install with registration closed showed a stray dot. It now belongs to
    the Register link it separates.

  • The invite pool's status filter and colours use the real statuses
    (#330). The filter
    sent upper-case values the api refuses with 400, including USED, which
    never existed, and the colour check matched nothing, so every row read as
    pending. The filter now offers pending, accepted, expired and
    cancelled, and each row shows a status chip: Pending amber, Accepted green,
    Expired and Cancelled neutral, since an invite that lapsed or was withdrawn
    ended normally.