Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e04cfcf433 | |||
| 6834302622 | |||
| 24c2e6003a |
@@ -0,0 +1,57 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- goos: linux
|
||||
goarch: amd64
|
||||
- goos: linux
|
||||
goarch: arm64
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- goos: darwin
|
||||
goarch: arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
run: |
|
||||
go build \
|
||||
-ldflags "-X main.version=${{ github.ref_name }}" \
|
||||
-o terdut-tui-${{ github.ref_name }}-${{ matrix.goos }}-${{ matrix.goarch }} \
|
||||
.
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: terdut-tui-${{ github.ref_name }}-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: terdut-tui-${{ github.ref_name }}-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
|
||||
release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
merge-multiple: true
|
||||
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: 'terdut-tui-*'
|
||||
@@ -66,3 +66,16 @@ go build -ldflags="-X main.version=v0.1.0" -o terdut-tui .
|
||||
| 3 | Alert detail: acknowledge, comment, statistics charts |
|
||||
| 4 | On-call schedule calendar view |
|
||||
| 5 | User management and API key lifecycle |
|
||||
|
||||
<!-- graymatter:instructions:begin — managed by `graymatter init`; edits inside this block are overwritten -->
|
||||
## Memory (GrayMatter)
|
||||
|
||||
This project has persistent agent memory via the `graymatter` MCP tools:
|
||||
|
||||
- `memory_search` (`agent_id`, `query`) — call at the **start of a task** when prior context might matter.
|
||||
- `memory_add` (`agent_id`, `text`) — call whenever you learn something **durable**: user preferences, decisions, conventions, gotchas.
|
||||
- `memory_reflect` (`action`, `agent`, `text`/`target`) — update or forget stale facts. ⚠ takes `agent`, not `agent_id`.
|
||||
- `checkpoint_save` / `checkpoint_resume` (`agent_id`) — snapshot/restore session state before major refactors or across restarts.
|
||||
|
||||
Use a stable `agent_id` of the form `<project>-<role>` (e.g. `myapp-backend`). Store conclusions, not conversation logs. Err on the side of remembering.
|
||||
<!-- graymatter:instructions:end -->
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
VERSION := $(shell git describe --tags --always --dirty)
|
||||
|
||||
.PHONY: build install test
|
||||
|
||||
build:
|
||||
go build -ldflags "-X main.version=$(VERSION)" -o terdut-tui .
|
||||
|
||||
install:
|
||||
go install -ldflags "-X main.version=$(VERSION)" .
|
||||
|
||||
test:
|
||||
go test ./...
|
||||
+23
-2
@@ -61,12 +61,16 @@ func (c *Client) do(req *http.Request, out any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListAlerts fetches alerts from the server. status may be "firing", "resolved", or "" for all.
|
||||
func (c *Client) ListAlerts(status string, limit int) ([]Alert, error) {
|
||||
// ListAlerts fetches alerts. status may be "firing", "resolved", or "" for all.
|
||||
// Set archived=true to fetch only archived alerts; false returns only non-archived.
|
||||
func (c *Client) ListAlerts(status string, archived bool, limit int) ([]Alert, error) {
|
||||
q := url.Values{}
|
||||
if status != "" {
|
||||
q.Set("status", status)
|
||||
}
|
||||
if archived {
|
||||
q.Set("archived", "true")
|
||||
}
|
||||
if limit > 0 {
|
||||
q.Set("limit", strconv.Itoa(limit))
|
||||
}
|
||||
@@ -83,6 +87,23 @@ func (c *Client) ListAlerts(status string, limit int) ([]Alert, error) {
|
||||
return alerts, c.do(req, &alerts)
|
||||
}
|
||||
|
||||
func (c *Client) ArchiveAlert(id int64) (*Alert, error) {
|
||||
req, err := c.newRequest(http.MethodPost, fmt.Sprintf("/api/alerts/%d/archive", id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var alert Alert
|
||||
return &alert, c.do(req, &alert)
|
||||
}
|
||||
|
||||
func (c *Client) UnarchiveAlert(id int64) error {
|
||||
req, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/api/alerts/%d/archive", id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.do(req, nil)
|
||||
}
|
||||
|
||||
// GetAlertStats fetches aggregate alert counts.
|
||||
func (c *Client) GetAlertStats() (*AlertStats, error) {
|
||||
req, err := c.newRequest(http.MethodGet, "/api/stats/alerts")
|
||||
|
||||
@@ -16,6 +16,12 @@ type Alert struct {
|
||||
AcknowledgedByID *int64 `json:"acknowledged_by_id"`
|
||||
AcknowledgedBy string `json:"acknowledged_by"`
|
||||
AcknowledgedAt *time.Time `json:"acknowledged_at"`
|
||||
ArchivedAt *time.Time `json:"archived_at,omitempty"`
|
||||
|
||||
// ResolutionSource records why a resolved alert left the firing state:
|
||||
// "alertmanager" for a real resolved webhook, "expiry" when the server
|
||||
// inferred it after the alert stopped being refreshed.
|
||||
ResolutionSource *string `json:"resolution_source,omitempty"`
|
||||
}
|
||||
|
||||
type AlertStats struct {
|
||||
|
||||
+69
-7
@@ -18,7 +18,8 @@ import (
|
||||
type section int
|
||||
|
||||
const (
|
||||
sectionAlerts section = iota
|
||||
sectionAlerts section = iota
|
||||
sectionArchived
|
||||
sectionSchedule
|
||||
sectionUsers
|
||||
)
|
||||
@@ -53,6 +54,9 @@ const (
|
||||
type connectedMsg struct{}
|
||||
type connectErrMsg struct{ err error }
|
||||
type alertsFetchedMsg struct{ alerts []api.Alert }
|
||||
type archivedAlertsFetchedMsg struct{ alerts []api.Alert }
|
||||
type alertArchivedMsg struct{ alerts []api.Alert }
|
||||
type alertUnarchivedMsg struct{ alerts []api.Alert }
|
||||
type statsFetchedMsg struct{ stats api.AlertStats }
|
||||
type fetchDataErrMsg struct{ err error }
|
||||
type tickMsg time.Time
|
||||
@@ -113,6 +117,11 @@ type Model struct {
|
||||
filterStatus string
|
||||
alertTable table.Model
|
||||
|
||||
// Archived alerts
|
||||
archivedAlerts []api.Alert
|
||||
archivedLoading bool
|
||||
archivedTable table.Model
|
||||
|
||||
// Detail
|
||||
selectedAlert api.Alert
|
||||
comments []api.Comment
|
||||
@@ -168,6 +177,9 @@ func NewModel(client *api.Client, serverURL string, refreshInterval time.Duratio
|
||||
alertT := table.New(table.WithFocused(true))
|
||||
alertT.SetStyles(ts)
|
||||
|
||||
archivedT := table.New(table.WithFocused(true))
|
||||
archivedT.SetStyles(ts)
|
||||
|
||||
schedT := table.New(table.WithFocused(true))
|
||||
schedT.SetStyles(ts)
|
||||
|
||||
@@ -215,6 +227,7 @@ func NewModel(client *api.Client, serverURL string, refreshInterval time.Duratio
|
||||
filterStatus: "firing",
|
||||
commentCursor: -1,
|
||||
alertTable: alertT,
|
||||
archivedTable: archivedT,
|
||||
commentInput: ti,
|
||||
scheduleWindow: window,
|
||||
scheduleTable: schedT,
|
||||
@@ -254,6 +267,16 @@ func (m *Model) rebuildTable() {
|
||||
m.alertTable.SetHeight(h)
|
||||
}
|
||||
|
||||
func (m *Model) rebuildArchivedTable() {
|
||||
m.archivedTable.SetColumns(alertColumns(m.width))
|
||||
m.archivedTable.SetRows(alertRows(m.archivedAlerts))
|
||||
h := m.height - 8
|
||||
if h < 1 {
|
||||
h = 1
|
||||
}
|
||||
m.archivedTable.SetHeight(h)
|
||||
}
|
||||
|
||||
func (m *Model) rebuildScheduleTable() {
|
||||
m.scheduleTable.SetColumns(scheduleColumns(m.width))
|
||||
m.scheduleTable.SetRows(scheduleRows(m.scheduleDays))
|
||||
@@ -317,18 +340,21 @@ func (m Model) detailViewportHeight() int {
|
||||
// ── Column definitions ─────────────────────────────────────────────────────
|
||||
|
||||
func alertColumns(width int) []table.Column {
|
||||
nameW := width/2 - 8
|
||||
const statusW, timeW = 10, 12
|
||||
nameW := width/2 - 14
|
||||
if nameW < 20 {
|
||||
nameW = 20
|
||||
}
|
||||
ackW := width - nameW - 10 - 12 - 6
|
||||
// 10 = bubbles' Padding(0, 1) on each of the five cells.
|
||||
ackW := width - nameW - statusW - 2*timeW - 10
|
||||
if ackW < 8 {
|
||||
ackW = 8
|
||||
}
|
||||
return []table.Column{
|
||||
{Title: "Name", Width: nameW},
|
||||
{Title: "Status", Width: 10},
|
||||
{Title: "Started", Width: 12},
|
||||
{Title: "Status", Width: statusW},
|
||||
{Title: "Started", Width: timeW},
|
||||
{Title: "Last Seen", Width: timeW},
|
||||
{Title: "Ack By", Width: ackW},
|
||||
}
|
||||
}
|
||||
@@ -377,7 +403,7 @@ func alertRows(alerts []api.Alert) []table.Row {
|
||||
now := time.Now()
|
||||
rows := make([]table.Row, len(alerts))
|
||||
for i, a := range alerts {
|
||||
rows[i] = table.Row{a.Name, a.Status, humanAgo(now, a.StartsAt), a.AcknowledgedBy}
|
||||
rows[i] = table.Row{a.Name, a.Status, humanAgo(now, a.StartsAt), humanAgo(now, a.ReceivedAt), a.AcknowledgedBy}
|
||||
}
|
||||
return rows
|
||||
}
|
||||
@@ -466,7 +492,7 @@ func connectCmd(client *api.Client) tea.Cmd {
|
||||
|
||||
func fetchAlertsCmd(client *api.Client, status string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
alerts, err := client.ListAlerts(status, 500)
|
||||
alerts, err := client.ListAlerts(status, false, 500)
|
||||
if err != nil {
|
||||
return fetchDataErrMsg{err}
|
||||
}
|
||||
@@ -474,6 +500,42 @@ func fetchAlertsCmd(client *api.Client, status string) tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
func fetchArchivedAlertsCmd(client *api.Client) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
alerts, err := client.ListAlerts("", true, 500)
|
||||
if err != nil {
|
||||
return fetchDataErrMsg{err}
|
||||
}
|
||||
return archivedAlertsFetchedMsg{alerts}
|
||||
}
|
||||
}
|
||||
|
||||
func archiveAlertCmd(client *api.Client, alertID int64, filterStatus string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if _, err := client.ArchiveAlert(alertID); err != nil {
|
||||
return actionErrMsg{err}
|
||||
}
|
||||
alerts, err := client.ListAlerts(filterStatus, false, 500)
|
||||
if err != nil {
|
||||
return actionErrMsg{err}
|
||||
}
|
||||
return alertArchivedMsg{alerts}
|
||||
}
|
||||
}
|
||||
|
||||
func unarchiveAlertCmd(client *api.Client, alertID int64) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if err := client.UnarchiveAlert(alertID); err != nil {
|
||||
return actionErrMsg{err}
|
||||
}
|
||||
alerts, err := client.ListAlerts("", true, 500)
|
||||
if err != nil {
|
||||
return actionErrMsg{err}
|
||||
}
|
||||
return alertUnarchivedMsg{alerts}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchStatsCmd(client *api.Client) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
stats, err := client.GetAlertStats()
|
||||
|
||||
+81
-2
@@ -16,6 +16,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
m.rebuildTable()
|
||||
m.rebuildArchivedTable()
|
||||
m.rebuildScheduleTable()
|
||||
m.rebuildUserPickerTable()
|
||||
m.rebuildUserManageTable()
|
||||
@@ -49,6 +50,28 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.rebuildTable()
|
||||
return m, nil
|
||||
|
||||
case archivedAlertsFetchedMsg:
|
||||
m.archivedAlerts = msg.alerts
|
||||
m.archivedLoading = false
|
||||
m.rebuildArchivedTable()
|
||||
return m, nil
|
||||
|
||||
case alertArchivedMsg:
|
||||
m.alerts = msg.alerts
|
||||
m.loading = false
|
||||
m.rebuildTable()
|
||||
m.mode = modeDashboard
|
||||
m.statusMsg = "Alert archived"
|
||||
return m, clearStatusCmd()
|
||||
|
||||
case alertUnarchivedMsg:
|
||||
m.archivedAlerts = msg.alerts
|
||||
m.archivedLoading = false
|
||||
m.rebuildArchivedTable()
|
||||
m.mode = modeDashboard
|
||||
m.statusMsg = "Alert unarchived"
|
||||
return m, clearStatusCmd()
|
||||
|
||||
case statsFetchedMsg:
|
||||
m.stats = &msg.stats
|
||||
return m, nil
|
||||
@@ -215,6 +238,11 @@ func (m Model) routeKey(msg tea.KeyMsg) (Model, tea.Cmd) {
|
||||
m.alertTable, tableCmd = m.alertTable.Update(msg)
|
||||
m2, ourCmd := m.handleKey(msg)
|
||||
return m2, tea.Batch(tableCmd, ourCmd)
|
||||
case sectionArchived:
|
||||
var tableCmd tea.Cmd
|
||||
m.archivedTable, tableCmd = m.archivedTable.Update(msg)
|
||||
m2, ourCmd := m.handleKey(msg)
|
||||
return m2, tea.Batch(tableCmd, ourCmd)
|
||||
case sectionSchedule:
|
||||
var tableCmd tea.Cmd
|
||||
m.scheduleTable, tableCmd = m.scheduleTable.Update(msg)
|
||||
@@ -266,8 +294,12 @@ func (m Model) handleDashboardKey(msg tea.KeyMsg) (Model, tea.Cmd) {
|
||||
return m, tea.Quit
|
||||
|
||||
case "tab":
|
||||
next := section((int(m.activeSection) + 1) % 3)
|
||||
next := section((int(m.activeSection) + 1) % 4)
|
||||
m.activeSection = next
|
||||
if next == sectionArchived && len(m.archivedAlerts) == 0 {
|
||||
m.archivedLoading = true
|
||||
return m, fetchArchivedAlertsCmd(m.client)
|
||||
}
|
||||
if next == sectionSchedule && len(m.scheduleDays) == 0 {
|
||||
m.scheduleLoading = true
|
||||
from := m.scheduleWindow
|
||||
@@ -284,6 +316,10 @@ func (m Model) handleDashboardKey(msg tea.KeyMsg) (Model, tea.Cmd) {
|
||||
if !m.connected {
|
||||
return m, connectCmd(m.client)
|
||||
}
|
||||
if m.activeSection == sectionArchived {
|
||||
m.archivedLoading = true
|
||||
return m, fetchArchivedAlertsCmd(m.client)
|
||||
}
|
||||
if m.activeSection == sectionSchedule {
|
||||
m.scheduleLoading = true
|
||||
from := m.scheduleWindow
|
||||
@@ -319,7 +355,7 @@ func (m Model) handleDashboardKey(msg tea.KeyMsg) (Model, tea.Cmd) {
|
||||
case "enter":
|
||||
if m.activeSection == sectionAlerts && len(m.alerts) > 0 {
|
||||
cursor := m.alertTable.Cursor()
|
||||
if cursor < len(m.alerts) {
|
||||
if cursor >= 0 && cursor < len(m.alerts) {
|
||||
m.selectedAlert = m.alerts[cursor]
|
||||
m.mode = modeDetail
|
||||
m.commentCursor = -1
|
||||
@@ -328,6 +364,34 @@ func (m Model) handleDashboardKey(msg tea.KeyMsg) (Model, tea.Cmd) {
|
||||
return m, fetchAlertDetailCmd(m.client, m.selectedAlert.ID)
|
||||
}
|
||||
}
|
||||
if m.activeSection == sectionArchived && len(m.archivedAlerts) > 0 {
|
||||
cursor := m.archivedTable.Cursor()
|
||||
if cursor >= 0 && cursor < len(m.archivedAlerts) {
|
||||
m.selectedAlert = m.archivedAlerts[cursor]
|
||||
m.mode = modeDetail
|
||||
m.commentCursor = -1
|
||||
m.detailLoading = true
|
||||
m.detailViewport = viewport.New(m.width, m.detailViewportHeight())
|
||||
return m, fetchAlertDetailCmd(m.client, m.selectedAlert.ID)
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "x":
|
||||
switch m.activeSection {
|
||||
case sectionAlerts:
|
||||
cursor := m.alertTable.Cursor()
|
||||
if cursor < 0 || cursor >= len(m.alerts) {
|
||||
return m, nil
|
||||
}
|
||||
return m, archiveAlertCmd(m.client, m.alerts[cursor].ID, m.filterStatus)
|
||||
case sectionArchived:
|
||||
cursor := m.archivedTable.Cursor()
|
||||
if cursor < 0 || cursor >= len(m.archivedAlerts) {
|
||||
return m, nil
|
||||
}
|
||||
return m, unarchiveAlertCmd(m.client, m.archivedAlerts[cursor].ID)
|
||||
}
|
||||
return m, nil
|
||||
|
||||
// Schedule-specific keys
|
||||
@@ -447,6 +511,9 @@ func (m Model) handleDetailKey(msg tea.KeyMsg) (Model, tea.Cmd) {
|
||||
return m, nil
|
||||
|
||||
case "a":
|
||||
if m.activeSection != sectionAlerts {
|
||||
return m, nil
|
||||
}
|
||||
if m.selectedAlert.AcknowledgedByID != nil {
|
||||
m.statusMsg = "already acknowledged"
|
||||
return m, clearStatusCmd()
|
||||
@@ -454,12 +521,24 @@ func (m Model) handleDetailKey(msg tea.KeyMsg) (Model, tea.Cmd) {
|
||||
return m, acknowledgeCmd(m.client, m.selectedAlert.ID)
|
||||
|
||||
case "A":
|
||||
if m.activeSection != sectionAlerts {
|
||||
return m, nil
|
||||
}
|
||||
if m.selectedAlert.AcknowledgedByID == nil {
|
||||
m.statusMsg = "not acknowledged"
|
||||
return m, clearStatusCmd()
|
||||
}
|
||||
return m, unacknowledgeCmd(m.client, m.selectedAlert.ID)
|
||||
|
||||
case "x":
|
||||
if m.activeSection == sectionAlerts {
|
||||
return m, archiveAlertCmd(m.client, m.selectedAlert.ID, m.filterStatus)
|
||||
}
|
||||
if m.activeSection == sectionArchived {
|
||||
return m, unarchiveAlertCmd(m.client, m.selectedAlert.ID)
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "c":
|
||||
m.mode = modeComment
|
||||
m.commentInput.Reset()
|
||||
|
||||
+47
-6
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/yeniklas/terdut-tui/internal/api"
|
||||
)
|
||||
|
||||
var sectionNames = []string{"Alerts", "Schedule", "Users"}
|
||||
var sectionNames = []string{"Alerts", "Archived", "Schedule", "Users"}
|
||||
|
||||
func (m Model) View() string {
|
||||
if m.width == 0 {
|
||||
@@ -92,7 +92,12 @@ func (m Model) renderBody() string {
|
||||
func (m Model) renderFooter() string {
|
||||
switch m.mode {
|
||||
case modeDetail:
|
||||
actions := styleFooter.Render(" a·ack A·unack c·comment [/]·select d·del s·assign S·stats esc·back")
|
||||
var actions string
|
||||
if m.activeSection == sectionArchived {
|
||||
actions = styleFooter.Render(" x·unarchive c·comment [/]·select d·del S·stats esc·back")
|
||||
} else {
|
||||
actions = styleFooter.Render(" a·ack A·unack x·archive c·comment [/]·select d·del s·assign S·stats esc·back")
|
||||
}
|
||||
if m.statusMsg != "" {
|
||||
return styleStatus.Render(" "+m.statusMsg) + "\n" + actions
|
||||
}
|
||||
@@ -163,6 +168,13 @@ func (m Model) renderFooter() string {
|
||||
return "\n" + styleFooter.Render(" enter·revoke esc·back")
|
||||
|
||||
default:
|
||||
if m.activeSection == sectionArchived {
|
||||
actions := styleFooter.Render(" x·unarchive enter·detail r·refresh tab·section q·quit")
|
||||
if m.statusMsg != "" {
|
||||
return styleStatus.Render(" "+m.statusMsg) + "\n" + actions
|
||||
}
|
||||
return "\n" + actions
|
||||
}
|
||||
if m.activeSection == sectionSchedule {
|
||||
actions := styleFooter.Render(" +·assign day W·assign week d·del ←/→·shift week tab·section r·refresh q·quit")
|
||||
if m.statusMsg != "" {
|
||||
@@ -177,6 +189,10 @@ func (m Model) renderFooter() string {
|
||||
}
|
||||
return "\n" + actions
|
||||
}
|
||||
var footerLeft string
|
||||
if m.activeSection == sectionAlerts {
|
||||
footerLeft = styleFooter.Render(" x·archive")
|
||||
}
|
||||
helpView := styleFooter.Render(m.help.ShortHelpView(m.keys.ShortHelp()))
|
||||
if m.statusMsg != "" {
|
||||
status := styleStatus.Render(m.statusMsg)
|
||||
@@ -186,6 +202,13 @@ func (m Model) renderFooter() string {
|
||||
}
|
||||
return "\n" + status + strings.Repeat(" ", gap) + helpView
|
||||
}
|
||||
if footerLeft != "" {
|
||||
gap := m.width - lipgloss.Width(footerLeft) - lipgloss.Width(helpView)
|
||||
if gap < 0 {
|
||||
gap = 0
|
||||
}
|
||||
return "\n" + footerLeft + strings.Repeat(" ", gap) + helpView
|
||||
}
|
||||
return "\n" + helpView
|
||||
}
|
||||
}
|
||||
@@ -196,6 +219,8 @@ func (m Model) renderDashboard() string {
|
||||
switch m.activeSection {
|
||||
case sectionAlerts:
|
||||
return m.renderAlerts()
|
||||
case sectionArchived:
|
||||
return m.renderArchived()
|
||||
case sectionSchedule:
|
||||
return m.renderSchedule()
|
||||
case sectionUsers:
|
||||
@@ -284,6 +309,16 @@ func (m Model) renderAlerts() string {
|
||||
return lipgloss.JoinVertical(lipgloss.Left, statsBar, content)
|
||||
}
|
||||
|
||||
func (m Model) renderArchived() string {
|
||||
if m.archivedLoading {
|
||||
return styleMuted.Render(" Loading archived alerts…")
|
||||
}
|
||||
if len(m.archivedAlerts) == 0 {
|
||||
return styleMuted.Render(" No archived alerts.")
|
||||
}
|
||||
return m.archivedTable.View()
|
||||
}
|
||||
|
||||
func (m Model) renderStatsBar() string {
|
||||
total, firing, resolved := 0, 0, 0
|
||||
if m.stats != nil {
|
||||
@@ -346,7 +381,11 @@ func buildDetailContent(alert api.Alert, comments []api.Comment, cursor, width i
|
||||
if alert.Status == "firing" {
|
||||
statusStr = styleFiring.Render("● FIRING")
|
||||
} else {
|
||||
statusStr = styleResolved.Render("✓ RESOLVED")
|
||||
label := "✓ RESOLVED"
|
||||
if alert.ResolutionSource != nil {
|
||||
label += " · " + *alert.ResolutionSource
|
||||
}
|
||||
statusStr = styleResolved.Render(label)
|
||||
}
|
||||
nameGap := contentW - lipgloss.Width(name) - lipgloss.Width(statusStr)
|
||||
if nameGap < 1 {
|
||||
@@ -355,17 +394,19 @@ func buildDetailContent(alert api.Alert, comments []api.Comment, cursor, width i
|
||||
b.WriteString("\n " + name + strings.Repeat(" ", nameGap) + statusStr + "\n\n")
|
||||
|
||||
// Timeline
|
||||
b.WriteString(fmt.Sprintf(" Started: %s (%s)\n",
|
||||
b.WriteString(fmt.Sprintf(" Started: %s (%s)\n",
|
||||
alert.StartsAt.UTC().Format("2006-01-02 15:04 UTC"), humanAgo(now, alert.StartsAt)))
|
||||
b.WriteString(fmt.Sprintf(" Last Seen: %s (%s)\n",
|
||||
alert.ReceivedAt.UTC().Format("2006-01-02 15:04 UTC"), humanAgo(now, alert.ReceivedAt)))
|
||||
if alert.EndsAt != nil {
|
||||
b.WriteString(fmt.Sprintf(" Ended: %s\n", alert.EndsAt.UTC().Format("2006-01-02 15:04 UTC")))
|
||||
b.WriteString(fmt.Sprintf(" Ended: %s\n", alert.EndsAt.UTC().Format("2006-01-02 15:04 UTC")))
|
||||
}
|
||||
if alert.GeneratorURL != "" {
|
||||
url := alert.GeneratorURL
|
||||
if len(url) > contentW-12 {
|
||||
url = url[:contentW-15] + "…"
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(" Source: %s\n", url))
|
||||
b.WriteString(fmt.Sprintf(" Source: %s\n", url))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user