From f555a1c9dd5d4bfc879c4555becfcc84d30fa992 Mon Sep 17 00:00:00 2001 From: seb Date: Thu, 29 Oct 2026 17:32:39 +0100 Subject: [PATCH] u --- automat-graph.dot | 2 +- automat-graph.svg | 92 ++++++++++++---------- generate-automat-graph.js | 2 + src/api/auth.js | 2 + src/automats/authAutomat.js | 16 +++- src/components/settings/SettingsDialog.jsx | 32 +++++++- 6 files changed, 99 insertions(+), 47 deletions(-) diff --git a/automat-graph.dot b/automat-graph.dot index ac005ca..3db6604 100644 --- a/automat-graph.dot +++ b/automat-graph.dot @@ -9,7 +9,7 @@ digraph automats { label="Session & Real-Time Hub"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; fontcolor="#e2e8f0"; margin=14; - authAutomat [label=<
authAutomat
Always-On Session & Multi-Tenant Gateway
STATEhasAuth: booleanAuthenticated session bit
STATEhasCompany: booleanhasAuth && selectedCompanyId
STATEuser: AuthUserCurrent user & company
STATEstatus'anon' | 'authed' | 'loading'
ACTIONbootstrapSession()Restore or fetch session
ACTIONswitchCompany(id)Multi-tenant switch
ACTIONlogout()Clear session cookie
SELECTselectHasAuth() / selectHasCompany()Auth & tenant gates
APIGET /api/auth/sessionCookie session check
>]; + authAutomat [label=<
authAutomat
Always-On Session & Multi-Tenant Gateway
STATEhasAuth: booleanAuthenticated session bit
STATEhasCompany: booleanhasAuth && selectedCompanyId
STATEuser: AuthUserCurrent user & company
STATEschemaVersion: number|nullBackend DB schema version
STATEstatus'anon' | 'authed' | 'loading'
ACTIONbootstrapSession()Restore or fetch session
ACTIONswitchCompany(id)Multi-tenant switch
ACTIONlogout()Clear session cookie
SELECTselectHasAuth() / selectHasCompany()Auth & tenant gates
SELECTselectSchemaVersion()Active DB schema version
APIGET /api/auth/sessionCookie session check
>]; sseAutomat [label=<
sseAutomat
Single Persistent SSE Connection
STATEstatus'connected' | 'error'
STATEclientId (cid)Tab-scoped UUID in session
ACTIONreconnect()Cycle on company switch
ACTIONon(event, handler)Register signal listener
ACTIONwatchArticle(sku)Attach article-<sku>
SELECTinvalidateArticle(sku)Silent cache refresh
SSEcategory / articlesCatalog mutation events
SSEorder / shipmentFulfillment events
APIGET /api/eventsPersistent stream
>]; } subgraph cluster_company_basedata { diff --git a/automat-graph.svg b/automat-graph.svg index 1defb7e..859546b 100644 --- a/automat-graph.svg +++ b/automat-graph.svg @@ -11,8 +11,8 @@ cluster_session_realtime - -Session & Real-Time Hub + +Session & Real-Time Hub cluster_company_basedata @@ -57,64 +57,76 @@ authAutomat - - -authAutomat -Always-On Session & Multi-Tenant Gateway - + + +authAutomat +Always-On Session & Multi-Tenant Gateway + + + +STATE +hasAuth: boolean +Authenticated session bit + - -STATE -hasAuth: boolean -Authenticated session bit - + +STATE +hasCompany: boolean +hasAuth && selectedCompanyId + - -STATE -hasCompany: boolean -hasAuth && selectedCompanyId - + +STATE +user: AuthUser +Current user & company + - -STATE -user: AuthUser -Current user & company - + +STATE +schemaVersion: number|null +Backend DB schema version + STATE status 'anon' | 'authed' | 'loading' - + ACTION bootstrapSession() Restore or fetch session - + ACTION switchCompany(id) Multi-tenant switch - + ACTION logout() Clear session cookie - + SELECT selectHasAuth() / selectHasCompany() Auth & tenant gates - + - -API -GET /api/auth/session -Cookie session check + +SELECT +selectSchemaVersion() +Active DB schema version + + + +API +GET /api/auth/session +Cookie session check @@ -184,11 +196,11 @@ authAutomat:e->sseAutomat:n - - + + -  hasAuth -> connect SSE   +  hasAuth -> connect SSE   @@ -301,7 +313,7 @@ authAutomat:e->companyAutomat:n - + @@ -379,7 +391,7 @@ authAutomat:w->categoryAutomat:n - + @@ -429,7 +441,7 @@ authAutomat:e->deliveryAutomat:n - + @@ -541,7 +553,7 @@ authAutomat:e->orderAutomat:n - + @@ -585,7 +597,7 @@ authAutomat:e->entityAutomat:n - + @@ -723,7 +735,7 @@ authAutomat:e->vfsAutomat:n - + diff --git a/generate-automat-graph.js b/generate-automat-graph.js index 72081c2..e98e45c 100644 --- a/generate-automat-graph.js +++ b/generate-automat-graph.js @@ -126,11 +126,13 @@ const AUTOMAT_SPECS = [ { type: 'STATE', name: 'hasAuth: boolean', desc: 'Authenticated session bit', port: 'p_auth_hasAuth' }, { type: 'STATE', name: 'hasCompany: boolean', desc: 'hasAuth && selectedCompanyId', port: 'p_auth_hasCompany' }, { type: 'STATE', name: 'user: AuthUser', desc: 'Current user & company', port: 'p_auth_user' }, + { type: 'STATE', name: 'schemaVersion: number|null', desc: 'Backend DB schema version', port: 'p_auth_schemaVersion' }, { type: 'STATE', name: 'status', desc: '\'anon\' | \'authed\' | \'loading\'', port: 'p_auth_status' }, { type: 'ACTION', name: 'bootstrapSession()', desc: 'Restore or fetch session' }, { type: 'ACTION', name: 'switchCompany(id)', desc: 'Multi-tenant switch', port: 'p_auth_switchCompany' }, { type: 'ACTION', name: 'logout()', desc: 'Clear session cookie' }, { type: 'SELECT', name: 'selectHasAuth() / selectHasCompany()', desc: 'Auth & tenant gates' }, + { type: 'SELECT', name: 'selectSchemaVersion()', desc: 'Active DB schema version' }, { type: 'API', name: 'GET /api/auth/session', desc: 'Cookie session check' }, ], }, diff --git a/src/api/auth.js b/src/api/auth.js index 94f4307..72d3605 100644 --- a/src/api/auth.js +++ b/src/api/auth.js @@ -8,12 +8,14 @@ export const auth = { return { user: data.user ?? null, googleClientId: data.googleClientId ?? null, + schemaVersion: data.schemaVersion ?? null, }; } catch (err) { if (err.status === 401 || err.status === 403) { return { user: null, googleClientId: err.data?.googleClientId ?? null, + schemaVersion: err.data?.schemaVersion ?? null, forbidden: err.status === 403, message: err.message, }; diff --git a/src/automats/authAutomat.js b/src/automats/authAutomat.js index edf0a9a..d62deb6 100644 --- a/src/automats/authAutomat.js +++ b/src/automats/authAutomat.js @@ -30,6 +30,7 @@ import { auth, company, number as numberApi } from '../api'; * @typedef {Object} AuthState * @property {AuthUser|null} user Authenticated user, or `null` for anonymous sessions. * @property {string|null} googleClientId Google Identity Services client ID. + * @property {number|null} schemaVersion Database schema version returned by the backend. * @property {'loading'|'anon'|'authed'} status Current authentication lifecycle state. * @property {boolean} hasAuth True when the session has a valid authenticated user. * @property {boolean} hasCompany True when `hasAuth` is true AND a company is selected. @@ -47,6 +48,7 @@ export const AUTH_LOGGED_OUT = 'auth/loggedOut'; const initialState = { user: null, googleClientId: null, + schemaVersion: null, status: 'loading', hasAuth: false, hasCompany: false, @@ -137,22 +139,26 @@ function syncSse(state) { function applySession(nextSession) { const googleClientId = nextSession.googleClientId ?? authAutomat.state.googleClientId; + const schemaVersion = + nextSession.schemaVersion !== undefined + ? nextSession.schemaVersion + : (authAutomat.state.schemaVersion ?? null); if (!nextSession.user) { if (authAutomat.state.status !== 'authed') { const nextState = authAutomat.setState( - buildAuthState(null, 'anon', { googleClientId, retryNotification: null }) + buildAuthState(null, 'anon', { googleClientId, schemaVersion, retryNotification: null }) ); syncSse(nextState); } else { - authAutomat.setState({ googleClientId, retryNotification: null }); + authAutomat.setState({ googleClientId, schemaVersion, retryNotification: null }); } return nextSession; } const user = mergeUser(authAutomat.state.user, nextSession.user); const nextState = authAutomat.setState( - buildAuthState(user, 'authed', { googleClientId, retryNotification: null }) + buildAuthState(user, 'authed', { googleClientId, schemaVersion, retryNotification: null }) ); syncSse(nextState); emit({ type: AUTH_SESSION_ESTABLISHED }); @@ -234,6 +240,7 @@ const actions = { const nextState = authAutomat.setState( buildAuthState(user, 'authed', { googleClientId: session.googleClientId ?? authAutomat.state.googleClientId, + schemaVersion: session.schemaVersion ?? authAutomat.state.schemaVersion ?? null, }) ); syncSse(nextState); @@ -344,6 +351,7 @@ export const selectHasCompany = () => Boolean(authAutomat.state.hasCompany); export const selectSelectedCompanyId = () => authAutomat.state.hasCompany ? authAutomat.state.user?.selectedCompanyId ?? null : null; export const selectRetryNotification = () => authAutomat.state.retryNotification; +export const selectSchemaVersion = () => authAutomat.state.schemaVersion; /** * Re-reads `GET /api/auth/me` from the server. @@ -351,7 +359,7 @@ export const selectRetryNotification = () => authAutomat.state.retryNotification * Called after login, logout, and company switches so the automat reflects * the current server session rather than an optimistic guess. * - * @returns {Promise<{user: AuthUser|null, googleClientId: string|null}>} Session. + * @returns {Promise<{user: AuthUser|null, googleClientId: string|null, schemaVersion?: number|null}>} Session. */ export function bootstrapAuthMe() { return authAutomat.actions.bootstrapSession(); diff --git a/src/components/settings/SettingsDialog.jsx b/src/components/settings/SettingsDialog.jsx index 71c852f..bda0eba 100644 --- a/src/components/settings/SettingsDialog.jsx +++ b/src/components/settings/SettingsDialog.jsx @@ -13,6 +13,8 @@ import PointOfSaleIcon from '@mui/icons-material/PointOfSale'; import ImportExportIcon from '@mui/icons-material/ImportExport'; import CloseIcon from '@mui/icons-material/Close'; import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; +import authAutomat from '../../automats/authAutomat'; import CompanySettingsPanel from './CompanySettingsPanel'; import NumberSettingsPanel from './NumberSettingsPanel'; import PrinterSettingsPanel from './PrinterSettingsPanel'; @@ -54,15 +56,31 @@ const TABS = [ export default class SettingsDialog extends Component { constructor(props) { super(props); - this.state = { tab: TAB_KEYS.health }; + this.state = { + tab: TAB_KEYS.health, + schemaVersion: authAutomat.state.schemaVersion, + }; this.handleTabChange = this.handleTabChange.bind(this); } + componentDidMount() { + this.unsubAuth = authAutomat.subscribe(this, ['schemaVersion']); + } + + componentWillUnmount() { + this.unsubAuth?.(); + } + componentDidUpdate(prevProps) { const { open } = this.props; if (!open && prevProps.open) { this.setState({ tab: TAB_KEYS.health }); } + if (open && !prevProps.open) { + if (this.state.schemaVersion !== authAutomat.state.schemaVersion) { + this.setState({ schemaVersion: authAutomat.state.schemaVersion }); + } + } } handleTabChange(_event, value) { @@ -71,7 +89,7 @@ export default class SettingsDialog extends Component { render() { const { open, onClose } = this.props; - const { tab } = this.state; + const { tab, schemaVersion } = this.state; const paperSx = { height: '85vh', @@ -120,6 +138,9 @@ export default class SettingsDialog extends Component { borderColor: 'divider', bgcolor: SURFACE_BASE, py: 2, + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', overflowY: 'auto', }} > @@ -172,6 +193,13 @@ export default class SettingsDialog extends Component { /> ))} + {schemaVersion != null && ( + + + DB-Version: {schemaVersion} + + + )} {/* Right content panel */}