The first five minutes, as the product and not as a developer's first run

The first launch was a code box with a link under it, then an empty
Live screen with 'No cameras' in amber in a far corner, then a form
asking for an IP address, and for the first few minutes of all of it
the engine silently downloading 275 MB with nothing on screen but a
stopped-looking status. Walked in a browser with the new mock; nobody
who was not an installer would have got through it.

Now: a welcome that asks the one question a shop owner can answer -
managed from a head office, or on this PC only - with each path in a
sentence; a Getting Started checklist on Live that reads its three steps
from the engine and ticks them itself (recognition ready, camera added
and connected, camera proven by a walk-past), with the one button for
the next step, and that disappears the moment somebody is recognised;
and the model download reported as a percentage in the tray, the
sidebar and the checklist, parsed by the supervisor from the engine's
own progress lines.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
This commit is contained in:
2026-09-21 12:32:19 +05:30
parent 1607f4ce74
commit 81e2c605b9
12 changed files with 261 additions and 79 deletions

View File

@@ -21,6 +21,8 @@ import (
"net/http"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"sync"
"time"
@@ -77,6 +79,7 @@ type Supervisor struct {
restarts int
cancel context.CancelFunc
done chan struct{}
progress Progress
}
func New(opts Options) *Supervisor {
@@ -110,6 +113,37 @@ func (s *Supervisor) Start() {
go s.supervise(ctx, done)
}
// Progress is what the engine is busy with before it answers - on first run,
// downloading ~275 MB of models. Empty once the engine is up.
type Progress struct {
What string `json:"what"`
Percent int `json:"percent"`
}
var progressRe = regexp.MustCompile(`download: (.+?) (\d{1,3})%`)
func (s *Supervisor) noteProgress(line string) {
m := progressRe.FindStringSubmatch(line)
if m == nil {
return
}
pct, _ := strconv.Atoi(m[2])
s.mu.Lock()
if pct >= 100 {
s.progress = Progress{}
} else {
s.progress = Progress{What: m[1], Percent: pct}
}
s.mu.Unlock()
}
// Progress reports the current first-run download, if any.
func (s *Supervisor) Progress() Progress {
s.mu.Lock()
defer s.mu.Unlock()
return s.progress
}
// Stop asks the engine to exit and waits for it.
func (s *Supervisor) Stop() {
s.mu.Lock()
@@ -233,6 +267,7 @@ func (s *Supervisor) runOnce(ctx context.Context) error {
for sc.Scan() {
line := sc.Text()
fmt.Fprintln(s.opts.LogWriter, line)
s.noteProgress(line)
tailMu.Lock()
tail = append(tail, line)
if len(tail) > 12 {
@@ -244,6 +279,9 @@ func (s *Supervisor) runOnce(ctx context.Context) error {
s.setState(Running, nil)
waitErr := cmd.Wait()
s.mu.Lock()
s.progress = Progress{}
s.mu.Unlock()
<-pumped
// A context cancel terminates the child through exec's own handling; the