From 56c525cb26529e065c62bea4fed7e68df1de43b1 Mon Sep 17 00:00:00 2001 From: Jocadbz Date: Tue, 9 Jun 2026 20:10:42 -0300 Subject: [PATCH] Power strip visual in rack view with outlet grid Added is_power_strip flag to device_models (schema migration, model, form checkbox, DB CRUD). Power strip devices in rack view now render a grid of plug-icon outlets instead of the regular port list, with custom cable colors showing which outlets are in use. --- internal/db/db.go | 2 ++ internal/db/device_models.go | 20 ++++++++++---------- internal/db/schema.sql | 1 + internal/handlers/handlers.go | 1 + internal/handlers/models.go | 2 ++ internal/models/models.go | 1 + static/style.css | 13 +++++++++++++ templates/_power_strip.html | 19 +++++++++++++++++++ templates/model_form.html | 5 +++++ templates/rack.html | 12 ++++++++++++ 10 files changed, 66 insertions(+), 10 deletions(-) create mode 100644 templates/_power_strip.html diff --git a/internal/db/db.go b/internal/db/db.go index f63f083..004d98f 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -28,6 +28,8 @@ func Init(path string) (*Store, error) { return nil, fmt.Errorf("run schema: %w", err) } + db.Exec("ALTER TABLE device_models ADD COLUMN is_power_strip BOOLEAN NOT NULL DEFAULT FALSE") + if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil { db.Close() return nil, fmt.Errorf("wal mode: %w", err) diff --git a/internal/db/device_models.go b/internal/db/device_models.go index ce8c4d0..32da71f 100644 --- a/internal/db/device_models.go +++ b/internal/db/device_models.go @@ -6,7 +6,7 @@ import ( ) func (s *Store) ModelGetAll() ([]models.DeviceModel, error) { - rows, err := s.DB.Query(`SELECT id, name, manufacturer, is_rack_mountable, height_units, front_image, back_image, is_patch_panel, is_wall_socket, created_at, updated_at FROM device_models ORDER BY name`) + rows, err := s.DB.Query(`SELECT id, name, manufacturer, is_rack_mountable, height_units, front_image, back_image, is_patch_panel, is_wall_socket, is_power_strip, created_at, updated_at FROM device_models ORDER BY name`) if err != nil { return nil, err } @@ -16,7 +16,7 @@ func (s *Store) ModelGetAll() ([]models.DeviceModel, error) { for rows.Next() { var m models.DeviceModel if err := rows.Scan(&m.ID, &m.Name, &m.Manufacturer, &m.IsRackMountable, &m.HeightUnits, - &m.FrontImage, &m.BackImage, &m.IsPatchPanel, &m.IsWallSocket, &m.CreatedAt, &m.UpdatedAt); err != nil { + &m.FrontImage, &m.BackImage, &m.IsPatchPanel, &m.IsWallSocket, &m.IsPowerStrip, &m.CreatedAt, &m.UpdatedAt); err != nil { return nil, err } list = append(list, m) @@ -26,9 +26,9 @@ func (s *Store) ModelGetAll() ([]models.DeviceModel, error) { func (s *Store) ModelGetByID(id int64) (*models.DeviceModel, error) { m := &models.DeviceModel{} - err := s.DB.QueryRow(`SELECT id, name, manufacturer, is_rack_mountable, height_units, front_image, back_image, is_patch_panel, is_wall_socket, created_at, updated_at FROM device_models WHERE id = ?`, id). + err := s.DB.QueryRow(`SELECT id, name, manufacturer, is_rack_mountable, height_units, front_image, back_image, is_patch_panel, is_wall_socket, is_power_strip, created_at, updated_at FROM device_models WHERE id = ?`, id). Scan(&m.ID, &m.Name, &m.Manufacturer, &m.IsRackMountable, &m.HeightUnits, - &m.FrontImage, &m.BackImage, &m.IsPatchPanel, &m.IsWallSocket, &m.CreatedAt, &m.UpdatedAt) + &m.FrontImage, &m.BackImage, &m.IsPatchPanel, &m.IsWallSocket, &m.IsPowerStrip, &m.CreatedAt, &m.UpdatedAt) if err == sql.ErrNoRows { return nil, nil } @@ -45,9 +45,9 @@ func (s *Store) ModelGetByID(id int64) (*models.DeviceModel, error) { func (s *Store) modelGetByIDTx(tx *sql.Tx, id int64) (*models.DeviceModel, error) { m := &models.DeviceModel{} - err := tx.QueryRow(`SELECT id, name, manufacturer, is_rack_mountable, height_units, front_image, back_image, is_patch_panel, is_wall_socket FROM device_models WHERE id = ?`, id). + err := tx.QueryRow(`SELECT id, name, manufacturer, is_rack_mountable, height_units, front_image, back_image, is_patch_panel, is_wall_socket, is_power_strip FROM device_models WHERE id = ?`, id). Scan(&m.ID, &m.Name, &m.Manufacturer, &m.IsRackMountable, &m.HeightUnits, - &m.FrontImage, &m.BackImage, &m.IsPatchPanel, &m.IsWallSocket) + &m.FrontImage, &m.BackImage, &m.IsPatchPanel, &m.IsWallSocket, &m.IsPowerStrip) if err != nil { return nil, err } @@ -74,8 +74,8 @@ func (s *Store) ModelCreate(m *models.DeviceModel, imageDir string) (int64, erro } defer tx.Rollback() - res, err := tx.Exec(`INSERT INTO device_models (name, manufacturer, is_rack_mountable, height_units, front_image, back_image, is_patch_panel, is_wall_socket) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, - m.Name, m.Manufacturer, m.IsRackMountable, m.HeightUnits, m.FrontImage, m.BackImage, m.IsPatchPanel, m.IsWallSocket) + res, err := tx.Exec(`INSERT INTO device_models (name, manufacturer, is_rack_mountable, height_units, front_image, back_image, is_patch_panel, is_wall_socket, is_power_strip) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + m.Name, m.Manufacturer, m.IsRackMountable, m.HeightUnits, m.FrontImage, m.BackImage, m.IsPatchPanel, m.IsWallSocket, m.IsPowerStrip) if err != nil { return 0, err } @@ -104,8 +104,8 @@ func (s *Store) ModelUpdate(m *models.DeviceModel) error { } defer tx.Rollback() - _, err = tx.Exec(`UPDATE device_models SET name=?, manufacturer=?, is_rack_mountable=?, height_units=?, front_image=?, back_image=?, is_patch_panel=?, is_wall_socket=?, updated_at=datetime('now') WHERE id=?`, - m.Name, m.Manufacturer, m.IsRackMountable, m.HeightUnits, m.FrontImage, m.BackImage, m.IsPatchPanel, m.IsWallSocket, m.ID) + _, err = tx.Exec(`UPDATE device_models SET name=?, manufacturer=?, is_rack_mountable=?, height_units=?, front_image=?, back_image=?, is_patch_panel=?, is_wall_socket=?, is_power_strip=?, updated_at=datetime('now') WHERE id=?`, + m.Name, m.Manufacturer, m.IsRackMountable, m.HeightUnits, m.FrontImage, m.BackImage, m.IsPatchPanel, m.IsWallSocket, m.IsPowerStrip, m.ID) if err != nil { return err } diff --git a/internal/db/schema.sql b/internal/db/schema.sql index ace6649..8acb666 100644 --- a/internal/db/schema.sql +++ b/internal/db/schema.sql @@ -12,6 +12,7 @@ CREATE TABLE IF NOT EXISTS device_models ( back_image TEXT DEFAULT '', is_patch_panel BOOLEAN NOT NULL DEFAULT FALSE, is_wall_socket BOOLEAN NOT NULL DEFAULT FALSE, + is_power_strip BOOLEAN NOT NULL DEFAULT FALSE, created_at DATETIME NOT NULL DEFAULT (datetime('now')), updated_at DATETIME NOT NULL DEFAULT (datetime('now')) ); diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 0a4ebb5..1afe3ab 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -55,6 +55,7 @@ func New(store *db.Store) *Handlers { files := append(baseFiles, filepath.Join("templates", page), filepath.Join("templates", "_port_list.html"), + filepath.Join("templates", "_power_strip.html"), ) t, err := template.New("base.html").Funcs(funcMap).ParseFiles(files...) if err != nil { diff --git a/internal/handlers/models.go b/internal/handlers/models.go index 98aa18b..9a5b160 100644 --- a/internal/handlers/models.go +++ b/internal/handlers/models.go @@ -60,6 +60,7 @@ func (h *Handlers) ModelCreate(w http.ResponseWriter, r *http.Request) { Manufacturer: r.FormValue("manufacturer"), IsPatchPanel: r.FormValue("is_patch_panel") == "on", IsWallSocket: r.FormValue("is_wall_socket") == "on", + IsPowerStrip: r.FormValue("is_power_strip") == "on", } isRack := r.FormValue("is_rack_mountable") == "on" @@ -109,6 +110,7 @@ func (h *Handlers) ModelUpdate(w http.ResponseWriter, r *http.Request) { m.Manufacturer = r.FormValue("manufacturer") m.IsPatchPanel = r.FormValue("is_patch_panel") == "on" m.IsWallSocket = r.FormValue("is_wall_socket") == "on" + m.IsPowerStrip = r.FormValue("is_power_strip") == "on" isRack := r.FormValue("is_rack_mountable") == "on" m.IsRackMountable = isRack diff --git a/internal/models/models.go b/internal/models/models.go index b92abd8..f871637 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -12,6 +12,7 @@ type DeviceModel struct { BackImage string IsPatchPanel bool IsWallSocket bool + IsPowerStrip bool CreatedAt time.Time UpdatedAt time.Time Ports []DeviceModelPort diff --git a/static/style.css b/static/style.css index 686fef1..254ce7a 100644 --- a/static/style.css +++ b/static/style.css @@ -167,6 +167,19 @@ th { background: var(--surface); font-weight: 600; } .port-ref { font-family: monospace; color: var(--green); font-size: 0.85em; } .connection-details { margin: 0.8em 0; } +/* POWER STRIP OUTLETS */ +.power-strip-outlets { display: grid; grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); gap: 0.3em; margin-top: 0.3em; } +.outlet { + display: flex; flex-direction: column; align-items: center; gap: 0.15em; + padding: 0.3em; border: 1px solid var(--border); border-radius: 4px; + background: var(--bg); cursor: pointer; font-size: 0.8em; + transition: border-color 0.15s, background 0.15s; +} +.outlet:hover { border-color: var(--accent); } +.outlet-used { border-width: 2px; } +.outlet-icon { font-size: 1.2em; } +.outlet-label { font-family: monospace; font-size: 0.8em; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; } + /* PORT EDITOR */ .port-entry { display: flex; gap: 0.5em; align-items: center; margin-bottom: 0.3em; } .port-entry input { width: 18em; margin-bottom: 0; } diff --git a/templates/_power_strip.html b/templates/_power_strip.html new file mode 100644 index 0000000..3dde06f --- /dev/null +++ b/templates/_power_strip.html @@ -0,0 +1,19 @@ +{{define "power_strip"}} +
+ {{range .Device.Ports}} + {{$pid := .ID}} + {{$color := "#333"}}{{$bg := ""}} + {{range $.Connections}}{{if and .Port1 .Port2}}{{if eq .Port1.ID $pid}}{{$color = .Color}}{{$bg = .Color}}{{else if eq .Port2.ID $pid}}{{$color = .Color}}{{$bg = .Color}}{{end}}{{end}}{{end}} + + 🔌 + {{.Name}} + + {{end}} +
+{{end}} diff --git a/templates/model_form.html b/templates/model_form.html index 540b30a..06db65d 100644 --- a/templates/model_form.html +++ b/templates/model_form.html @@ -28,6 +28,11 @@ {{if .Model}}{{if .Model.IsWallSocket}}checked{{end}}{{end}}> Wall socket (behaves like patch panel) + diff --git a/templates/rack.html b/templates/rack.html index 7154b7e..24c5b59 100644 --- a/templates/rack.html +++ b/templates/rack.html @@ -60,7 +60,13 @@ + {{if .Model}}{{if .Model.IsPowerStrip}} + {{template "power_strip" dict "Device" . "Connections" $.Connections}} + {{else}} {{template "port_list" dict "Device" . "Connections" $.Connections}} + {{end}}{{else}} + {{template "port_list" dict "Device" . "Connections" $.Connections}} + {{end}} {{end}} @@ -75,7 +81,13 @@ + {{if .Model}}{{if .Model.IsPowerStrip}} + {{template "power_strip" dict "Device" . "Connections" $.Connections}} + {{else}} {{template "port_list" dict "Device" . "Connections" $.Connections}} + {{end}}{{else}} + {{template "port_list" dict "Device" . "Connections" $.Connections}} + {{end}} {{end}} {{end}}