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.master
parent
a845b242a2
commit
56c525cb26
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ type DeviceModel struct {
|
|||
BackImage string
|
||||
IsPatchPanel bool
|
||||
IsWallSocket bool
|
||||
IsPowerStrip bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Ports []DeviceModelPort
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
{{define "power_strip"}}
|
||||
<div class="power-strip-outlets">
|
||||
{{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}}
|
||||
<span class="outlet {{if ne $bg ""}}outlet-used{{end}}"
|
||||
style="{{if ne $bg ""}}border-color:{{$color}};background:{{$color}}22{{end}}"
|
||||
hx-get="/connections/{{.ID}}"
|
||||
hx-target="#modal-content"
|
||||
hx-trigger="click"
|
||||
onclick="openModal()"
|
||||
title="Outlet: {{.Name}}">
|
||||
<span class="outlet-icon">🔌</span>
|
||||
<span class="outlet-label">{{.Name}}</span>
|
||||
</span>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
@ -28,6 +28,11 @@
|
|||
{{if .Model}}{{if .Model.IsWallSocket}}checked{{end}}{{end}}>
|
||||
Wall socket <small>(behaves like patch panel)</small>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="is_power_strip" id="chk-power"
|
||||
{{if .Model}}{{if .Model.IsPowerStrip}}checked{{end}}{{end}}>
|
||||
Power strip <small>(outlets shown as socket grid in rack view)</small>
|
||||
</label>
|
||||
<div id="patch-note" style="display:none;color:var(--text-muted);font-size:0.85em;margin-top:0.3em">
|
||||
Port side selectors disabled — patch panels create front+back pairs from every port regardless of side.
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -60,7 +60,13 @@
|
|||
<button class="btn-sm btn-danger">Remove</button>
|
||||
</form>
|
||||
</h4>
|
||||
{{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}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
|
|
@ -75,7 +81,13 @@
|
|||
<button class="btn-sm btn-danger">Remove</button>
|
||||
</form>
|
||||
</h4>
|
||||
{{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}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
|
|
|||
Loading…
Reference in New Issue