Skip to content

Render the landing page's plugin rows from the roster [#26] - #189

Merged
iderex merged 2 commits into
mainfrom
enhancement/the-landing-page-shows-the-roster
Aug 25, 2026
Merged

Render the landing page's plugin rows from the roster [#26]#189
iderex merged 2 commits into
mainfrom
enhancement/the-landing-page-shows-the-roster

Conversation

@iderex

@iderex iderex commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

What was wrong

The landing page said what this project is and listed none of the plugins it is
about, because nothing in the tree held a roster to list them from:

git ls-tree --name-only 65353c6 data/
data/clients.json
data/design-tokens.json
data/publisher.json
data/security-contact.json

Run 2026-08-25. 65353c6 is the mainline this branch was cut from.

What it waited on was entry 6 of #7, which asked who lands the file this board
only reads and what the build starts from. That entry is answered, on
2026-08-24:

gh issue view 7 --repo Flowfin/site --json comments --jq '.comments[].body' \
  | grep -o 'Answer to entry 6, decided 2026-08-24: option (b)[^.]*\.'
Answer to entry 6, decided 2026-08-24: option (b) - the site builds against fixtures now and vendors the published roster file once it lands in the data repository; until then the freshness checks stay disabled by name.

Run 2026-08-25.

What this does

data/roster.json carries twelve rows in the shape docs/roster-schema.md
declares. The content is taken from the table the organisation profile
publishes, which is the other consumer of the same list rather than a second
opinion about it:

gh api repos/Flowfin/.github/contents/profile/README.md --jq '.content' \
  | base64 -d | grep -c '^| \[.*jellyfin-plugin'
12

Run 2026-08-25.

data/repositories.json is the recorded answer to the one question the roster
cannot answer about itself. The parser asks whether each row's repository is
there and refuses a read that skipped it, and asking a host is a request off
this machine, which a build may not make: it would produce different bytes on
different days and fail with no network. So the answer is taken once, written
down with the command that took it, and read from the tree.

internal/site/plugins.go reads both and hands the page one entry per row. The
template gains the table and learns no count and no state word: the rows carry
the words, and the sentence above them is composed where they are read, so a
thirteenth plugin is a row in a file and nothing else.

content/index.txt said of itself that it was the placeholder the pages would
replace with something that knows about a roster. It does now, so the paragraph
says what the table is and what a state word declares.

docs/roster-schema.md said that who lands the file is a question on the
tracker. It is answered, and the document now says where the bytes come from
today and what the second file beside it is for.

What this does not do, because it is somebody else's object rather than a
gap left open. The computed shipping state is #22 and none of it is here: the
table shows what a row declares, which the schema calls the floor rather than
the answer, and the page says so in as many words. The requests row declares
shell while its repository has published one release, and that disagreement is
#23, in this tree as well as in the manifest the catalogue serves; nothing here
decides it, and no check in this tree reads it yet. The comparison against the
organisation profile is #25. The per-plugin pages are #28.

Closes

Closes #26.

What was run

The gate, at the commit being pushed, d622af5:

go run . ci
gate: 7 legs, in order: format, vet, test, build, links, sitemap, invariants
  needs-network was not asked for. Asking costs a request to the public name from whatever machine runs it, and a verdict that moves when somebody else's service does rather than when this tree changes. Ask with: go run ./harness/needs-network
  format: ok, 58 file(s)
  vet: ok
  test: ok, 28 test file(s)
  build: ok, 8 file(s)
  links: every reference that stays inside this site resolves to a file the build wrote
  sitemap: every page the build wrote is listed once, and every entry has a page behind it
  invariants: ok, 39 rule(s) decided, 1 owed and not decided
7 of 7 legs ran. None was skipped.

No test was skipped, and none needed a display or elevation.

The rows match the roster, row for row

The identifier and the sentence of every row, out of the file and out of the
page, compared rather than eyeballed:

go run . build > /dev/null
paste -d "\t" <(jq -r '.[].id' data/roster.json) <(jq -r '.[].summary' data/roster.json) \
  | tr -d '\r' > /tmp/a
tr -d '\r\n' < dist/index.html \
  | grep -o -E '<td><a[^>]*>[a-z-]+</a></td>[^<]*<td>[^<]*</td>' \
  | sed -E 's#<td><a[^>]*>([a-z-]+)</a></td>[^<]*<td>([^<]*)</td>#\1\t\2#' > /tmp/b
diff /tmp/a /tmp/b ; echo "exit=$?"
exit=0
wc -l /tmp/a /tmp/b
  12 /tmp/a
  12 /tmp/b

Run 2026-08-25 at d622af5. Empty diff, twelve lines each side.

Every line of the budget in #6 that this tree can read

go run . invariants | grep -E '^  (page-fits-the-markup-budget|page-fits-the-stylesheet-budget|page-fetches-no-script|page-downloads-no-web-font|landing-page-asks-for-at-most-two-images|output-references-no-domain-outside-the-allowlist)'
  page-fetches-no-script: ok, 5 file(s) of every page the build produced
  page-fits-the-markup-budget: ok, 5 file(s) of every page the build produced
  page-fits-the-stylesheet-budget: 5 file(s) of every page the build produced carried no inlined stylesheets, so this rule decided nothing
  page-downloads-no-web-font: ok, 5 file(s) of every page the build produced
  landing-page-asks-for-at-most-two-images: ok, 1 file(s) of every page the build produced
  output-references-no-domain-outside-the-allowlist: ok, 8 file(s) of every file the build produced

wc -c dist/index.html
5687 dist/index.html

Both run 2026-08-25 at d622af5. Two lines of that budget are properties
of a render rather than of the bytes, the largest contentful paint and the
layout shift, and neither is decided anywhere in this repository today; those
are #35, and this change moves neither of them. The stylesheet line still
decides nothing while no produced page carries one, which is the same reading
#26 already carried and is unchanged by the table.

The guards bite

The roster taken out of the tree, which is what losing the table looks like from
a page nobody read:

go run . invariants
data/roster.json is not tracked in this tree, so the landing page lists no plugin and reads as a page about a project that has none

One repository dropped from the record, which is what the parser's question
exists to refuse:

go run . build
reading data/roster.json: the roster was refused, 1 reason(s):
  row 1, sso: names the repository Flowfin/jellyfin-plugin-sso, which is not there, so every link this row produces would resolve to nothing

The rows dropped on the way from the read to the page, which is the mistake the
suite exists for:

go test ./internal/site/ -run 'TestThePageCarriesOneRowPerRosterRowInOrder|TestARowAddedToTheRoster|TestASummaryCarryingMarkup'
--- FAIL: TestThePageCarriesOneRowPerRosterRowInOrder (0.04s)
--- FAIL: TestARowAddedToTheRosterProducesARowWithNoOtherEdit (0.02s)
--- FAIL: TestASummaryCarryingMarkupRendersAsText (0.02s)
FAIL

All three run 2026-08-25, each edit reverted before the next, and the tree
restored afterwards.

What the suite asserts that the runs above do not

A row added to the roster produces a row with nothing else edited. A sentence
carrying markup renders as text, with data that tries rather than with the data
this tree happens to hold. A state word the page has no words for reds the build
rather than reaching a reader as a bare identifier. And the read fails closed in
four ways: a row whose repository the record does not carry, a record carrying
no repository, a record saying nothing about when it was taken, and a roster
with no record beside it.

What a server check caught that this machine's gate did not

The first head of this branch was refused by the static analysis workflow, and
the refusal is recorded here rather than removed with the commit that repaired
it. Both reads took their path from a variable one line above, and the rule
refuses that so a reader can decide from the call whether a path is whatever
arrived:

Ran 5 rules on 30 files: 2 findings.
  internal/site/plugins.go
  tools.semgrep.file-read-with-a-path-the-call-site-cannot-vouch-for
    63: body, err := os.ReadFile(rosterPath)
   111: body, err := os.ReadFile(p)

Both are assembled at the call site on d622af5. That leg is not part of
go run . ci, which is why the gate on this machine was green over it, and
that gap is a property of the leg list rather than of this change.

The means

Go, the build verb and the invariant table this repository already carries.
The roster parser was already here and already fails closed; this change adds
its caller and adds no door beside it. JSON for both data files, which is what
docs/roster-schema.md already decided for the roster and what every other file
the build reads is written in. Nothing is added to the module graph: both new
files are read with encoding/json and the rest is the standard library.

The alternative refused was having the build ask the host whether each
repository is there. That is a request off the machine, so the build would stop
being reproducible and would fail with no network, which
decisions/0001-where-the-plugin-list-comes-from.md already refuses for the
release lists and for the same reason.

Who read it

Nobody but me. The ruleset requires no approving review, so this is merged by
whoever opened it, and the evidence above stands in place of a second reader.

The landing page said what this project is and listed none of the plugins it is
about. Nothing in the tree held a roster to list them from, and no reading of
the produced bytes told a page whose table was lost apart from a project that
has no plugins.

Entry 6 of #7 answered on 2026-08-24 that the site builds against a copy
committed here and vendors the published file once it exists in the repository
that holds the machine-readable data. So data/roster.json is a copy this
repository authors for now, twelve rows taken from the table the organisation
profile publishes, which is the other consumer of the same list.

The parser asks whether each row's repository is there and refuses a read that
skipped the question. Asking a host is a request off this machine, which a build
may not make: it would produce different bytes on different days and fail with
no network. data/repositories.json is that answer taken once, written down with
the command that took it, and read from the tree. A row whose repository the
record does not carry reds the build rather than rendering a link to nothing.

The template gains the table and learns no count and no state word: the rows
carry the words, and the sentence above them is composed where they are read.
A tree that lost the roster is refused by name by the invariant gate, for the
reason the claim about the clients already is.

docs/roster-schema.md said that who lands the file is a question on the tracker.
It is answered, and the document says where the bytes come from today and what
the second file beside them is for.

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
Comment thread internal/site/plugins.go Fixed
Comment thread internal/site/plugins.go Fixed
@iderex iderex added the enhancement New feature or request label Aug 25, 2026
@iderex iderex self-assigned this Aug 25, 2026
@iderex iderex added this to the The pages milestone Aug 25, 2026
The static analysis rule refuses a read whose path the line itself does not
show to be assembled, so that whether a path is whatever arrived is decidable
from the call rather than from every caller. Both reads took the path from a
variable one line above.

    Ran 5 rules on 30 files: 2 findings.
      internal/site/plugins.go
      tools.semgrep.file-read-with-a-path-the-call-site-cannot-vouch-for
        63: body, err := os.ReadFile(rosterPath)
       111: body, err := os.ReadFile(p)

Run by the static analysis workflow on the head before this one.

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
@iderex
iderex merged commit b2b47bd into main Aug 25, 2026
17 checks passed
@iderex
iderex deleted the enhancement/the-landing-page-shows-the-roster branch August 25, 2026 00:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The landing page

2 participants