feat(panel): 15-minute CPU average and dark-mode IPv6 select

- Widen the CPU average window from 5 to 15 minutes across the admin
  and user panels (List, Show, BatchUsers, PanelResources) and relabel
  the columns/cards accordingly.
- Replace the hardcoded white background of the create-user IPv6
  dropdown with a themed class so it matches the dark color scheme.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
bluebluesodaandCommandCodeBot committed 2026-08-22 08:22:50 +00:00
1 parent f9114c86f3
commit 5cc2f10ae4
6 files changed
+14 -12

No files matched your search

@@ -63,6 +63,8 @@ form.inline { display: inline; }
@media (prefers-color-scheme: dark) { .field label { color: #9aa0a8; } }
.field input { width: 100%; padding: 8px 10px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 13px; background: #fff; color: inherit; }
@media (prefers-color-scheme: dark) { .field input { background: #15171b; border-color: #3a3f47; } }
.field select.field-select { width: 100%; padding: 8px 10px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 13px; background: #fff; color: inherit; }
@media (prefers-color-scheme: dark) { .field select.field-select { background: #15171b; border-color: #3a3f47; } }
.cpu-row { display: flex; gap: 4px; align-items: stretch; }
.cpu-row input { flex: 1; min-width: 0; }
.cpu-row .step { display: flex; flex-direction: column; }
@@ -158,7 +160,7 @@ a.copy.copied { border-color: #86efac; background: #dcfce7; color: #15803d; }
<div class="field"><label>{{if eq .Lang "zh"}}流量配额 GiB{{else}}Bandwidth GiB{{end}}</label><input name="bandwidth" id="cfBandwidth" type="number" min="0" step="1" value="0" title="{{if eq .Lang "zh"}}每月流量上限(上传+下载),0 = 不限;超限后限速 1Mbps{{else}}monthly quota (upload+download), 0 = unlimited; throttled to 1Mbps when exceeded{{end}}"></div>
{{if .IPv6PoolMode}}
<div class="field"><label>{{if eq .Lang "zh"}}IPv6 地址{{else}}IPv6 address{{end}}</label>
<select name="ipv6" id="cfIpv6" style="width:100%;padding:8px 10px;border:1px solid #d1d5db;border-radius:6px;font-size:13px;background:#fff;color:inherit;">
<select name="ipv6" id="cfIpv6" class="field-select">
<option value="auto" selected>{{if eq .Lang "zh"}}自动分配(第一个空闲){{else}}Auto (first free){{end}}</option>
{{range .PoolFree}}<option value="{{.}}">{{.}}</option>{{end}}
<option value="none">{{if eq .Lang "zh"}}不分配 V6{{else}}No IPv6{{end}}</option>
@@ -180,7 +182,7 @@ a.copy.copied { border-color: #86efac; background: #dcfce7; color: #15803d; }
<th>{{if eq .Lang "zh"}}状态{{else}}State{{end}}</th>
<th>{{if eq .Lang "zh"}}端口/SSH{{else}}Ports/SSH{{end}}</th>
<th>{{if eq .Lang "zh"}}配额 CPU/内存/磁盘{{else}}Quota CPU/Mem/Disk{{end}}</th>
<th class="sortable" data-type="number">{{if eq .Lang "zh"}}5分钟CPU{{else}}5m CPU{{end}}<span class="ind"></span></th>
<th class="sortable" data-type="number">{{if eq .Lang "zh"}}15分钟CPU{{else}}15m CPU{{end}}<span class="ind"></span></th>
<th class="sortable" data-type="number">{{if eq .Lang "zh"}}内存{{else}}Mem{{end}}<span class="ind"></span></th>
<th class="sortable" data-type="number">{{if eq .Lang "zh"}}进程{{else}}Procs{{end}}<span class="ind"></span></th>
<th class="sortable" data-type="number">{{if eq .Lang "zh"}}磁盘{{else}}Disk{{end}}<span class="ind"></span></th>
@@ -371,7 +373,7 @@ function del(name){
var qc = document.getElementById('quotaCpu');
if (qc) qc.addEventListener('input', function(){ syncCpuStep(qc); });
})();
// Clickable column headers for Name / 5m CPU / Mem / Procs / Disk: cycle
// Clickable column headers for Name / 15m CPU / Mem / Procs / Disk: cycle
// ascending -> descending -> default. Only one column sorts at a time; the
// default state restores the original render order.
(function(){
+2 -2
View File
@@ -130,7 +130,7 @@ func readUptime() time.Duration {
}
// UserStatus is one row of the admin user table: the DB user record plus the
// latest persisted resource sample and five-minute CPU average.
// latest persisted resource sample and fifteen-minute CPU average.
type UserStatus struct {
User *db.User
State string
@@ -154,7 +154,7 @@ func (m *Manager) BatchUsers() ([]*UserStatus, error) {
if err != nil {
return nil, err
}
average, err := m.db.AverageCPU(time.Now().Add(-5 * time.Minute).Unix())
average, err := m.db.AverageCPU(time.Now().Add(-15 * time.Minute).Unix())
if err != nil {
return nil, err
}
+2 -2
View File
@@ -716,7 +716,7 @@ func (m *Manager) List() ([]*Result, error) {
return nil, err
}
latest, _ := m.db.LatestResourceSamples()
average, _ := m.db.AverageCPU(time.Now().Add(-5 * time.Minute).Unix())
average, _ := m.db.AverageCPU(time.Now().Add(-15 * time.Minute).Unix())
live := m.liveStatusFallback(users, latest)
out := make([]*Result, 0, len(users))
for _, u := range users {
@@ -735,7 +735,7 @@ func (m *Manager) Show(name string) (*Result, error) {
return nil, err
}
latest, _ := m.db.LatestResourceSamples()
average, _ := m.db.AverageCPU(time.Now().Add(-5 * time.Minute).Unix())
average, _ := m.db.AverageCPU(time.Now().Add(-15 * time.Minute).Unix())
sample, hasSample := latest[u.ID]
live := m.liveStatusFallback([]*db.User{u}, latest)
avg, hasAverage := average[u.ID]
+2 -2
View File
@@ -229,14 +229,14 @@ func decorateStoredUsage(r *Result, sample db.ResourceSample, hasSample bool, av
}
// PanelResources returns the persisted usage for a user's panel page: the
// five-minute CPU average and the latest memory/disk usage, formatted for
// fifteen-minute CPU average and the latest memory/disk usage, formatted for
// display. No Incus call is made — the data comes from the background sampler.
func (m *Manager) PanelResources(userID int64) (cpuUse, memUse, diskUse string) {
latest, err := m.db.LatestResourceSamples()
if err != nil {
return "-", "-", "-"
}
avg, err := m.db.AverageCPU(time.Now().Add(-5 * time.Minute).Unix())
avg, err := m.db.AverageCPU(time.Now().Add(-15 * time.Minute).Unix())
if err != nil {
avg = map[int64]float64{}
}
+2 -2
View File
@@ -293,8 +293,8 @@ func (s *Server) buildData(u *db.User, msg, errMsg string) pageData {
Msg: msg,
Err: errMsg,
}
// Resource usage comes from the persisted sampler snapshot (five-minute CPU
// average plus latest memory/disk), never from a live Incus sample.
// Resource usage comes from the persisted sampler snapshot (fifteen-minute
// CPU average plus latest memory/disk), never from a live Incus sample.
d.CPUUse, d.MemUse, d.DiskUsed = s.mgr.PanelResources(u.ID)
// One `incus list` call only for the container status (must be live).
// Bandwidth is read from the DB — the background sampler writes it every 60s.
+1 -1
View File
@@ -150,7 +150,7 @@ footer a{color:var(--muted);text-decoration:underline}
<div class="qitem"><span class="qlabel">{{if eq .Lang "zh"}}磁盘{{else}}Disk{{end}}</span><span class="qval">{{.QuotaDisk}}</span></div>
</div>
<div class="qgrid">
<div class="qitem"><span class="qlabel">{{if eq .Lang "zh"}}5分钟CPU{{else}}5m CPU{{end}}</span><span class="qval">{{.CPUUse}}</span></div>
<div class="qitem"><span class="qlabel">{{if eq .Lang "zh"}}15分钟CPU{{else}}15m CPU{{end}}</span><span class="qval">{{.CPUUse}}</span></div>
<div class="qitem"><span class="qlabel">{{if eq .Lang "zh"}}已用内存{{else}}Mem used{{end}}</span><span class="qval">{{.MemUse}}</span></div>
<div class="qitem"><span class="qlabel">{{if eq .Lang "zh"}}已用磁盘{{else}}Disk used{{end}}</span><span class="qval">{{.DiskUsed}}</span></div>
</div>