3 Commits

Author SHA1 Message Date
7c74431fcf Loya says 'sign in again' instead of 'session expired' 2026-09-19 15:29:15 +05:30
01f1c17c7f A claimed PC forgets the old login and the old cameras
Seen on the first claimed demo install: 'session expired' on every
screen, signed in as a user from the previous demo's head office, and
'Watching 3 cameras' for a shop with one - the PC had offered its two
leftover cameras up to head office, without their passwords, so the
same lens was listed twice and one copy could never be pushed anywhere.

Claiming now clears any stored session (a new head office is a new
world), a session whose refresh fails is forgotten on disk as well as
in memory so the app returns to Login by itself, and the demo setup
removes cameras left from an earlier install before it joins the shop,
because head office is the source of truth from then on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
2026-09-19 15:26:23 +05:30
448fba8770 Build the desktop app with the Wails build tags
A plain go build of a Wails app starts, shows 'Wails applications will
not build without the correct build tags' and exits. That is what the
first Windows install of v0.4.4-demo saw. -tags desktop,production is
what wails build passes; both build paths pass it now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
2026-09-19 15:13:43 +05:30
11 changed files with 62 additions and 15 deletions

View File

@@ -136,6 +136,16 @@ func run() error {
// Proving it starts is the point. An installer that reports success and
// leaves a shop with an engine that will not run has done worse than
// failing: the failure surfaces later, to someone who did not install it.
// Joining a shop: head office supplies the cameras, so any left on this PC
// from an earlier install go first. Otherwise the reconciler offers them UP
// to head office - without their passwords, which the engine never returns
// - and the shop ends up with the same lens listed twice, one copy of which
// can never be pushed to another PC. Measured on the first claimed demo.
if bundle != nil && bundle.EnrolCode != "" {
if err := os.Remove(paths.CamerasFile()); err == nil {
step("Earlier cameras", "removed - head office supplies them now")
}
}
if err := smokeTest(vpy, demoCams); err != nil {
return fmt.Errorf("the engine installed but would not start: %w", err)
}
@@ -523,6 +533,7 @@ func claimShop(code, base string) (string, error) {
cfg.AgentToken = b.AgentToken
cfg.CloudBase = base
cfg.Standalone = false
cfg.SessionToken, cfg.SessionRefresh, cfg.SessionEmail = "", "", ""
caPath, err := enrol.SaveCA(b.CACert, paths.BrokerCA())
if err != nil {
return "", err

View File

@@ -119,6 +119,7 @@ func cmdClaim(args []string) error {
cfg.BrokerPassword = b.MQTTPass
cfg.AgentToken = b.AgentToken
cfg.CloudBase = base
cfg.SessionToken, cfg.SessionRefresh, cfg.SessionEmail = "", "", ""
caPath, err := enrol.SaveCA(b.CACert, paths.BrokerCA())
if err != nil {
return err

View File

@@ -70,6 +70,12 @@ func APICredentials() string {
return filepath.Join(StateRoot(), "data", "api_credentials.txt")
}
// CamerasFile is the engine's own camera store. The agent never edits it -
// cameras go through the engine's API so passwords are sealed - but setup
// removes it when a PC joins a shop, because head office is the source of
// truth from then on.
func CamerasFile() string { return filepath.Join(StateRoot(), "data", "cameras.json") }
// EnsureState creates the writable tree. Called before anything opens a file
// under it, so a first run on a fresh machine does not fail on a missing dir.
func EnsureState() error {

View File

@@ -435,6 +435,9 @@ func (a *App) Claim(code string) (SessionInfo, error) {
a.cfg.BrokerPassword = b.MQTTPass
a.cfg.AgentToken = b.AgentToken
a.cfg.CloudBase = a.cloud.Base
// A new head office: whoever was signed in was signed in somewhere else.
a.cfg.SessionToken, a.cfg.SessionRefresh, a.cfg.SessionEmail = "", "", ""
a.cloud.Clear()
caPath, err := enrol.SaveCA(b.CACert, agentpaths.BrokerCA())
if err != nil {
return SessionInfo{}, err

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Behavision</title>
<script type="module" crossorigin src="./assets/index-upywadx9.js"></script>
<script type="module" crossorigin src="./assets/index-C-oYbwC6.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-Z_jL3Bie.css">
</head>
<body>

View File

@@ -40,10 +40,20 @@ export default function App() {
const [helping, setHelping] = useState(false)
useEffect(() => {
(async () => {
try { setSession(await api.session()) } catch { setSession(null) }
setBooting(false)
})()
let alive = true
const load = async () => {
try {
const s = await api.session()
if (alive) setSession(prev => JSON.stringify(prev) === JSON.stringify(s) ? prev : s)
} catch { if (alive) setSession(null) }
if (alive) setBooting(false)
}
load()
// Re-read every few seconds: a session the server has ended - or one
// that never belonged to this head office - must put Login back on
// screen, not leave "session expired" banners on every page.
const id = setInterval(load, 8000)
return () => { alive = false; clearInterval(id) }
}, [])
if (!isDesktop()) {

View File

@@ -36,7 +36,12 @@ export default function Assistant({ session, onClose }) {
const a = await api.ask(history.map(t => ({ role: t.role, text: t.text })))
setTurns([...history, { role: 'assistant', text: a.text, used: a.used ?? [] }])
} catch (e) {
setError(message(e))
const m = message(e)
// Her only failure that is not hers: the login is gone. Say what to do,
// not "session expired" - the shell returns to Login within seconds.
setError(/session expired|unauthori[sz]ed/i.test(m)
? 'You’re signed out of head office, so I can’t look anything up. Sign in again and ask me once more.'
: m)
setTurns(turns) // the question stays in the box, not in the transcript
setDraft(q)
} finally { setBusy(false) }

View File

@@ -108,8 +108,16 @@ func (c *Client) do(ctx context.Context, method, path string, body, out any) err
}
if rerr := c.Refresh(ctx); rerr != nil {
// The refresh token is gone too, so this really is a sign-in, not a
// transient failure. Report it as such so the UI shows the login sheet
// rather than an error dialog.
// transient failure. Forget the session - in memory AND on disk, through
// the same callback that persists rotations - so the app goes back to
// Login instead of showing "session expired" on every screen until
// somebody finds Sign out. Seen on a PC that had been claimed against a
// demo head office and then re-claimed against the real one: the old
// login sat there, dead, for the whole session.
c.Clear()
if c.onRefresh != nil {
c.onRefresh(Session{})
}
return ErrUnauthorized
}
return c.send(ctx, method, path, raw, out)

View File

@@ -71,7 +71,7 @@ Push-Location (Join-Path $root "desktop")
# from brand/loyaly-icon-512.png), and `wails build` would add a second copy
# of both and fail the link with duplicate resources.
$env:CGO_ENABLED = "0"
go build -ldflags "-H windowsgui -X main.version=$Version" -o (Join-Path $root "desktop\build\bin\Behavision.exe") .
go build -tags desktop,production -ldflags "-H windowsgui -X main.version=$Version" -o (Join-Path $root "desktop\build\bin\Behavision.exe") .
if ($LASTEXITCODE -ne 0) { throw "desktop build failed" }
Pop-Location

View File

@@ -32,7 +32,10 @@ fi
step "1. Desktop app (Wails, pure-Go Windows target)"
(cd desktop/frontend && npm run build >/dev/null)
rm -rf "$STAGE" && mkdir -p "$STAGE/engine-src"
(cd desktop && CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath \
# -tags desktop,production is what `wails build` passes; without them the
# binary starts, shows "Wails applications will not build without the correct
# build tags" and exits. Measured on the first Windows install of v0.4.4-demo.
(cd desktop && CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -tags desktop,production \
-ldflags "-H windowsgui -s -w -X main.version=$TAG" -o "../$STAGE/Behavision.exe" .)
step "2. Agent and setup tool"