generated from mathias/template-go-web
feat(gitea): surface mathias's own open Gitea issues on stage 03 (#4)
TDD: IssueNodes parses /repos/issues/search into stage nodes (title, repo tag, clickable html_url). gitea.MyIssues() reads GITEA_TOKEN and skips gracefully when unset, mirroring the existing liveOverlay fallback pattern. Single-operator homelab, not per-visitor OAuth: a static read-only PAT gates on "cleared Authentik forward-auth", not per-user token exchange — see #4 discussion. GITEA_TOKEN provisioning in infra (ExternalSecret) is a separate follow-up; without it the overlay is inert (no crash, just no live nodes), so this ships safely ahead of that wiring. Nodes with a url now render as clickable <a class="node"> instead of <div class="node">.
This commit is contained in:
@@ -23,7 +23,7 @@
|
||||
{"t":"🏛️ LLM Council","plain_t":"AI review panel","plain":"For hard calls, several AI models answer independently, anonymously critique each other, and a \"chair\" model synthesises one verdict — reducing any single model's bias.","cls":"council","pill":"var(--violet)","d":"fan-out → anonymous cross-review → chairman synth. glm-4.7-flash · qwen36-35b · gemma4-31b (chair).","tags":["hard strategic Q","chat.d-ma.be"]},
|
||||
{"t":"Autoresearch Council","plain_t":"Research review panel","plain":"A parallel version of the same review that vets research findings before they're allowed through.","cls":"council","pill":"var(--violet)","d":"Sibling pipe — ratifies research before the gate.","tags":["proposed: → standalone svc"]}
|
||||
]},
|
||||
{"no":"STAGE 03","short":"Write order","title":"Spec → Gitea issue","plain_title":"Write the work order","plain":"The decision is turned into a precise, self-contained work order an AI agent can execute unsupervised — with a pass/fail definition of done, a risk rating, and a tamper-proof seal.","path":"agent-ready contract",
|
||||
{"no":"STAGE 03","short":"Write order","title":"Spec → Gitea issue","plain_title":"Write the work order","plain":"The decision is turned into a precise, self-contained work order an AI agent can execute unsupervised — with a pass/fail definition of done, a risk rating, and a tamper-proof seal.","path":"agent-ready contract","generate":"gitea-issues",
|
||||
"trans_label":"Sealed & agent-ready","trans":"Advances to the gate only when the spec is a complete contract: a pass/fail test, a risk tier, a regulatory note, no open human dependencies, one embedded Oath, and a valid cryptographic signature. A malformed or unsigned order fails closed and never reaches the gate.","nodes":[
|
||||
{"t":"Contract enforced","plain_t":"The work-order rules","plain":"The work order must have a clear pass/fail test, a risk rating, a regulatory-risk note, and no unfinished human dependencies before it counts as agent-ready.","d":"Binary ISC · declared risk tier · reg-risk assessment · no open human deps.","tags":["LOW / MED / HIGH"]},
|
||||
{"t":"Admission controller","plain_t":"Tamper-proof seal","plain":"The work order is cryptographically signed when created, so any later tampering is detectable and the eventual change can be checked against it.","d":"Ed25519-sign issue body at creation (#36). Verify sig + PR alignment at infra boundary.","tags":["chain of custody"]},
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package atlas
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// IssueNodes parses a Gitea `/repos/issues/search` response (the
|
||||
// authenticated user's own open issues, newest first) into one node per
|
||||
// issue, linking out to the issue.
|
||||
func IssueNodes(searchJSON []byte) ([]Node, error) {
|
||||
var issues []struct {
|
||||
Number int `json:"number"`
|
||||
Title string `json:"title"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
Repository struct {
|
||||
FullName string `json:"full_name"`
|
||||
} `json:"repository"`
|
||||
}
|
||||
if err := json.Unmarshal(searchJSON, &issues); err != nil {
|
||||
return nil, fmt.Errorf("parse issues: %w", err)
|
||||
}
|
||||
nodes := make([]Node, 0, len(issues))
|
||||
for _, i := range issues {
|
||||
nodes = append(nodes, Node{
|
||||
Title: fmt.Sprintf("#%d %s", i.Number, i.Title),
|
||||
Tags: []string{"live · Gitea", i.Repository.FullName},
|
||||
URL: i.HTMLURL,
|
||||
})
|
||||
}
|
||||
return nodes, nil
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package atlas_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.d-ma.be/mathias/cad-atlas/internal/atlas"
|
||||
)
|
||||
|
||||
func TestIssueNodes_OneNodePerIssueWithRepoTagAndURL(t *testing.T) {
|
||||
search := []byte(`[
|
||||
{"number":212,"title":"segment-embedder: add smoke test","html_url":"https://git.d-ma.be/mathias/infra/issues/212","repository":{"full_name":"mathias/infra"}},
|
||||
{"number":8,"title":"Write cad-atlas's own vargo-gate candidate","html_url":"https://git.d-ma.be/mathias/cad-atlas/issues/8","repository":{"full_name":"mathias/cad-atlas"}}
|
||||
]`)
|
||||
|
||||
nodes, err := atlas.IssueNodes(search)
|
||||
if err != nil {
|
||||
t.Fatalf("IssueNodes: %v", err)
|
||||
}
|
||||
if len(nodes) != 2 {
|
||||
t.Fatalf("want 2 nodes, got %d", len(nodes))
|
||||
}
|
||||
if nodes[0].Title != "#212 segment-embedder: add smoke test" {
|
||||
t.Fatalf("title = %q", nodes[0].Title)
|
||||
}
|
||||
if nodes[0].URL != "https://git.d-ma.be/mathias/infra/issues/212" {
|
||||
t.Fatalf("url = %q", nodes[0].URL)
|
||||
}
|
||||
if len(nodes[0].Tags) != 2 || nodes[0].Tags[0] != "live · Gitea" || nodes[0].Tags[1] != "mathias/infra" {
|
||||
t.Fatalf("tags = %v", nodes[0].Tags)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueNodes_EmptyListReturnsEmptyNotNil(t *testing.T) {
|
||||
nodes, err := atlas.IssueNodes([]byte(`[]`))
|
||||
if err != nil {
|
||||
t.Fatalf("IssueNodes: %v", err)
|
||||
}
|
||||
if nodes == nil || len(nodes) != 0 {
|
||||
t.Fatalf("nodes = %+v, want empty non-nil slice", nodes)
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ type Node struct {
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Risk bool `json:"risk,omitempty"`
|
||||
Gate bool `json:"gate,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// Stage is one column of the pipeline. PlainTitle/Plain are the plain-language
|
||||
|
||||
Reference in New Issue
Block a user