Compare commits
4 Commits
de51dde6e6
..
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
adf8fed44f
|
|||
|
c2bfa5ad82
|
|||
|
2e07e16b45
|
|||
|
dd34da4d67
|
+5
-1
@@ -1,6 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
## [v0.1.0] — 2026-06-12
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
- extract Server type with named handler methods
|
||||
- split Site into SiteConfig + SiteState
|
||||
- unify logging with log/slog
|
||||
- restructure site form to 2 type-aware pages
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -176,4 +177,7 @@
|
||||
- remove tagged scan image in cleanup step
|
||||
- exclude rc tags from cliff tag_pattern so launch notes span full history
|
||||
- fall back to embedded build info when ldflags absent
|
||||
- drop body-grep Security grouping, map polish type in cliff
|
||||
- sync selectedID on click so refreshLive doesn't revert cursor
|
||||
- resolve 4 tag-blocking issues for v0.1.0
|
||||
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
## Development
|
||||
|
||||
```sh
|
||||
go run cmd/uptop/main.go -demo # starts with sample data
|
||||
go run ./cmd/uptop -demo # starts with sample data
|
||||
ssh -p 23234 localhost # connect to TUI
|
||||
```
|
||||
|
||||
|
||||
@@ -53,14 +53,14 @@ Canonical repo: [gitea.lerkolabs.com/lerkolabs/uptop](https://gitea.lerkolabs.co
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
go run ./cmd/uptop
|
||||
UPTOP_ADMIN_KEY="$(cat ~/.ssh/id_ed25519.pub)" go run ./cmd/uptop
|
||||
ssh -p 23234 localhost
|
||||
```
|
||||
|
||||
Want some data to look at first:
|
||||
|
||||
```bash
|
||||
go run ./cmd/uptop -demo
|
||||
UPTOP_ADMIN_KEY="$(cat ~/.ssh/id_ed25519.pub)" go run ./cmd/uptop -demo
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
@@ -3,7 +3,7 @@ services:
|
||||
# LEADER NODE
|
||||
# -------------------------
|
||||
leader:
|
||||
build: .
|
||||
image: lerkolabs/uptop:latest
|
||||
container_name: uptop-leader
|
||||
ports:
|
||||
- "23234:23234" # SSH
|
||||
@@ -38,7 +38,7 @@ services:
|
||||
# FOLLOWER NODE
|
||||
# -------------------------
|
||||
follower:
|
||||
build: .
|
||||
image: lerkolabs/uptop:latest
|
||||
container_name: uptop-follower
|
||||
ports:
|
||||
- "23233:23234" # SSH (Mapped to different host port)
|
||||
|
||||
@@ -2,7 +2,7 @@ services:
|
||||
# The Application
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
context: ..
|
||||
dockerfile: Dockerfile
|
||||
container_name: uptop-dev
|
||||
ports:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
leader:
|
||||
build: .
|
||||
image: lerkolabs/uptop:latest
|
||||
environment:
|
||||
- UPTOP_CLUSTER_MODE=leader
|
||||
- UPTOP_CLUSTER_SECRET=changeme # EXAMPLE ONLY — rotate before use
|
||||
@@ -11,7 +11,7 @@ services:
|
||||
- "23234:23234"
|
||||
|
||||
probe-us-east:
|
||||
build: .
|
||||
image: lerkolabs/uptop:latest
|
||||
environment:
|
||||
- UPTOP_CLUSTER_MODE=probe
|
||||
- UPTOP_NODE_ID=us-east-1
|
||||
@@ -23,7 +23,7 @@ services:
|
||||
- leader
|
||||
|
||||
probe-eu-west:
|
||||
build: .
|
||||
image: lerkolabs/uptop:latest
|
||||
environment:
|
||||
- UPTOP_CLUSTER_MODE=probe
|
||||
- UPTOP_NODE_ID=eu-west-1
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: lerkolabs/uptop:latest
|
||||
container_name: uptop
|
||||
restart: unless-stopped
|
||||
read_only: true
|
||||
|
||||
@@ -54,6 +54,7 @@ func Apply(ctx context.Context, s store.Store, f *File, opts ApplyOpts) ([]Chang
|
||||
alertMap[ea.Name] = ea.ID
|
||||
}
|
||||
|
||||
nextPlaceholderID := -1
|
||||
desiredAlertNames := make(map[string]bool, len(f.Alerts))
|
||||
for _, a := range f.Alerts {
|
||||
desiredAlertNames[a.Name] = true
|
||||
@@ -66,6 +67,9 @@ func Apply(ctx context.Context, s store.Store, f *File, opts ApplyOpts) ([]Chang
|
||||
return changes, fmt.Errorf("create alert %q: %w", a.Name, err)
|
||||
}
|
||||
alertMap[a.Name] = id
|
||||
} else {
|
||||
alertMap[a.Name] = nextPlaceholderID
|
||||
nextPlaceholderID--
|
||||
}
|
||||
} else {
|
||||
alertMap[a.Name] = existing.ID
|
||||
@@ -109,6 +113,9 @@ func Apply(ctx context.Context, s store.Store, f *File, opts ApplyOpts) ([]Chang
|
||||
return changes, fmt.Errorf("create group %q: %w", g.Name, err)
|
||||
}
|
||||
groupMap[g.Name] = id
|
||||
} else {
|
||||
groupMap[g.Name] = nextPlaceholderID
|
||||
nextPlaceholderID--
|
||||
}
|
||||
} else {
|
||||
groupMap[g.Name] = existing.ID
|
||||
|
||||
@@ -266,6 +266,74 @@ func TestApplyDuplicateNames(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyDryRunNewAlertAndMonitor(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
f := &File{
|
||||
Alerts: []Alert{
|
||||
{Name: "Discord", Type: "discord", Settings: map[string]string{"url": "https://example.com"}},
|
||||
},
|
||||
Monitors: []Monitor{
|
||||
{Name: "Web", Type: "http", URL: "https://example.com", Interval: 30, Alert: "Discord"},
|
||||
},
|
||||
}
|
||||
|
||||
changes, err := Apply(context.Background(), s, f, ApplyOpts{DryRun: true})
|
||||
if err != nil {
|
||||
t.Fatalf("dry-run with new alert+monitor should not error: %v", err)
|
||||
}
|
||||
|
||||
creates := 0
|
||||
for _, c := range changes {
|
||||
if c.Action == "create" {
|
||||
creates++
|
||||
}
|
||||
}
|
||||
if creates != 2 {
|
||||
t.Fatalf("expected 2 creates (alert+monitor), got %d: %+v", creates, changes)
|
||||
}
|
||||
|
||||
sites, _ := s.GetSites(context.Background())
|
||||
alerts, _ := s.GetAllAlerts(context.Background())
|
||||
if len(sites) != 0 {
|
||||
t.Fatalf("dry-run should not persist sites, got %d", len(sites))
|
||||
}
|
||||
if len(alerts) != 0 {
|
||||
t.Fatalf("dry-run should not persist alerts, got %d", len(alerts))
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyDryRunNewGroupWithChildren(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
f := &File{
|
||||
Alerts: []Alert{
|
||||
{Name: "Slack", Type: "slack", Settings: map[string]string{"url": "https://hooks.example.com"}},
|
||||
},
|
||||
Monitors: []Monitor{
|
||||
{
|
||||
Name: "Prod", Type: "group", Alert: "Slack",
|
||||
Monitors: []Monitor{
|
||||
{Name: "API", Type: "http", URL: "https://api.example.com", Interval: 15, Alert: "Slack"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
changes, err := Apply(context.Background(), s, f, ApplyOpts{DryRun: true})
|
||||
if err != nil {
|
||||
t.Fatalf("dry-run with new group+alert should not error: %v", err)
|
||||
}
|
||||
|
||||
creates := 0
|
||||
for _, c := range changes {
|
||||
if c.Action == "create" {
|
||||
creates++
|
||||
}
|
||||
}
|
||||
if creates != 3 {
|
||||
t.Fatalf("expected 3 creates (alert+group+child), got %d: %+v", creates, changes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyExistingAlertReference(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
s.AddAlert(context.Background(), "Existing", "webhook", map[string]string{"url": "https://example.com"})
|
||||
|
||||
+163
-138
@@ -326,101 +326,104 @@ func (m *Model) initSiteHuhForm() tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
// m.alerts is the tab-data cache (≤5s stale) — no store IO in Update.
|
||||
alertOpts := []huh.Option[string]{huh.NewOption("None", "0")}
|
||||
return m.rebuildSiteForm()
|
||||
}
|
||||
|
||||
func (m *Model) rebuildSiteForm() tea.Cmd {
|
||||
groups := m.buildSiteFormGroups()
|
||||
m.huhForm = huh.NewForm(groups...).WithTheme(m.theme.HuhTheme())
|
||||
if m.termWidth > 0 {
|
||||
m.huhForm.WithWidth(m.termWidth)
|
||||
}
|
||||
formHeight := m.termHeight - 7
|
||||
if formHeight < 5 {
|
||||
formHeight = 5
|
||||
}
|
||||
m.huhForm.WithHeight(formHeight)
|
||||
m.lastSiteType = m.siteFormData.SiteType
|
||||
return m.huhForm.Init()
|
||||
}
|
||||
|
||||
func (m *Model) siteFormOptions() (alertOpts, groupOpts []huh.Option[string]) {
|
||||
alertOpts = []huh.Option[string]{huh.NewOption("None", "0")}
|
||||
for _, a := range m.alerts {
|
||||
alertOpts = append(alertOpts, huh.NewOption(
|
||||
fmt.Sprintf("%s (%s)", a.Name, a.Type),
|
||||
strconv.Itoa(a.ID),
|
||||
))
|
||||
}
|
||||
|
||||
groupOpts := []huh.Option[string]{huh.NewOption("None", "0")}
|
||||
groupOpts = []huh.Option[string]{huh.NewOption("None", "0")}
|
||||
for _, s := range m.sites {
|
||||
if s.Type == "group" && s.ID != m.editID {
|
||||
groupOpts = append(groupOpts, huh.NewOption(s.Name, strconv.Itoa(s.ID)))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
m.huhForm = huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().Title("Monitor Name").
|
||||
Placeholder("My Service").
|
||||
Value(&m.siteFormData.Name).
|
||||
Validate(func(s string) error {
|
||||
if s == "" {
|
||||
return fmt.Errorf("name is required")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
huh.NewSelect[string]().Title("Monitor Type").
|
||||
Options(
|
||||
huh.NewOption("HTTP/HTTPS", "http"),
|
||||
huh.NewOption("Push / Heartbeat", "push"),
|
||||
huh.NewOption("Ping (ICMP)", "ping"),
|
||||
huh.NewOption("TCP Port", "port"),
|
||||
huh.NewOption("DNS", "dns"),
|
||||
huh.NewOption("Group", "group"),
|
||||
).Value(&m.siteFormData.SiteType),
|
||||
huh.NewSelect[string]().Title("Alert Channel").
|
||||
Options(alertOpts...).
|
||||
Value(&m.siteFormData.AlertID),
|
||||
).Title("Monitor Settings"),
|
||||
huh.NewGroup(
|
||||
huh.NewInput().Title("URL").
|
||||
Placeholder("https://example.com").
|
||||
Description("Required for HTTP monitors").
|
||||
Value(&m.siteFormData.URL).
|
||||
Validate(func(s string) error {
|
||||
if m.siteFormData.SiteType != "http" {
|
||||
return nil
|
||||
}
|
||||
if s == "" {
|
||||
return fmt.Errorf("URL is required for HTTP monitors")
|
||||
}
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid URL")
|
||||
}
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return fmt.Errorf("URL must start with http:// or https://")
|
||||
}
|
||||
if u.Host == "" {
|
||||
return fmt.Errorf("URL must include a host")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
huh.NewInput().Title("Check Interval (seconds)").
|
||||
Placeholder("60").
|
||||
Value(&m.siteFormData.Interval).
|
||||
Validate(func(s string) error {
|
||||
if m.siteFormData.SiteType == "group" {
|
||||
return nil
|
||||
}
|
||||
v, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("must be a number")
|
||||
}
|
||||
if v < 5 {
|
||||
return fmt.Errorf("minimum interval is 5 seconds")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
huh.NewSelect[string]().Title("Parent Group").
|
||||
Options(groupOpts...).
|
||||
Value(&m.siteFormData.GroupID),
|
||||
func (m *Model) buildSiteFormGroups() []*huh.Group {
|
||||
d := m.siteFormData
|
||||
alertOpts, groupOpts := m.siteFormOptions()
|
||||
|
||||
// Page 1 — Monitor Setup: core fields + type-specific target
|
||||
setup := []huh.Field{
|
||||
huh.NewInput().Title("Monitor Name").
|
||||
Placeholder("My Service").
|
||||
Value(&d.Name).
|
||||
Validate(func(s string) error {
|
||||
if s == "" {
|
||||
return fmt.Errorf("name is required")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
huh.NewSelect[string]().Title("Monitor Type").
|
||||
Options(
|
||||
huh.NewOption("HTTP/HTTPS", "http"),
|
||||
huh.NewOption("Push / Heartbeat", "push"),
|
||||
huh.NewOption("Ping (ICMP)", "ping"),
|
||||
huh.NewOption("TCP Port", "port"),
|
||||
huh.NewOption("DNS", "dns"),
|
||||
huh.NewOption("Group", "group"),
|
||||
).Value(&d.SiteType),
|
||||
huh.NewSelect[string]().Title("Alert Channel").
|
||||
Options(alertOpts...).
|
||||
Value(&d.AlertID),
|
||||
}
|
||||
|
||||
switch d.SiteType {
|
||||
case "http":
|
||||
setup = append(setup, huh.NewInput().Title("URL").
|
||||
Placeholder("https://example.com").
|
||||
Value(&d.URL).
|
||||
Validate(func(s string) error {
|
||||
if s == "" {
|
||||
return fmt.Errorf("URL is required")
|
||||
}
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid URL")
|
||||
}
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return fmt.Errorf("URL must start with http:// or https://")
|
||||
}
|
||||
if u.Host == "" {
|
||||
return fmt.Errorf("URL must include a host")
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
case "ping", "dns":
|
||||
setup = append(setup, huh.NewInput().Title("Hostname / IP").
|
||||
Placeholder("10.0.0.1").
|
||||
Value(&d.Hostname))
|
||||
case "port":
|
||||
setup = append(setup,
|
||||
huh.NewInput().Title("Hostname / IP").
|
||||
Placeholder("10.0.0.1").
|
||||
Description("Target for ping/port/DNS monitors").
|
||||
Value(&m.siteFormData.Hostname),
|
||||
Value(&d.Hostname),
|
||||
huh.NewInput().Title("Port").
|
||||
Placeholder("0").
|
||||
Description("Target port for TCP port monitors").
|
||||
Value(&m.siteFormData.Port).
|
||||
Placeholder("443").
|
||||
Value(&d.Port).
|
||||
Validate(func(s string) error {
|
||||
if m.siteFormData.SiteType != "port" {
|
||||
return nil
|
||||
}
|
||||
v, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("must be a number")
|
||||
@@ -429,34 +432,20 @@ func (m *Model) initSiteHuhForm() tea.Cmd {
|
||||
return fmt.Errorf("port must be 1-65535")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
huh.NewInput().Title("Timeout (seconds)").
|
||||
Placeholder("5").
|
||||
Value(&m.siteFormData.Timeout).
|
||||
Validate(func(s string) error {
|
||||
if m.siteFormData.SiteType == "group" {
|
||||
return nil
|
||||
}
|
||||
v, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("must be a number")
|
||||
}
|
||||
if v < 1 || v > 300 {
|
||||
return fmt.Errorf("timeout must be 1-300 seconds")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
huh.NewInput().Title("Description").
|
||||
Placeholder("Optional description").
|
||||
Value(&m.siteFormData.Description),
|
||||
huh.NewInput().Title("Probe Regions").
|
||||
Placeholder("us-east, eu-west (empty = all)").
|
||||
Description("Comma-separated regions for distributed probing").
|
||||
Value(&m.siteFormData.Regions),
|
||||
).Title("Connection").WithHideFunc(func() bool {
|
||||
return m.siteFormData.SiteType == "group"
|
||||
}),
|
||||
huh.NewGroup(
|
||||
}))
|
||||
}
|
||||
|
||||
groups := []*huh.Group{huh.NewGroup(setup...).Title("Monitor Setup")}
|
||||
|
||||
if d.SiteType == "group" {
|
||||
return groups
|
||||
}
|
||||
|
||||
// Page 2 — Configuration: type-specific options + shared defaults
|
||||
var config []huh.Field
|
||||
|
||||
if d.SiteType == "http" {
|
||||
config = append(config,
|
||||
huh.NewSelect[string]().Title("HTTP Method").
|
||||
Options(
|
||||
huh.NewOption("GET", "GET"),
|
||||
@@ -466,22 +455,75 @@ func (m *Model) initSiteHuhForm() tea.Cmd {
|
||||
huh.NewOption("DELETE", "DELETE"),
|
||||
huh.NewOption("HEAD", "HEAD"),
|
||||
huh.NewOption("OPTIONS", "OPTIONS"),
|
||||
).Value(&m.siteFormData.Method),
|
||||
).Value(&d.Method),
|
||||
huh.NewInput().Title("Accepted Status Codes").
|
||||
Placeholder("200-299").
|
||||
Description("Ranges (200-299) and singles (301) separated by commas").
|
||||
Value(&m.siteFormData.AcceptedCodes),
|
||||
).Title("HTTP Settings").WithHideFunc(func() bool {
|
||||
return m.siteFormData.SiteType != "http"
|
||||
}),
|
||||
huh.NewGroup(
|
||||
Value(&d.AcceptedCodes),
|
||||
)
|
||||
}
|
||||
|
||||
config = append(config,
|
||||
huh.NewInput().Title("Check Interval (seconds)").
|
||||
Placeholder("60").
|
||||
Value(&d.Interval).
|
||||
Validate(func(s string) error {
|
||||
v, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("must be a number")
|
||||
}
|
||||
if v < 5 {
|
||||
return fmt.Errorf("minimum interval is 5 seconds")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
huh.NewInput().Title("Timeout (seconds)").
|
||||
Placeholder("5").
|
||||
Value(&d.Timeout).
|
||||
Validate(func(s string) error {
|
||||
v, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("must be a number")
|
||||
}
|
||||
if v < 1 || v > 300 {
|
||||
return fmt.Errorf("timeout must be 1-300 seconds")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
huh.NewInput().Title("Max Retries Before Alert").
|
||||
Placeholder("0").
|
||||
Value(&d.Retries).
|
||||
Validate(func(s string) error {
|
||||
v, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("must be a number")
|
||||
}
|
||||
if v < 0 {
|
||||
return fmt.Errorf("retries cannot be negative")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
huh.NewSelect[string]().Title("Parent Group").
|
||||
Options(groupOpts...).
|
||||
Value(&d.GroupID),
|
||||
huh.NewInput().Title("Description").
|
||||
Placeholder("Optional description").
|
||||
Value(&d.Description),
|
||||
huh.NewInput().Title("Probe Regions").
|
||||
Placeholder("us-east, eu-west (empty = all)").
|
||||
Description("Comma-separated regions for distributed probing").
|
||||
Value(&d.Regions),
|
||||
)
|
||||
|
||||
if d.SiteType == "http" {
|
||||
config = append(config,
|
||||
huh.NewConfirm().Title("Monitor SSL Certificate?").
|
||||
Value(&m.siteFormData.CheckSSL),
|
||||
Value(&d.CheckSSL),
|
||||
huh.NewInput().Title("SSL Warning Threshold (days)").
|
||||
Placeholder("7").
|
||||
Value(&m.siteFormData.Threshold).
|
||||
Value(&d.Threshold).
|
||||
Validate(func(s string) error {
|
||||
if !m.siteFormData.CheckSSL {
|
||||
if !d.CheckSSL {
|
||||
return nil
|
||||
}
|
||||
v, err := strconv.Atoi(s)
|
||||
@@ -493,30 +535,13 @@ func (m *Model) initSiteHuhForm() tea.Cmd {
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
huh.NewInput().Title("Max Retries Before Alert").
|
||||
Placeholder("0").
|
||||
Value(&m.siteFormData.Retries).
|
||||
Validate(func(s string) error {
|
||||
if m.siteFormData.SiteType == "group" {
|
||||
return nil
|
||||
}
|
||||
v, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("must be a number")
|
||||
}
|
||||
if v < 0 {
|
||||
return fmt.Errorf("retries cannot be negative")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
huh.NewConfirm().Title("Ignore TLS Errors?").
|
||||
Value(&m.siteFormData.IgnoreTLS),
|
||||
).Title("Advanced").WithHideFunc(func() bool {
|
||||
return m.siteFormData.SiteType == "group"
|
||||
}),
|
||||
).WithTheme(m.theme.HuhTheme())
|
||||
Value(&d.IgnoreTLS),
|
||||
)
|
||||
}
|
||||
|
||||
return m.huhForm.Init()
|
||||
groups = append(groups, huh.NewGroup(config...).Title("Configuration"))
|
||||
return groups
|
||||
}
|
||||
|
||||
func (m *Model) submitSiteForm() tea.Cmd {
|
||||
|
||||
@@ -115,6 +115,7 @@ type Model struct {
|
||||
|
||||
huhForm *huh.Form
|
||||
siteFormData *siteFormData
|
||||
lastSiteType string
|
||||
alertFormData *alertFormData
|
||||
userFormData *userFormData
|
||||
maintFormData *maintFormData
|
||||
|
||||
@@ -128,6 +128,13 @@ func (m *Model) handleFormMsg(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if f, ok := form.(*huh.Form); ok {
|
||||
m.huhForm = f
|
||||
}
|
||||
if m.state == stateFormSite && m.siteFormData != nil &&
|
||||
m.siteFormData.SiteType != m.lastSiteType {
|
||||
rebuildCmd := m.rebuildSiteForm()
|
||||
// Advance to Type select — user just changed it.
|
||||
skipName := m.huhForm.NextField()
|
||||
return m, tea.Batch(rebuildCmd, skipName)
|
||||
}
|
||||
if m.huhForm.State == huh.StateCompleted {
|
||||
// The store write runs in the returned Cmd; its writeDoneMsg
|
||||
// triggers the tab-data reload once the row actually exists.
|
||||
@@ -727,6 +734,7 @@ func (m *Model) handleClick(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
|
||||
for i := m.tableOffset; i < end; i++ {
|
||||
if m.zones.Get(fmt.Sprintf("%s-%d", prefix, i)).InBounds(msg) {
|
||||
m.cursor = i
|
||||
m.syncSelectedID()
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user