feat: Stage 3 — alert detail, acknowledge, comments, stats charts

Press enter on any alert to open a full-screen detail pane (viewport).
Keybindings in detail mode:
  a/A   acknowledge / unacknowledge (updates immediately)
  c     compose a comment (text input at bottom)
  [/]   cycle comment cursor up/down
  d     delete selected comment (y/N confirmation)
  s     assign placeholder — shows "not yet supported by server"
  S     open stats view: top alerts + by-hour/by-day ASCII bar charts
  esc   back to dashboard

API additions: GetAlert, AcknowledgeAlert, UnacknowledgeAlert,
GetComments, AddComment, DeleteComment, GetTopAlerts,
GetStatsByHour, GetStatsByDay.
This commit is contained in:
Niklas Ye
2026-05-21 13:48:01 +02:00
parent e29cff21ae
commit b5ee62f145
7 changed files with 825 additions and 36 deletions
+95
View File
@@ -1,6 +1,7 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
@@ -92,6 +93,100 @@ func (c *Client) GetAlertStats() (*AlertStats, error) {
return &stats, c.do(req, &stats)
}
func (c *Client) newRequestWithBody(method, path string, body any) (*http.Request, error) {
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequest(method, c.baseURL+path, bytes.NewReader(data))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
return req, nil
}
func (c *Client) GetAlert(id int64) (*Alert, error) {
req, err := c.newRequest(http.MethodGet, fmt.Sprintf("/api/alerts/%d", id))
if err != nil {
return nil, err
}
var alert Alert
return &alert, c.do(req, &alert)
}
func (c *Client) AcknowledgeAlert(id int64) (*Alert, error) {
req, err := c.newRequest(http.MethodPost, fmt.Sprintf("/api/alerts/%d/acknowledge", id))
if err != nil {
return nil, err
}
var alert Alert
return &alert, c.do(req, &alert)
}
func (c *Client) UnacknowledgeAlert(id int64) error {
req, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/api/alerts/%d/acknowledge", id))
if err != nil {
return err
}
return c.do(req, nil)
}
func (c *Client) GetComments(alertID int64) ([]Comment, error) {
req, err := c.newRequest(http.MethodGet, fmt.Sprintf("/api/alerts/%d/comments", alertID))
if err != nil {
return nil, err
}
var comments []Comment
return comments, c.do(req, &comments)
}
func (c *Client) AddComment(alertID int64, content string) (*Comment, error) {
req, err := c.newRequestWithBody(http.MethodPost, fmt.Sprintf("/api/alerts/%d/comments", alertID), map[string]string{"content": content})
if err != nil {
return nil, err
}
var comment Comment
return &comment, c.do(req, &comment)
}
func (c *Client) DeleteComment(alertID, commentID int64) error {
req, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/api/alerts/%d/comments/%d", alertID, commentID))
if err != nil {
return err
}
return c.do(req, nil)
}
func (c *Client) GetTopAlerts(limit int) ([]TopAlert, error) {
req, err := c.newRequest(http.MethodGet, fmt.Sprintf("/api/stats/alerts/top?limit=%d", limit))
if err != nil {
return nil, err
}
var result []TopAlert
return result, c.do(req, &result)
}
func (c *Client) GetStatsByHour() ([]HourStat, error) {
req, err := c.newRequest(http.MethodGet, "/api/stats/alerts/by-hour")
if err != nil {
return nil, err
}
var result []HourStat
return result, c.do(req, &result)
}
func (c *Client) GetStatsByDay() ([]DayStat, error) {
req, err := c.newRequest(http.MethodGet, "/api/stats/alerts/by-day")
if err != nil {
return nil, err
}
var result []DayStat
return result, c.do(req, &result)
}
// HealthCheck calls GET /healthz (unauthenticated path, no auth needed but we send it anyway).
func (c *Client) HealthCheck() error {
req, err := http.NewRequest(http.MethodGet, c.baseURL+"/healthz", nil)
+25
View File
@@ -23,3 +23,28 @@ type AlertStats struct {
Firing int `json:"firing"`
Resolved int `json:"resolved"`
}
type Comment struct {
ID int64 `json:"id"`
AlertID int64 `json:"alert_id"`
UserID int64 `json:"user_id"`
Username string `json:"username"`
Content string `json:"content"`
CreatedAt time.Time `json:"created_at"`
}
type TopAlert struct {
Name string `json:"name"`
Count int `json:"count"`
}
type HourStat struct {
Hour int `json:"hour"`
Count int `json:"count"`
}
type DayStat struct {
Day int `json:"day"`
DayName string `json:"day_name"`
Count int `json:"count"`
}