Expose the existing bee-install script through the web UI: - platform/install.go: remove USB exclusion, add SizeBytes/MountedParts fields, add MinInstallBytes()/DiskWarnings() safety checks (size, mounted partitions, toram+low-RAM warning) - webui: add GET /api/install/disks, POST /api/install/run, GET /api/install/stream endpoints - webui: add Install to Disk page with disk table, warning badges, device-name confirmation gate, SSE progress terminal, reboot button Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
92 lines
1.8 KiB
Go
92 lines
1.8 KiB
Go
package webui
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// jobState holds the output lines and completion status of an async job.
|
|
type jobState struct {
|
|
lines []string
|
|
done bool
|
|
err string
|
|
mu sync.Mutex
|
|
// subs is a list of channels that receive new lines as they arrive.
|
|
subs []chan string
|
|
}
|
|
|
|
func (j *jobState) append(line string) {
|
|
j.mu.Lock()
|
|
defer j.mu.Unlock()
|
|
j.lines = append(j.lines, line)
|
|
for _, ch := range j.subs {
|
|
select {
|
|
case ch <- line:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
func (j *jobState) finish(errMsg string) {
|
|
j.mu.Lock()
|
|
defer j.mu.Unlock()
|
|
j.done = true
|
|
j.err = errMsg
|
|
for _, ch := range j.subs {
|
|
close(ch)
|
|
}
|
|
j.subs = nil
|
|
}
|
|
|
|
// subscribe returns a channel that receives all future lines.
|
|
// Existing lines are returned first, then the channel streams new ones.
|
|
func (j *jobState) subscribe() ([]string, <-chan string) {
|
|
j.mu.Lock()
|
|
defer j.mu.Unlock()
|
|
existing := make([]string, len(j.lines))
|
|
copy(existing, j.lines)
|
|
if j.done {
|
|
return existing, nil
|
|
}
|
|
ch := make(chan string, 256)
|
|
j.subs = append(j.subs, ch)
|
|
return existing, ch
|
|
}
|
|
|
|
// jobManager manages async jobs identified by string IDs.
|
|
type jobManager struct {
|
|
mu sync.Mutex
|
|
jobs map[string]*jobState
|
|
}
|
|
|
|
var globalJobs = &jobManager{jobs: make(map[string]*jobState)}
|
|
|
|
func (m *jobManager) create(id string) *jobState {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
j := &jobState{}
|
|
m.jobs[id] = j
|
|
// Schedule cleanup after 30 minutes
|
|
go func() {
|
|
time.Sleep(30 * time.Minute)
|
|
m.mu.Lock()
|
|
delete(m.jobs, id)
|
|
m.mu.Unlock()
|
|
}()
|
|
return j
|
|
}
|
|
|
|
// isDone returns true if the job has finished (either successfully or with error).
|
|
func (j *jobState) isDone() bool {
|
|
j.mu.Lock()
|
|
defer j.mu.Unlock()
|
|
return j.done
|
|
}
|
|
|
|
func (m *jobManager) get(id string) (*jobState, bool) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
j, ok := m.jobs[id]
|
|
return j, ok
|
|
}
|