Files
tia-love/internal/app/home.go
T

193 lines
5.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"fmt"
"runtime"
"strings"
"time"
"charm.land/lipgloss/v2"
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/ui/components"
)
// homeBody — dashboard beranda (F-027). Tata letak terinspirasi mirador:
// grid panel dengan judul di border; baris atas Jam · Docker · Host,
// baris bawah Kontainer · Aktivitas.
func (m Model) homeBody() string {
W := max(m.width, 60)
bodyH := m.bodyHeight()
row1H := 9
row2H := bodyH - row1H
if row2H < 5 {
row1H = 6
row2H = max(bodyH-row1H, 3)
}
w1 := int(float64(W) * 0.36)
w2 := int(float64(W) * 0.30)
w3 := W - w1 - w2
row1 := lipgloss.JoinHorizontal(lipgloss.Top,
m.clockPanel(w1, row1H),
m.dockerPanel(w2, row1H),
m.hostPanel(w3, row1H),
)
k1 := int(float64(W) * 0.55)
k2 := W - k1
row2 := lipgloss.JoinHorizontal(lipgloss.Top,
panel("Kontainer", fmt.Sprintf("%d total", len(m.containers)),
m.miniContainerLines(row2H-2), k1, row2H, false),
panel("Aktivitas", "Phase 2", activityLines(), k2, row2H, false),
)
return row1 + "\n" + row2
}
func (m Model) clockPanel(w, h int) string {
now := time.Now()
rows := components.CenterBlock(components.RenderClock(now), w-4)
for i, r := range rows {
rows[i] = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#cba6f7")).Render(r)
}
lines := append(rows, "", dim(" "+hariID(now)+", "+tanggalID(now)+" · "+now.Format("MST")))
if len(lines) > h-2 {
lines = lines[:h-2]
}
return panel("Jam", "", lines, w, h, false)
}
func (m Model) dockerPanel(w, h int) string {
var lines []string
switch {
case m.engineErr != nil:
lines = append(lines, red("GAGAL"), "", dim(truncate(m.engineErr.Error(), w-6)))
case m.engine.ServerVersion == "":
lines = append(lines, dim("memuat…"))
default:
lines = append(lines,
text("Engine ")+dim(m.engine.ServerVersion),
text("API ")+dim(m.engine.APIVersion),
text("Kontainer ")+dim(fmt.Sprintf("%d jalan / %d total", m.engine.Running, m.engine.Containers)),
text("Image ")+dim(fmt.Sprintf("%d", m.engine.Images)),
"",
styled("#a6e3a1", "● terhubung"),
)
}
return panel("Docker", "lokal", lines, w, h, false)
}
func (m Model) hostPanel(w, h int) string {
s := m.sys
cpus := m.engine.CPUs
if cpus == 0 {
cpus = runtime.NumCPU()
}
pct := 0
if s.MemTotal > 0 {
pct = int((s.MemTotal - s.MemAvail) * 100 / s.MemTotal)
}
lines := []string{
text("Host ") + dim(m.hostname),
text("CPU ") + dim(fmt.Sprintf("%d core · load %.2f", cpus, s.Load1)),
text("RAM ") + dim(fmt.Sprintf("%3d%% ", pct)) + memBar(pct, 10) +
dim(fmt.Sprintf(" %s/%s", humanKBs(s.MemTotal-s.MemAvail), humanKBs(s.MemTotal))),
text("Uptime ") + dim(humanDuration(s.Uptime)),
}
return panel("Host", "", lines, w, h, false)
}
func (m Model) miniContainerLines(maxLines int) []string {
if m.loading && len(m.containers) == 0 {
return []string{dim("memuat…")}
}
if len(m.containers) == 0 {
return []string{dim("belum ada kontainer — tekan 1")}
}
max := maxLines
if len(m.containers) > max {
max--
}
lines := make([]string, 0, maxLines)
for i := 0; i < len(m.containers) && i < max; i++ {
c := m.containers[i]
glyph, color := stateStyle(c.State)
lines = append(lines, styled(color, glyph)+" "+text(truncate(c.Name, 24))+" "+dim(truncate(c.Image, 32)))
}
if extra := len(m.containers) - len(lines); extra > 0 {
lines = append(lines, dim(fmt.Sprintf("… %d lainnya (tekan 1)", extra)))
}
return lines
}
func activityLines() []string {
return []string{
dim("Listener Docker events menyusul (Phase 2)."),
"",
dim("Panel ini akan menampilkan 5 aktivitas terbaru:"),
dim("kontainer start/stop, image di-pull, error terakhir."),
}
}
// --- small formatting helpers ---
var hari = [...]string{"Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"}
var bulan = [...]string{"Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember"}
func hariID(t time.Time) string { return hari[int(t.Weekday())] }
func tanggalID(t time.Time) string {
return fmt.Sprintf("%d %s %d", t.Day(), bulan[int(t.Month())-1], t.Year())
}
func humanKB(kb uint64) string {
f := float64(kb) * 1024
units := []string{"B", "KB", "MB", "GB", "TB"}
i := 0
for f >= 1024 && i < len(units)-1 {
f /= 1024
i++
}
if i <= 1 {
return fmt.Sprintf("%.0f %s", f, units[i])
}
return fmt.Sprintf("%.1f %s", f, units[i])
}
func humanKBs(kb uint64) string {
return strings.Replace(humanKB(kb), " ", "", 1)
}
func humanDuration(sec float64) string {
d := time.Duration(sec) * time.Second
days := int(d.Hours()) / 24
hours := int(d.Hours()) % 24
mins := int(d.Minutes()) % 60
switch {
case days > 0:
return fmt.Sprintf("%d hari %d jam", days, hours)
case hours > 0:
return fmt.Sprintf("%d jam %d mnt", hours, mins)
default:
return fmt.Sprintf("%d mnt", mins)
}
}
// memBar — bar RAM; warna ikut beban (hijau <60% · kuning 60–85% · merah >85%).
func memBar(pct, w int) string {
fill := pct * w / 100
if fill > w {
fill = w
}
color := "#a6e3a1"
switch {
case pct >= 85:
color = "#f38ba8"
case pct >= 60:
color = "#f9e2af"
}
return styled(color, strings.Repeat("█", fill)) + dim(strings.Repeat("░", w-fill))
}