feat(settings): the instance reports which build it is (318 step 6)
CI / lint (push) Successful in 4s
Build images / sign-extension (push) Successful in 4s
CI / extension-version (push) Successful in 4s
Build images / build-agent (push) Successful in 4s
CI / frontend-build (push) Successful in 21s
extension / lint (push) Successful in 25s
CI / backend-lint-and-test (push) Successful in 32s
Build images / build-ml (push) Successful in 2m50s
Build images / build-web (push) Successful in 2m49s
CI / integration (push) Successful in 3m52s
CI / lint (push) Successful in 4s
Build images / sign-extension (push) Successful in 4s
CI / extension-version (push) Successful in 4s
Build images / build-agent (push) Successful in 4s
CI / frontend-build (push) Successful in 21s
extension / lint (push) Successful in 25s
CI / backend-lint-and-test (push) Successful in 32s
Build images / build-ml (push) Successful in 2m50s
Build images / build-web (push) Successful in 2m49s
CI / integration (push) Successful in 3m52s
A dim line at the foot of Settings: `FabledCurator 2026.08.28.1249 · dev`. This is no longer a convenience. Milestone 318 stopped publishing version image tags, so an instance's own report is the ONLY answer to "which build is this?" — there is no registry name left to check it against. Note #3127 §5 says it directly: a wrong answer here has no second source to contradict it. Three states, kept distinct because collapsing any two of them lies: not asked yet render nothing asked, no version render "unknown" asked, has a version render it A blank footer reads as "no version", which is a different claim from "I cannot say". And a failed health call deliberately does NOT mark the build loaded — a network blip says nothing about the image, and presenting it as "unknown" would look like a defective build. Carried on /api/health rather than a new route: it answers at the same cost (two module constants, no I/O) and TopNav already fetches it app-wide, so a separate endpoint would mean a second request for two strings. Both fields are OMITTED when unset rather than sent empty. Absence already means "cannot say" — an image predating the field says exactly that by not having the key — so a second spelling would make every reader special-case it. The pre-existing test asserting the body is EXACTLY {"status": "ok"} is what keeps a well-meaning `or ""` default from creeping in. FC_CHANNEL now has one definition. It was read from the environment in extension.py and would have been read again here; the new build_info module holds both, and extension.py binds it as a module-level name so existing tests monkeypatch it exactly as before. Separate from config.py on purpose: those are operator settings meant to be changed, these describe the artifact. Channel sits beside the version, never inside it (rule 149), asserted from both ends. A `-dev` suffix would read as a 0 segment to the extension's parseInt comparator and make every dev build compare equal — #2993 exactly. Not hidden, per the operator and §7: the JS bundle and asset hashes fingerprint the build anyway, and "I'm on 2026.08.28.1249" is the single most useful line in a bug report.
This commit is contained in:
@@ -5,6 +5,18 @@ import { useApi } from '../composables/useApi.js'
|
||||
export const useSystemStore = defineStore('system', () => {
|
||||
const api = useApi()
|
||||
const healthy = ref(null) // null=unknown, true=ok, false=down
|
||||
// What the instance says it is. Since milestone 318 stopped publishing
|
||||
// version image tags, this is the only answer to "which build is this?" —
|
||||
// there is no registry name left to check it against.
|
||||
//
|
||||
// Three states, and collapsing any two of them would lie:
|
||||
// buildLoaded=false we have not asked yet -> render nothing
|
||||
// buildLoaded=true, version='' the build cannot say -> render "unknown"
|
||||
// buildLoaded=true, version=x this build is x
|
||||
// A blank footer would read as "no version", which is a different claim.
|
||||
const buildVersion = ref('')
|
||||
const buildChannel = ref('')
|
||||
const buildLoaded = ref(false)
|
||||
const stats = ref(null)
|
||||
const statsLoading = ref(false)
|
||||
|
||||
@@ -12,8 +24,17 @@ export const useSystemStore = defineStore('system', () => {
|
||||
try {
|
||||
const body = await api.get('/api/health')
|
||||
healthy.value = body.status === 'ok'
|
||||
// Absent means "cannot say" — the server omits these rather than
|
||||
// sending empty strings, so `?? ''` preserves that rather than
|
||||
// inventing a value for it.
|
||||
buildVersion.value = body.version ?? ''
|
||||
buildChannel.value = body.channel ?? ''
|
||||
buildLoaded.value = true
|
||||
} catch {
|
||||
healthy.value = false
|
||||
// Deliberately NOT setting buildLoaded: a failed health call tells us
|
||||
// nothing about the build, and claiming "unknown" would present a
|
||||
// network blip as a defective image.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,5 +47,8 @@ export const useSystemStore = defineStore('system', () => {
|
||||
}
|
||||
}
|
||||
|
||||
return { healthy, stats, statsLoading, refreshHealth, refreshStats }
|
||||
return {
|
||||
healthy, stats, statsLoading, refreshHealth, refreshStats,
|
||||
buildVersion, buildChannel, buildLoaded,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -54,6 +54,21 @@
|
||||
<MaintenancePanel />
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
|
||||
<!-- Which build is this? With no version image tags (milestone 318) the
|
||||
instance's own report is the only answer, so it is shown rather than
|
||||
hidden. The instinct to treat it as information disclosure does not
|
||||
survive contact: the JS bundle and asset hashes fingerprint the build
|
||||
anyway, and "I'm on 2026.08.28.1249" is the single most useful line in
|
||||
a bug report.
|
||||
|
||||
Channel sits BESIDE the version, never inside it (rule 149) — a
|
||||
`-dev` suffix would read as a 0 segment to the extension's comparator
|
||||
and make every dev build compare equal (#2993). -->
|
||||
<div v-if="system.buildLoaded" class="text-caption text-medium-emphasis text-center mt-8">
|
||||
FabledCurator {{ system.buildVersion || 'unknown' }}
|
||||
<span v-if="system.buildChannel"> · {{ system.buildChannel }}</span>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { useSystemStore } from '../src/stores/system.js'
|
||||
|
||||
// Which build am I running? Milestone 318 stopped publishing version image
|
||||
// tags, so the instance's own report is the ONLY answer — there is no registry
|
||||
// name left to check it against. That promotes this from a convenience to the
|
||||
// mechanism, and it means the three states below have to stay distinct: a
|
||||
// wrong answer here has nothing to contradict it.
|
||||
//
|
||||
// not asked yet -> render nothing
|
||||
// asked, no version -> render "unknown"
|
||||
// asked, has a version -> render it
|
||||
//
|
||||
// Collapsing the first two would show "unknown" during every page load, and
|
||||
// collapsing either into a blank would read as "no version", which is a
|
||||
// different and false claim.
|
||||
|
||||
function stubHealth(body, { fail = false } = {}) {
|
||||
globalThis.fetch = vi.fn(async () => {
|
||||
if (fail) throw new Error('network down')
|
||||
return {
|
||||
ok: true, status: 200, statusText: '200',
|
||||
text: async () => JSON.stringify(body),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('system store — build identity', () => {
|
||||
beforeEach(() => setActivePinia(createPinia()))
|
||||
afterEach(() => { vi.restoreAllMocks(); delete globalThis.fetch })
|
||||
|
||||
it('starts having asked nothing, so the footer renders nothing', () => {
|
||||
const s = useSystemStore()
|
||||
expect(s.buildLoaded).toBe(false)
|
||||
})
|
||||
|
||||
it('reports the version and channel the instance claims', async () => {
|
||||
stubHealth({ status: 'ok', version: '2026.08.28.1249', channel: 'dev' })
|
||||
const s = useSystemStore()
|
||||
await s.refreshHealth()
|
||||
|
||||
expect(s.buildLoaded).toBe(true)
|
||||
expect(s.buildVersion).toBe('2026.08.28.1249')
|
||||
expect(s.buildChannel).toBe('dev')
|
||||
})
|
||||
|
||||
it('keeps the channel OUT of the version string', async () => {
|
||||
// The tempting shortcut is a `-dev` suffix. The extension's comparator
|
||||
// parses each dotted segment with parseInt, so a suffixed segment reads as
|
||||
// 0 and every dev build compares equal to every other — #2993 exactly
|
||||
// (rule 149). If anyone ever "simplifies" by folding them together, the
|
||||
// version stops being the bare derived number and this fails.
|
||||
stubHealth({ status: 'ok', version: '2026.08.28.1249', channel: 'dev' })
|
||||
const s = useSystemStore()
|
||||
await s.refreshHealth()
|
||||
|
||||
expect(s.buildVersion).toBe('2026.08.28.1249')
|
||||
expect(s.buildVersion).not.toContain('dev')
|
||||
})
|
||||
|
||||
it('treats an absent version as "cannot say", not as a value', async () => {
|
||||
// A locally-built image, or one predating the field. The server omits the
|
||||
// key rather than sending an empty string; `?? ''` must preserve that
|
||||
// rather than inventing something. The view renders "unknown" from it.
|
||||
stubHealth({ status: 'ok' })
|
||||
const s = useSystemStore()
|
||||
await s.refreshHealth()
|
||||
|
||||
expect(s.buildLoaded).toBe(true)
|
||||
expect(s.buildVersion).toBe('')
|
||||
expect(s.buildChannel).toBe('')
|
||||
})
|
||||
|
||||
it('reports a version with no channel without inventing one', async () => {
|
||||
stubHealth({ status: 'ok', version: '2026.08.28.1249' })
|
||||
const s = useSystemStore()
|
||||
await s.refreshHealth()
|
||||
|
||||
expect(s.buildVersion).toBe('2026.08.28.1249')
|
||||
expect(s.buildChannel).toBe('')
|
||||
})
|
||||
|
||||
it('does not claim "unknown" when the health call itself failed', async () => {
|
||||
// A network blip says nothing about the build. Marking it loaded here
|
||||
// would present a transient failure as a defective image — and since
|
||||
// nothing else names the build, there would be no second source to
|
||||
// correct the impression.
|
||||
stubHealth(null, { fail: true })
|
||||
const s = useSystemStore()
|
||||
await s.refreshHealth()
|
||||
|
||||
expect(s.healthy).toBe(false)
|
||||
expect(s.buildLoaded).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user