Let the taxonomy table be the only list of sections, and filter on breaking/featured - #231
Merged
Conversation
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
force-pushed
the
fix/taxonomy-not-hardcoded
branch
from
September 4, 2026 02:46
e9ed1e4 to
0b21052
Compare
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
force-pushed
the
fix/taxonomy-not-hardcoded
branch
from
September 4, 2026 03:08
0b21052 to
6dee5a0
Compare
This was referenced Sep 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.goheld 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 nospecial-editionsat 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 takeMAX(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
typeLabelspecial case that printed "Column" for anything parented by the literal slugcolumns.Deliberately left alone:
GetHomepage's slug/key/limit table. Those JSON keys (news,candp, …) are the response contract Scalene reads and are fixed fields onmodels.HomepageResponse— changing them is a public API change, not a filter cleanup. Seed data indatabase/taxonomy.go,footer_settings.goandscripts/*.pyis 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/articlestakes?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) = 0rather thancol = 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 intosql.NullBool, so live values are constrained to NULL/0/1 andcol = 1is right for the on-side.priorityis the featured column;is_featuredis the name it goes by over the wire, andGetFeaturedArticlesalready 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_ORDERlisted, plusgraduationat id 37. Graduation sorted to the end under the old list (unlisted → appended) and sorts to the end underid(37 > 7). Same result.*sql.DB, andmain.goexits if the connection fails, so theconn == nilbranch it backed was never reached outside tests.idx_articles_breaking_newsalready exist viaEnsureArticlesSchema.priority, but every listing query already carries the non-sargable artifact filter and so already scans, andGetFeaturedArticlesalready runs an unindexedpriority = 1on every homepage load.is_featuredandbreaking_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 --parseInternalrerun 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_DSNand 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.TestTaxonomyHTTPArticleFiltersResolveFromTheTablereplaces 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.TestArticleFlagFiltersHTTPcovers both flags on and off, the two combined, stacked on?section_slug=, the bare?breakingform, 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