CI / extension-version (push) Successful in 4s
CI / lint (push) Successful in 4s
Build images / sign-extension (push) Successful in 4s
Build images / build-agent (push) Successful in 9s
CI / frontend-build (push) Successful in 27s
CI / backend-lint-and-test (push) Successful in 34s
Build images / build-web (push) Successful in 1m17s
Build images / smoke-web (push) Skipped
CI / integration (push) Failing after 2m8s
Build images / build-ml (push) Successful in 2m18s
Build images / promote (push) Skipped
The step phase A was building toward. A1 made the gated count true, A2 made it a durable state, A3 made it visible in Subscriptions — but Subscriptions is where you go once you already suspect something. This is the line that reaches someone who wasn't looking. A thin grey strip above the feed, front door only: last check, sources failing, sources you can't see. Only the actionable items take a colour, and nothing renders at zero — a permanent "0 failing" trains you to skip the line, which would hide the real number when it appears. Two predicates, defined once. The ribbon counts and the surfaces it links to have to agree on what "failing" and "no access" MEAN, or the ribbon says 3 and the card shows 4. They live in db_helpers, which exists for exactly this reason (its docstring: divergent copies are how the race bugs crept in). Not in source_service, because scheduler_service needs them too and source_service already imports scheduler_service — the other direction is a cycle. Counting deliberately spans all ENABLED sources rather than the auto_check subset scheduler_status already walks: a source erroring on a manual-only artist is still erroring. Disabled sources count for nothing, which is what makes issue 1285 the real escape hatch for a sub you stopped paying for. Extends the existing schedule-status endpoint rather than adding a parallel aggregate — the store already fetches it. Two scalar COUNTs. The status filter is now URL-addressable, which it had to be for the ribbon's links to land anywhere: a count that drops you on an unfiltered list makes the reader redo the filtering the ribbon just did. Mirrors how artistFilter already reads from route.query. Front-door-only via a route prop, not a route.name check, so the view doesn't need to know what it's mounted as and the router states the intent in one place. Inside Browse's Posts tab you're looking FOR something and the hub is one click away. The fetch is swallowed on mount by design (rule 164): this is an aside, and the feed must render whether or not the status call succeeds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LNXXULQDjVZmbuNa2G9mD9
118 lines
6.7 KiB
JavaScript
118 lines
6.7 KiB
JavaScript
import { createRouter, createWebHistory, createMemoryHistory } from 'vue-router'
|
|
import SettingsView from './views/SettingsView.vue'
|
|
import GalleryView from './views/GalleryView.vue'
|
|
import ShowcaseView from './views/ShowcaseView.vue'
|
|
import ExploreView from './views/ExploreView.vue'
|
|
import BrowseView from './views/BrowseView.vue'
|
|
import ArtistView from './views/ArtistView.vue'
|
|
import SeriesView from './views/SeriesView.vue'
|
|
import SeriesManageView from './views/SeriesManageView.vue'
|
|
import SeriesReaderView from './views/SeriesReaderView.vue'
|
|
import SubscriptionsView from './views/SubscriptionsView.vue'
|
|
import PostsView from './views/PostsView.vue'
|
|
|
|
// The application's front door. `/` redirects here. Changing the front door
|
|
// is a one-line edit (e.g. '/gallery' or '/tags').
|
|
//
|
|
// Moved from '/showcase' to '/latest' (milestone #387 B1). Showcase answers
|
|
// "show me something" — a random TABLESAMPLE, lean-back, and it can never tell
|
|
// you anything is wrong. The feed answers "what arrived?", which is a question
|
|
// the operator has every day, and it is the only view where a failing source
|
|
// shows up on its own as a creator who has gone quiet. The per-artist "new
|
|
// since last visit" badges (#597) were a workaround for this view not being
|
|
// the door. Showcase is demoted to a nav entry, not removed.
|
|
export const FRONT_DOOR = '/latest'
|
|
|
|
const routes = [
|
|
// Root is a redirect only — no meta.title so it stays out of the nav.
|
|
{ path: '/', redirect: FRONT_DOOR },
|
|
|
|
// The front door (#387 B1): the post feed as its own surface. Reuses
|
|
// PostsView unchanged — it is already a self-contained feed (own container,
|
|
// own store, infinite scroll, deep-link anchoring), and Browse only ever
|
|
// wrapped it in a tab strip. Mounting it directly IS the difference the
|
|
// promotion was after: a door you arrive at, not a hub you navigate out of.
|
|
//
|
|
// No stickyChrome: unlike Browse/Gallery/Settings this view has no sticky
|
|
// sub-header for the nav to butt against, so the nav keeps its normal fade.
|
|
// `props` turns on the ingestion-status ribbon (#387 B3). Only here: inside
|
|
// Browse's Posts tab the same view renders without it.
|
|
{ path: '/latest', name: 'latest', component: PostsView, props: { statusRibbon: true },
|
|
meta: { title: 'Latest', navOrder: 5 } },
|
|
|
|
// FC-2: image backbone
|
|
{ path: '/showcase', name: 'showcase', component: ShowcaseView, meta: { title: 'Showcase', navOrder: 10 } },
|
|
{ path: '/gallery', name: 'gallery', component: GalleryView, meta: { title: 'Gallery', navOrder: 20, stickyChrome: true } },
|
|
// Explore: a 3-pane tagging workspace — walk an image's visual neighbours
|
|
// (left) while tagging the focused image (center viewer + modal-parity tag
|
|
// rail). Optional anchor param — the bare /explore nav entry SEEDS a random
|
|
// image so the tab kick-starts a rabbit hole. navOrder pins it after Gallery.
|
|
{ path: '/explore/:imageId?', name: 'explore', component: ExploreView, meta: { title: 'Explore', navOrder: 25 } },
|
|
// Browse hub (operator-asked 2026-06-09): Posts / Artists / Tags as tabs —
|
|
// the three "browse the library by an axis" surfaces. One nav entry; the old
|
|
// standalone paths redirect into the matching tab (below).
|
|
{ path: '/browse', name: 'browse', component: BrowseView, meta: { title: 'Browse', navOrder: 30, stickyChrome: true } },
|
|
// Artist detail — no meta.title (reached by clicking an artist, not nav).
|
|
{ path: '/artist/:slug', name: 'artist', component: ArtistView },
|
|
// Series browse — a nav entry (meta.title).
|
|
{ path: '/series', name: 'series', component: SeriesView, meta: { title: 'Series', navOrder: 40, stickyChrome: true } },
|
|
// Series management — no meta.title (reached from a series card/tag).
|
|
{ path: '/series/:tagId', name: 'series-manage', component: SeriesManageView },
|
|
// Series reader — immersive (no top nav, no meta.title).
|
|
{ path: '/series/:tagId/read', name: 'series-read', component: SeriesReaderView, meta: { immersive: true } },
|
|
|
|
// FC-3: subscription backbone — purely management (sources/downloads),
|
|
// distinct from the Browse hub.
|
|
{ path: '/subscriptions', name: 'subscriptions', component: SubscriptionsView, meta: { title: 'Subscriptions', navOrder: 50, stickyChrome: true } },
|
|
|
|
// Settings — config, pinned to the right of the nav (TopNav special-cases it).
|
|
{ path: '/settings', name: 'settings', component: SettingsView, meta: { title: 'Settings', stickyChrome: true } },
|
|
// System health is a Settings TAB, not a route of its own (operator
|
|
// 2026-09-02). It first shipped as /system reachable only from the health
|
|
// dot, which is a target you have to already suspect something to go
|
|
// looking for. Settings is where someone goes to ask the instance about
|
|
// itself. The path stays as a redirect so the dot's old link, and any
|
|
// bookmark from that build, still land somewhere real.
|
|
{ path: '/system', name: 'system', redirect: () => ({ name: 'settings', query: { tab: 'system' } }) },
|
|
|
|
// The old standalone paths now redirect into the Browse hub, preserving any
|
|
// deep-link query (e.g. /posts?post_id=N → /browse?tab=posts&post_id=N). The
|
|
// route NAMES stay so existing { name: 'posts' | 'artists' | 'tags' } links
|
|
// and path pushes keep resolving.
|
|
{ path: '/posts', name: 'posts', redirect: (to) => ({ name: 'browse', query: { ...to.query, tab: 'posts' } }) },
|
|
{ path: '/artists', name: 'artists', redirect: (to) => ({ name: 'browse', query: { ...to.query, tab: 'artists' } }) },
|
|
{ path: '/tags', name: 'tags', redirect: (to) => ({ name: 'browse', query: { ...to.query, tab: 'tags' } }) },
|
|
|
|
// Bookmark/back-button safety net for the routes that got folded into
|
|
// /subscriptions (no meta.title — stay out of TopNav).
|
|
{ path: '/credentials', redirect: '/subscriptions?tab=settings' },
|
|
{ path: '/downloads', redirect: '/subscriptions?tab=downloads' }
|
|
]
|
|
|
|
// Browser uses HTML5 history; non-browser (Vitest/SSR) falls back to memory
|
|
// history so the module is importable without `window`.
|
|
const history =
|
|
typeof window !== 'undefined' ? createWebHistory() : createMemoryHistory()
|
|
|
|
const router = createRouter({
|
|
history,
|
|
routes
|
|
})
|
|
|
|
const DEFAULT_TITLE = 'FabledCurator'
|
|
|
|
// Keep the tab title in sync with the route on EVERY navigation. List routes set
|
|
// it from meta.title; detail routes (artist, series) have no meta.title, so they
|
|
// reset to the default here and then overwrite it with their own dynamic title
|
|
// (e.g. ArtistView sets "<artist> — FabledCurator"). Without this reset the
|
|
// previous view's dynamic title stuck around — an artist name showing on the
|
|
// Showcase tab, operator-flagged 2026-06-07.
|
|
router.afterEach((to) => {
|
|
if (typeof document === 'undefined') return
|
|
document.title = to.meta?.title
|
|
? `${to.meta.title} — ${DEFAULT_TITLE}`
|
|
: DEFAULT_TITLE
|
|
})
|
|
|
|
export default router
|