feat: integrate Grafana dashboards into BOC DashboardPage

- Add Infrastructure Health section with CPU/Memory/Disk panels
- Add Service Status section with PM2/Docker panels
- Create GrafanaPanel component for iframe embedding
- Build passes successfully
This commit is contained in:
Bernt
2026-07-29 19:03:06 +00:00
parent af874040ca
commit e5623d2f84
77 changed files with 11338 additions and 779 deletions
+188 -130
View File
@@ -4,11 +4,12 @@ import (
"database/sql"
"encoding/json"
"net/http"
"time"
"boc/legal"
"github.com/go-chi/chi/v5"
)
// LegalHandler hanterar legal/contract endpoints
type LegalHandler struct {
DB *sql.DB
}
@@ -17,44 +18,34 @@ func NewLegalHandler(db *sql.DB) *LegalHandler {
return &LegalHandler{DB: db}
}
// Contract representerar ett avtal i systemet
type Contract struct {
ID string `json:"id"`
Title string `json:"title"`
Counterparty string `json:"counterparty"`
Type string `json:"type"`
Status string `json:"status"`
Value float64 `json:"value"`
Currency string `json:"currency"`
StartDate *time.Time `json:"start_date"`
EndDate *time.Time `json:"end_date"`
RenewalDate *time.Time `json:"renewal_date"`
DocumentURL *string `json:"document_url"`
CreatedAt time.Time `json:"created_at"`
}
type ContractReminder struct {
ID string `json:"id"`
ContractID string `json:"contract_id"`
Type string `json:"type"`
DueDate time.Time `json:"due_date"`
Status string `json:"status"`
ID string `json:"id"`
TemplateType string `json:"template_type"`
Name string `json:"name"`
Counterparty string `json:"counterparty"`
CounterpartyOrg string `json:"counterparty_org,omitempty"`
Status string `json:"status"` // draft, pending, active, expired, terminated
Value float64 `json:"value,omitempty"`
Currency string `json:"currency,omitempty"`
StartDate string `json:"start_date,omitempty"`
EndDate string `json:"end_date,omitempty"`
RenewalDate string `json:"renewal_date,omitempty"`
Responsible string `json:"responsible,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// ListContracts returnerar alla avtal
func (h *LegalHandler) ListContracts(w http.ResponseWriter, r *http.Request) {
status := r.URL.Query().Get("status")
if status == "" {
status = "active"
}
rows, err := h.DB.Query(`
SELECT id, title, counterparty, type, status, value, currency,
start_date, end_date, renewal_date, document_url, created_at
SELECT id, template_type, name, counterparty, counterparty_org, status,
value, currency, start_date, end_date, renewal_date, responsible, created_at, updated_at
FROM boc_contracts
WHERE status = $1
ORDER BY renewal_date ASC NULLS LAST
`, status)
ORDER BY created_at DESC
`)
if err != nil {
writeError(w, http.StatusInternalServerError, "database error")
http.Error(w, `{"error":"database error"}`, http.StatusInternalServerError)
return
}
defer rows.Close()
@@ -62,143 +53,210 @@ func (h *LegalHandler) ListContracts(w http.ResponseWriter, r *http.Request) {
contracts := []Contract{}
for rows.Next() {
var c Contract
if err := rows.Scan(&c.ID, &c.Title, &c.Counterparty, &c.Type, &c.Status,
&c.Value, &c.Currency, &c.StartDate, &c.EndDate, &c.RenewalDate,
&c.DocumentURL, &c.CreatedAt); err != nil {
continue
var value sql.NullFloat64
var currency, startDate, endDate, renewalDate, responsible sql.NullString
rows.Scan(&c.ID, &c.TemplateType, &c.Name, &c.Counterparty, &c.CounterpartyOrg,
&c.Status, &value, &currency, &startDate, &endDate, &renewalDate, &responsible,
&c.CreatedAt, &c.UpdatedAt)
if value.Valid {
c.Value = value.Float64
}
if currency.Valid {
c.Currency = currency.String
}
if startDate.Valid {
c.StartDate = startDate.String
}
if endDate.Valid {
c.EndDate = endDate.String
}
if renewalDate.Valid {
c.RenewalDate = renewalDate.String
}
if responsible.Valid {
c.Responsible = responsible.String
}
contracts = append(contracts, c)
}
writeJSON(w, http.StatusOK, map[string]interface{}{
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"contracts": contracts,
"total": len(contracts),
})
}
func (h *LegalHandler) CreateContract(w http.ResponseWriter, r *http.Request) {
var req Contract
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request")
return
}
var id string
err := h.DB.QueryRow(`
INSERT INTO boc_contracts (title, counterparty, type, status, value, currency,
start_date, end_date, renewal_date, document_url)
VALUES ($1, $2, $3, 'draft', $4, $5, $6, $7, $8, $9)
RETURNING id
`, req.Title, req.Counterparty, req.Type, req.Value, req.Currency,
req.StartDate, req.EndDate, req.RenewalDate, req.DocumentURL).Scan(&id)
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to create contract")
return
}
// Create reminder if renewal date is set
if req.RenewalDate != nil {
reminderDate := req.RenewalDate.AddDate(0, 0, -30) // 30 days before
h.DB.Exec(`
INSERT INTO boc_contract_reminders (contract_id, type, due_date, status)
VALUES ($1, 'renewal', $2, 'pending')
`, id, reminderDate)
}
writeJSON(w, http.StatusCreated, map[string]interface{}{
"id": id,
"message": "Contract created",
})
}
// GetContract returnerar ett specifikt avtal
func (h *LegalHandler) GetContract(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
var c Contract
var value sql.NullFloat64
var currency, startDate, endDate, renewalDate, responsible sql.NullString
err := h.DB.QueryRow(`
SELECT id, title, counterparty, type, status, value, currency,
start_date, end_date, renewal_date, document_url, created_at
SELECT id, template_type, name, counterparty, counterparty_org, status,
value, currency, start_date, end_date, renewal_date, responsible, created_at, updated_at
FROM boc_contracts WHERE id = $1
`, id).Scan(&c.ID, &c.Title, &c.Counterparty, &c.Type, &c.Status,
&c.Value, &c.Currency, &c.StartDate, &c.EndDate, &c.RenewalDate,
&c.DocumentURL, &c.CreatedAt)
`, id).Scan(&c.ID, &c.TemplateType, &c.Name, &c.Counterparty, &c.CounterpartyOrg,
&c.Status, &value, &currency, &startDate, &endDate, &renewalDate, &responsible,
&c.CreatedAt, &c.UpdatedAt)
if err == sql.ErrNoRows {
writeError(w, http.StatusNotFound, "contract not found")
http.Error(w, `{"error":"contract not found"}`, http.StatusNotFound)
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, "database error")
http.Error(w, `{"error":"database error"}`, http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusOK, c)
if value.Valid {
c.Value = value.Float64
}
if currency.Valid {
c.Currency = currency.String
}
if startDate.Valid {
c.StartDate = startDate.String
}
if endDate.Valid {
c.EndDate = endDate.String
}
if renewalDate.Valid {
c.RenewalDate = renewalDate.String
}
if responsible.Valid {
c.Responsible = responsible.String
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(c)
}
// GetContractTemplates returnerar alla standardavtal
func (h *LegalHandler) GetContractTemplates(w http.ResponseWriter, r *http.Request) {
templates := legal.GetStandardTemplates()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"templates": templates,
"total": len(templates),
})
}
// GetContractTemplate returnerar ett specifikt template
func (h *LegalHandler) GetContractTemplate(w http.ResponseWriter, r *http.Request) {
templateType := chi.URLParam(r, "type")
templates := legal.GetStandardTemplates()
for _, t := range templates {
if string(t.Type) == templateType {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(t)
return
}
}
http.Error(w, `{"error":"template not found"}`, http.StatusNotFound)
}
// GetProductContractLinks returnerar produkt-avtal kopplingar
func (h *LegalHandler) GetProductContractLinks(w http.ResponseWriter, r *http.Request) {
links := legal.GetProductContractLinks()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"links": links,
"total": len(links),
})
}
// CreateContract skapar ett nytt avtal från template
func (h *LegalHandler) CreateContract(w http.ResponseWriter, r *http.Request) {
var req struct {
TemplateType string `json:"template_type"`
Counterparty string `json:"counterparty"`
CounterpartyOrg string `json:"counterparty_org,omitempty"`
Variables map[string]string `json:"variables,omitempty"`
Terms legal.ContractTerms `json:"terms,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
// Hitta template
var template *legal.ContractTemplate
for _, t := range legal.GetStandardTemplates() {
if string(t.Type) == req.TemplateType {
template = &t
break
}
}
if template == nil {
http.Error(w, `{"error":"template not found"}`, http.StatusNotFound)
return
}
// Skapa avtal i databas
var id string
err := h.DB.QueryRow(`
INSERT INTO boc_contracts (template_type, name, counterparty, counterparty_org, status, currency)
VALUES ($1, $2, $3, $4, 'draft', $5)
RETURNING id
`, req.TemplateType, template.Name, req.Counterparty, req.CounterpartyOrg, template.DefaultTerms.Currency).Scan(&id)
if err != nil {
http.Error(w, `{"error":"failed to create contract"}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"id": id,
"message": "Contract created",
"template": template,
})
}
// UpdateContract uppdaterar ett avtal
func (h *LegalHandler) UpdateContract(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
var req Contract
var req struct {
Status string `json:"status,omitempty"`
Value float64 `json:"value,omitempty"`
StartDate string `json:"start_date,omitempty"`
EndDate string `json:"end_date,omitempty"`
Responsible string `json:"responsible,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request")
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
_, err := h.DB.Exec(`
UPDATE boc_contracts
SET title = $1, counterparty = $2, type = $3, status = $4,
value = $5, currency = $6, start_date = $7, end_date = $8,
renewal_date = $9, document_url = $10
WHERE id = $11
`, req.Title, req.Counterparty, req.Type, req.Status, req.Value, req.Currency,
req.StartDate, req.EndDate, req.RenewalDate, req.DocumentURL, id)
SET status = COALESCE(NULLIF($1, ''), status),
value = COALESCE($2, value),
start_date = COALESCE(NULLIF($3, ''), start_date),
end_date = COALESCE(NULLIF($4, ''), end_date),
responsible = COALESCE(NULLIF($5, ''), responsible),
updated_at = NOW()
WHERE id = $6
`, req.Status, req.Value, req.StartDate, req.EndDate, req.Responsible, id)
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to update contract")
http.Error(w, `{"error":"failed to update contract"}`, http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"message": "Contract updated",
})
}
func (h *LegalHandler) ListReminders(w http.ResponseWriter, r *http.Request) {
rows, err := h.DB.Query(`
SELECT r.id, r.contract_id, r.type, r.due_date, r.status,
c.title as contract_title
FROM boc_contract_reminders r
JOIN boc_contracts c ON r.contract_id = c.id
WHERE r.status = 'pending'
ORDER BY r.due_date ASC
LIMIT 50
`)
if err != nil {
writeError(w, http.StatusInternalServerError, "database error")
return
}
defer rows.Close()
reminders := []map[string]interface{}{}
for rows.Next() {
var id, contractID, reminderType, status, contractTitle string
var dueDate time.Time
if err := rows.Scan(&id, &contractID, &reminderType, &dueDate, &status, &contractTitle); err != nil {
continue
}
reminders = append(reminders, map[string]interface{}{
"id": id,
"contract_id": contractID,
"contract_title": contractTitle,
"type": reminderType,
"due_date": dueDate,
"status": status,
})
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"reminders": reminders,
"total": len(reminders),
})
}