Repository navigation
v0.9.5
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.groupinlines 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
404for 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
429rather than firing the
remainder into the same rate limit. The catch is kept regardless:403also
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) — aShow releases · albumstoggle on the release browse page. In album mode the row
is the group, so an album held in three communities is one result and
totalcounts albums; each row lists the versions this viewer can reach.It is a separate endpoint rather than a flag because
/search/releases
paginates withskip/take: collapsing a fetched page there would leave
totaloverstating 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, soconsumers,contributors
andrandomare reconciled to the group default on toggle rather than sent,
since the group endpoint answers400for 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.
A409or404, meaning the invite was accepted, lapsed or cancelled
first, shows the api'smsgand refreshes the list. On existing installs
no rank carriesinvites_edituntil an admin grants it. - A Sent column shows when each invite was last sent (
createdAt).
- Search is a case-insensitive substring, sent 300ms after typing stops
-
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 asexpectedInviteCount. Save stays disabled while the value is
unchanged. A409means 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
theexpectedInviteCounta save sends are always the same number, and they
follow a background refresh. - The panel needs
invites_editand a profile that carries both invite
fields. The api sends them only to staff it recognises bystaff,admin,
users_edit,users_warnorusers_disable, so a rank holding
invites_editalone sees no panel. On existing installs no rank carries
invites_edituntil 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.
- Revoke invites / Restore invites opens a dialog that says what the
-
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 fromGET /rules/treeand render throughutils/rulesText.tsxwith
the tree'svariables, replacing two hard-coded paragraphs. They are
selected by their stablecodeand 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/eligibilityand, when it refuses, shows the api's
ownmsgin place of the form; a trailing site path in that message is
rendered as a link. A send still surfaces its own403, 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'smsg— 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 holdinginvites_unlimited(oradmin). 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).
- The Golden Rules are quoted, not paraphrased. Rules 1.1, 2.1 and 2.2
-
A date in the future reads as the future.
readableTimecounted 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 the403itself
(AccountDisabledResponse), so neither value is written into this repo.Reactivation is handled by staff on IRC, not in this app: the vendored
/auth/reactivation-requestand/auth/reactivation-confirmroutes
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
403that 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.5enforcesmaxUsersas a count of enabled accounts, and reports it
anonymously asregistrationFullonGET /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 wheremaxUserslives.The banner polls, alone among this query's readers.
App.tsxholds 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:
registrationFullis forcedfalsewhile
registration isclosed, 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
gainsinviteGrantPerPeriodandinviteCap, the two settings stellar-api
0.9.5added for the periodic class-based handout (ADR-0039). The api ships
every rank at0, 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 the0 = unlimitedon
Personal Collage Limit beside them. That split is the api's own — the schema
documentsassetLimitas 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 of0. 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 at0is 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
0is 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.UserRankManagergains one Invites column showingrate / cap, or—
when the rank earns none. One column rather than two, because neither number
means anything alone —5 / 2is 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,inviteGrantPerPeriodandinviteCap, 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 accruesinviteGrantPerPeriod
invites every 14 days, up toinviteCap, and both default to0.UserRankManager.test.tsx'smakeRankfixture adds the two fields. It builds
a completeUserRankRecordthat mirrors what the API'sformatRanksends, so
the now-required fields failedtypecheckuntil it did. No shipped code was
affected.info.versionmoves to0.9.4; the coupling stays at0.9, so no
parity cut is owed under
ADR-0004.Nothing in the app consumes them yet.
UserRankFormPagehas no inputs for
either field, so staff cannot opt a rank into the handout from the UI. Because
every rank defaults to0and the api job shipsINVITE_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 whiletypecheckasserted agreement with a contract the API had
stopped serving./auth/reactivation-requestand/auth/reactivation-confirm, with
ReactivationRequestBodyandReactivationConfirmBody, arrived with
api#279 — the way back
in for a disabled account.AbsorbedCollageEntrygains a requireduser
({ 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, andtypecheckis clean, so there was no drift damage to repair.
info.versionis0.9.3on both sides and the coupling stays at0.9, so no
parity cut is owed under
ADR-0004.Nothing in the app consumes any of it yet.
AbsorbedCopiesstill reads
not yours to removeandremovalConfirmMessagestill says "added by other
members", both of whichuser.usernamenow 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
ReleaseGroupsurface — 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-groupsand the additive fields arrived with
api#605 and
ADR-0037.Three existing schemas gain fields:
Releasegainsgroupand
releaseGroupId,CollageEntrygainsgroupandgroupedWith, and
CollageDetailgains a requirednumVisibleEntries.releaseGroupIdis the
reason this was owed rather than merely late — the API has always emitted it,
but the hand-authoredReleaseschema 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, andinfo.versionis0.9.3on 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
numVisibleEntriesrather thannumEntries, because it sits directly above
the list it describes. The Statistics panel keepsnumEntries: a collage's
size is a property of the collage, and it is the quantity the per-collage
quota is enforced against.CollageBrowseandUserProfileare 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
groupareRelease,CollageEntryand the/search/releasesitem — rather
than from the site table in #318, which namedReleaseBrowsePage(it renders
no cover art at all, only a text table) and omittedCommunityPage. -
The vendored contract catches up with stellar-api
0.9.5
(#328). The resync
forces one type error:POST /ratio-policy/{userId}/overridenow 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-requestand
/auth/reactivation-confirmleave 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.5the panel's{ status }-only request answered400.RatioPolicyPanelsends{ 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'smsg.- 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, andfieldon the select and textareas). It was one of the last
staff tools still painting gray utilities, illegible on a light theme. RatioWatchPagereads "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
RatioStatssplits 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 mountsRatioStatssince63d64cf; 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 with400, includingUSED, which
never existed, and the colour check matched nothing, so every row read as
pending. The filter now offerspending,accepted,expiredand
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.