Skip to content

Let the taxonomy table be the only list of sections, and filter on breaking/featured - #231

Merged
ssavutu merged 2 commits into
mainfrom
fix/taxonomy-not-hardcoded
Sep 4, 2026
Merged

Let the taxonomy table be the only list of sections, and filter on breaking/featured#231
ssavutu merged 2 commits into
mainfrom
fix/taxonomy-not-hardcoded

Conversation

@ssavutu

@ssavutu ssavutu commented Sep 3, 2026

Copy link
Copy Markdown
Member

Two related changes to the articles and sections screens: the section filters stop carrying their own frozen copy of the taxonomy, and the two article flags become filterable.

Nothing hard-codes the section tree any more

Three places kept their own copy of the section list, each frozen at whatever the desk had configured on the day it was written.

article_params.go held a section → subsection map used to validate ?section_slug= and ?subsection_slug= whenever there was no database handle. It had drifted: it knew 41 subsections where the live tree has 93, and it had no special-editions at all. It only ran in tests, which is worse than it sounds — the tests were asserting against a fiction, so a subsection an editor added resolved in production and failed here. Without a handle there is now simply no taxonomy, and every slug is unknown.

The sections screen ordered sections by a list of seven slugs, alphabetising anything else onto the end. It now orders by id — the order the sections were created in, which is what the articles screen's own section filter already did. The two screens agree, and a new section appears in both instead of being invisible to a list frozen in a file. New rows take MAX(id)+1, so they land at the end exactly as unlisted ones used to.

The dashboard's section count fell back to counting homepage blocks against a list of six keys, so it answered "6" however many sections existed (there are 8 live). It keeps its placeholder instead when taxonomy is unavailable, rather than showing a wrong number that looks like a real one.

Also drops the typeLabel special case that printed "Column" for anything parented by the literal slug columns.

Deliberately left alone: GetHomepage's slug/key/limit table. Those JSON keys (news, candp, …) are the response contract Scalene reads and are fixed fields on models.HomepageResponse — changing them is a public API change, not a filter cleanup. Seed data in database/taxonomy.go, footer_settings.go and scripts/*.py is also intentionally literal; it is editable once seeded.

Breaking and featured filters

Both flags are set from the article form and shown as badges in the listing, but there was no way to ask the listing for them: finding what is still flagged as breaking, or what is currently pinned, meant paging through everything and reading badges.

GET /v1/articles takes ?breaking= and ?featured=, and the articles screen gets a three-state control for each. Three-state because "not breaking" has to be reachable — an editor clearing a false alarm wants the stories that are not flagged, which is not the same request as not filtering.

The off-side condition is COALESCE(col, 0) = 0 rather than col = 0. Both columns are nullable, so if any row has NULL rather than 0 the plain comparison silently drops it from the "off" half. Both columns scan into sql.NullBool, so live values are constrained to NULL/0/1 and col = 1 is right for the on-side.

priority is the featured column; is_featured is the name it goes by over the wire, and GetFeaturedArticles already reads it the same way. The listing also gains a Featured badge next to the Breaking one, so a filtered list says why each row is in it.

Checked against production

  • Section ordering is unchanged. Live sections are ids 1–7 in exactly the order the deleted SECTION_ORDER listed, plus graduation at id 37. Graduation sorted to the end under the old list (unlisted → appended) and sorts to the end under id (37 > 7). Same result.
  • The deleted map was a no-op in production. Routes always wire a live *sql.DB, and main.go exits if the connection fails, so the conn == nil branch it backed was never reached outside tests.
  • No schema change and no migration. Both columns and idx_articles_breaking_news already exist via EnsureArticlesSchema.
  • No performance regression. There is no index on priority, but every listing query already carries the non-sargable artifact filter and so already scans, and GetFeaturedArticles already runs an unindexed priority = 1 on every homepage load.
  • Payload shape confirmed. The live listing already returns is_featured and breaking_news, which is what the new badge and filter mapping read.

Testing

go build ./..., go vet ./..., go test ./... -p 1, tsc --noEmit, eslint and vitest all pass. swag init --parseDependency --parseInternal rerun for the CI docs gate; that diff is only the two new params.

Two integration tests in this PR are unrun locally — they need CMS_TEST_DSN and there was no MariaDB reachable on this machine. They compile and vet, and reuse the existing harnesses in their files, but the assertions have not executed; CI is the first real run.

  • TestTaxonomyHTTPArticleFiltersResolveFromTheTable replaces what the deleted map really pinned: a subsection resolving to its root section, and a subsection that contradicts the named section staying a 400 rather than a 404.
  • TestArticleFlagFiltersHTTP covers both flags on and off, the two combined, stacked on ?section_slug=, the bare ?breaking form, and the unparseable value — including a NULL-flagged row specifically to catch the COALESCE bug.

🤖 Generated with Claude Code

https://claude.ai/code/session_01L4qBhBdQto1yNp1zP7VLYc

Three places kept their own copy of the section tree, each frozen at whatever
the desk had configured on the day it was written.

article_params.go held a section -> subsection map used to validate
?section_slug= and ?subsection_slug= whenever there was no database handle. It
had drifted: it knew 41 subsections where the live tree has 93, and it had no
special-editions at all. It only ran in tests, which is worse than it sounds --
the tests were asserting against a fiction, so a subsection an editor added
resolved in production and failed here. Without a handle there is now simply no
taxonomy, and every slug is unknown. The two behaviours that map really pinned,
a subsection resolving to its root section and a subsection that contradicts
the named section staying a 400, move to taxonomy_integration_test.go where a
real table can answer.

The sections screen ordered sections by a list of seven slugs; anything else
was alphabetised onto the end. It now orders by id, the order the sections were
created in, which is what the articles screen's own filter already did -- so
the two screens agree and a new section appears in both. New rows take
MAX(id)+1, so they land at the end exactly as the unlisted ones used to. Live,
this is a no-op: sections are ids 1-7 in precisely the order the list gave, and
Graduation (id 37) sorted last as an unlisted slug and sorts last by id too.

The dashboard's section count fell back to counting homepage blocks against a
list of six keys, so it answered "6" however many sections existed; there are
eight. It keeps its placeholder instead when taxonomy is unavailable, rather
than showing a wrong number that looks like a real one.

Also drops the typeLabel special case that printed "Column" for anything
parented by the literal slug "columns".

The homepage handler's slug/key/limit table is deliberately left alone: those
JSON keys are the contract Scalene reads, not a filter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4qBhBdQto1yNp1zP7VLYc
@ssavutu
ssavutu force-pushed the fix/taxonomy-not-hardcoded branch from e9ed1e4 to 0b21052 Compare September 4, 2026 02:46
Both flags are set from the article form and shown as badges in the listing,
but there was no way to ask the listing for them: finding what is still flagged
as breaking, or what is currently pinned, meant paging through everything and
reading badges.

GET /v1/articles takes ?breaking= and ?featured=, and the articles screen gets
a three-state control for each. Three-state because "not breaking" has to be
reachable: an editor clearing a false alarm wants the stories that are NOT
flagged, which is not the same request as not filtering.

The off-side condition is COALESCE(col, 0) = 0 rather than col = 0. Both
columns are nullable, so any row holding NULL rather than 0 would be dropped
from the "off" half by the plain comparison -- and the WordPress archive is
nine thousand of the ten thousand rows. Both columns scan into sql.NullBool, so
live values can only be NULL, 0 or 1, which is what makes col = 1 right for the
on-side. An unparseable value is treated as absent rather than guessed at, so a
typo widens the listing instead of narrowing it to the wrong half.

priority is the featured column; is_featured is the name it goes by over the
wire, and GetFeaturedArticles already reads it the same way. There is no index
on it, but every listing already scans -- the artifact filter is not sargable --
and the homepage already runs an unindexed priority = 1 on every load.

The listing also gains a Featured badge next to the Breaking one, so a filtered
list says why each row is in it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4qBhBdQto1yNp1zP7VLYc
@ssavutu
ssavutu force-pushed the fix/taxonomy-not-hardcoded branch from 0b21052 to 6dee5a0 Compare September 4, 2026 03:08
@ssavutu
ssavutu merged commit 0cced76 into main Sep 4, 2026
7 checks passed
@ssavutu
ssavutu deleted the fix/taxonomy-not-hardcoded branch September 4, 2026 03:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant