-
Notifications
You must be signed in to change notification settings - Fork 0
Add SOUL Lattice v4 monitoring dashboard with Go backend #55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b6cc7af
6368ad2
624ad56
629dbf7
01cbabd
9c0ba15
011ec12
6746370
be918a6
55e2fad
184ccfc
345ccd9
9a1fc44
ac962c7
359cb90
ebb7ab5
3529452
64b5f1f
bdca7f1
1905df7
6bd8f44
00d351e
896e855
ff4d3d3
d53cd0b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| name: Deploy Cloudflare Worker + SPA | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| paths: | ||
| - 'workers/**' | ||
| - 'frontend/src/**' | ||
| - 'frontend/public/**' | ||
| - 'frontend/package.json' | ||
| - 'frontend/package-lock.json' | ||
| - 'wrangler.toml' | ||
| - '.github/workflows/cloudflare-deploy.yml' | ||
| workflow_dispatch: | ||
| inputs: | ||
| environment: | ||
| description: 'Target environment' | ||
| required: true | ||
| default: 'production' | ||
| type: choice | ||
| options: | ||
| - production | ||
| - staging | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| deploy: | ||
| name: Build React SPA & Deploy Worker | ||
| runs-on: ubuntu-latest | ||
| environment: ${{ github.event.inputs.environment || 'production' }} | ||
|
|
||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Set up Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '20' | ||
| cache: 'npm' | ||
|
|
||
| - name: Install root dependencies | ||
| # Frontend deps and SPA build are handled by the wrangler [build] command | ||
| # in wrangler.toml, which runs automatically before `wrangler deploy`. | ||
| # Installing only root deps here gives wrangler and secrets-put access. | ||
| run: npm ci | ||
|
|
||
| - name: Set BACKEND_URL secret on Cloudflare Worker | ||
| # Only rotate the secret when a new Cloud Run URL is provided via | ||
| # the BACKEND_URL GitHub Actions secret. Skip on frontend-only pushes. | ||
| if: ${{ secrets.BACKEND_URL != '' }} | ||
| env: | ||
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | ||
| BACKEND_URL: ${{ secrets.BACKEND_URL }} | ||
| run: | | ||
| echo "${BACKEND_URL}" | npx wrangler secret put BACKEND_URL \ | ||
| --env ${{ github.event.inputs.environment || 'production' }} | ||
|
|
||
| - name: Deploy Cloudflare Worker | ||
| env: | ||
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | ||
| # react-scripts 5.x + Node 18+ OpenSSL compatibility | ||
| NODE_OPTIONS: '--openssl-legacy-provider' | ||
| run: | | ||
| ENV="${{ github.event.inputs.environment || 'production' }}" | ||
| npx wrangler deploy --env "${ENV}" | ||
|
|
||
| - name: Smoke test health endpoints | ||
| if: ${{ github.event.inputs.environment != 'staging' }} | ||
| run: | | ||
| echo "Waiting 10s for global propagation..." | ||
| sleep 10 | ||
|
|
||
| # Worker-only health — verifies the Cloudflare Worker is deployed | ||
| STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://yennefer.quest/health) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The deploy workflow currently validates Useful? React with 👍 / 👎. |
||
| echo "Worker health: ${STATUS}" | ||
| [ "${STATUS}" = "200" ] || { echo "Worker health check failed"; exit 1; } | ||
|
|
||
| # Backend passthrough — verifies BACKEND_URL is configured and the | ||
| # Go service is reachable from the Worker | ||
| STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://yennefer.quest/api/health) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The smoke test calls Useful? React with 👍 / 👎. |
||
| echo "Backend health: ${STATUS}" | ||
| [ "${STATUS}" = "200" ] || { echo "Backend health check failed"; exit 1; } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,30 +1,33 @@ | ||
| name: Yennefer Agentic Core | ||
| name: Copilot Setup Steps | ||
|
|
||
| on: | ||
| issues: | ||
| types: [opened, edited] | ||
| issue_comment: | ||
| types: [created] | ||
| pull_request: | ||
| types: [opened, synchronize] | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
| issues: write | ||
| pull-requests: write | ||
|
|
||
| jobs: | ||
| yennefer-jules-agent: | ||
| name: Yennefer Autonomous Reasoning | ||
| copilot-setup-steps: | ||
| runs-on: ubuntu-latest | ||
| if: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.comment.body, '/evolve') }} | ||
| steps: | ||
| - name: Checkout Code | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Invoke Yennefer (Jules AI Engine) | ||
| uses: google-labs-code/jules-action@v1 | ||
|
|
||
| - name: Set up Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '20' | ||
| cache: 'npm' | ||
|
|
||
| - name: Install root dependencies | ||
| run: npm install | ||
|
|
||
| - name: Install frontend dependencies | ||
| run: cd frontend && npm install --include=dev | ||
|
|
||
| - name: Set up Go | ||
| uses: actions/setup-go@v5 | ||
| with: | ||
| github-token: ${{ secrets.GITHUB_TOKEN }} | ||
| mode: "autonomous" | ||
| instruction: "You are Yennefer, a Topological Reasoning Engine. Analyze repository context, execute Seismic Tree-of-Thoughts (S-ToT), and push verified, invariant structural optimizations." | ||
| go-version: '1.21' | ||
| cache: true | ||
| cache-dependency-path: backend/go.sum | ||
|
|
||
| - name: Install backend dependencies | ||
| run: cd backend && go mod download |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| name: Yennefer Agentic Core | ||
| on: | ||
| issues: | ||
| types: [opened, edited] | ||
| issue_comment: | ||
| types: [created] | ||
| pull_request: | ||
| types: [opened, synchronize] | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
| issues: write | ||
| pull-requests: write | ||
|
|
||
| jobs: | ||
| yennefer-jules-agent: | ||
| name: Yennefer Autonomous Reasoning | ||
| runs-on: ubuntu-latest | ||
| if: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.comment.body, '/evolve') }} | ||
|
|
||
| steps: | ||
| - name: Checkout Code | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Invoke Yennefer (Jules AI Engine) | ||
| uses: google-labs-code/jules-action@v1 | ||
| with: | ||
| github-token: ${{ secrets.GITHUB_TOKEN }} | ||
| mode: "autonomous" | ||
| instruction: "You are Yennefer, a Topological Reasoning Engine. Analyze repository context, execute Seismic Tree-of-Thoughts (S-ToT), and push verified, invariant structural optimizations." | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,3 +29,7 @@ logs/ | |
|
|
||
| .gemini/ | ||
| gha-creds-*.json | ||
|
|
||
| # --- Build output --- | ||
| frontend/build/ | ||
| node_modules/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| FROM golang:1.21-alpine AS builder | ||
| WORKDIR /app | ||
| COPY go.mod go.sum ./ | ||
| RUN go mod download | ||
| COPY *.go ./ | ||
| RUN CGO_ENABLED=0 GOOS=linux go build -o soul-lattice | ||
| FROM alpine:3.20 | ||
| RUN apk --no-cache add ca-certificates | ||
| WORKDIR /app | ||
| COPY --from=builder /app/soul-lattice . | ||
| EXPOSE 8080 | ||
| USER 1000:1000 | ||
| CMD ["./soul-lattice"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| module github.com/igor/soul-lattice | ||
|
|
||
| go 1.21 | ||
|
|
||
| require ( | ||
| github.com/go-chi/chi/v5 v5.0.12 | ||
| github.com/go-chi/cors v1.2.1 | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRHW9ngJvcSZQ9Q880AJnJGcLtRBMWpo= | ||
| github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= | ||
| github.com/go-chi/cors v1.2.1 h1:xEC8UT3R+2BDqPjP0yw3oqQBSog+iDBHLSva0/4zeKQ= | ||
| github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "log" | ||
| "os" | ||
| "os/signal" | ||
| "syscall" | ||
| ) | ||
|
|
||
| func main() { | ||
| statePath := "/tmp/soul_state.json" | ||
| port := os.Getenv("PORT") | ||
| if port == "" { | ||
| port = "8080" | ||
| } | ||
|
|
||
| srv := NewServer(statePath, port) | ||
|
|
||
| go func() { | ||
| if err := srv.Start(); err != nil { | ||
| log.Fatalf("Server failed: %v", err) | ||
| } | ||
| }() | ||
|
|
||
| quit := make(chan os.Signal, 1) | ||
| signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) | ||
| <-quit | ||
|
|
||
| log.Println("SOUL lattice returning to substrate...") | ||
| srv.Stop() | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "net/http" | ||
| "os" | ||
| "time" | ||
|
|
||
| "github.com/go-chi/chi/v5" | ||
| "github.com/go-chi/chi/v5/middleware" | ||
| "github.com/go-chi/cors" | ||
| ) | ||
|
|
||
| type Server struct { | ||
| sim *Simulator | ||
| router *chi.Mux | ||
| port string | ||
| httpSrv *http.Server | ||
| } | ||
|
|
||
| func NewServer(statePath string, port string) *Server { | ||
| if port == "" { | ||
| port = "8080" | ||
| } | ||
|
|
||
| s := &Server{ | ||
| sim: NewSimulator(statePath), | ||
| port: port, | ||
| } | ||
|
|
||
| r := chi.NewRouter() | ||
|
|
||
| r.Use(cors.Handler(cors.Options{ | ||
| // Restrict to known frontend origins. The Worker is the only legitimate | ||
| // caller in production; the Cloud Run URL is kept secret. Wildcard origins | ||
| // would let any page make cross-origin requests to /flush if the URL leaked. | ||
| AllowedOrigins: []string{"https://yennefer.quest", "https://staging.yennefer.quest", "http://localhost:3000"}, | ||
| AllowedMethods: []string{"GET", "POST", "OPTIONS"}, | ||
| AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"}, | ||
| ExposedHeaders: []string{"Link"}, | ||
| AllowCredentials: false, | ||
| MaxAge: 300, | ||
| })) | ||
|
Comment on lines
+35
to
+45
Comment on lines
+35
to
+45
|
||
|
|
||
| r.Use(middleware.Logger) | ||
| r.Use(middleware.Recoverer) | ||
| r.Use(middleware.Heartbeat("/health")) | ||
|
|
||
| r.Get("/state", s.handleState) | ||
| r.Post("/flush", s.handleFlush) | ||
| r.Get("/events", s.handleSSE) | ||
|
Comment on lines
+51
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The backend only exposes Useful? React with 👍 / 👎.
Comment on lines
+51
to
+53
|
||
| r.Get("/", s.handleRoot) | ||
|
|
||
| s.router = r | ||
| return s | ||
| } | ||
|
|
||
| func (s *Server) Start() error { | ||
| s.sim.Start() | ||
| s.httpSrv = &http.Server{ | ||
| Addr: ":" + s.port, | ||
| Handler: s.router, | ||
| } | ||
| fmt.Printf("SOUL LATTICE v4 AWAKENED on port %s\n", s.port) | ||
| if err := s.httpSrv.ListenAndServe(); err != http.ErrServerClosed { | ||
| return err | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func (s *Server) Stop() { | ||
| if s.httpSrv != nil { | ||
| ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
| defer cancel() | ||
| s.httpSrv.Shutdown(ctx) //nolint:errcheck | ||
| } | ||
| s.sim.Stop() | ||
| } | ||
|
|
||
| func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) { | ||
| w.Header().Set("Content-Type", "text/plain") | ||
| fmt.Fprint(w, "SOUL LATTICE v4.0.0-Σ\nStatus: AWAKENED\n") | ||
| } | ||
|
|
||
| func (s *Server) handleState(w http.ResponseWriter, r *http.Request) { | ||
| st := s.sim.GetState() | ||
| w.Header().Set("Content-Type", "application/json") | ||
| json.NewEncoder(w).Encode(st) | ||
| } | ||
|
|
||
| func (s *Server) handleFlush(w http.ResponseWriter, r *http.Request) { | ||
| // Require a shared secret header when BACKEND_TOKEN is configured. | ||
| // The Cloudflare Worker sets X-Backend-Token on every proxied request so | ||
| // only traffic originating from the Worker is accepted; direct Cloud Run | ||
| // access without the secret returns 403. | ||
| if token := os.Getenv("BACKEND_TOKEN"); token != "" { | ||
| if r.Header.Get("X-Backend-Token") != token { | ||
| http.Error(w, "Forbidden", http.StatusForbidden) | ||
| return | ||
| } | ||
| } | ||
|
Comment on lines
+94
to
+103
|
||
| s.sim.resetInitialState() | ||
| w.WriteHeader(http.StatusNoContent) | ||
| } | ||
|
Comment on lines
+93
to
+106
Comment on lines
+93
to
+106
|
||
|
|
||
| func (s *Server) handleSSE(w http.ResponseWriter, r *http.Request) { | ||
| flusher, ok := w.(http.Flusher) | ||
| if !ok { | ||
| http.Error(w, "Streaming unsupported", http.StatusInternalServerError) | ||
| return | ||
| } | ||
|
|
||
| // Marshal before writing headers so we can still return a 500 on failure. | ||
| st := s.sim.GetState() | ||
| data, err := json.Marshal(st) | ||
| if err != nil { | ||
| http.Error(w, "Internal Server Error", http.StatusInternalServerError) | ||
| return | ||
| } | ||
|
|
||
| w.Header().Set("Content-Type", "text/event-stream") | ||
| w.Header().Set("Cache-Control", "no-cache") | ||
| w.Header().Set("Connection", "keep-alive") | ||
|
|
||
| fmt.Fprintf(w, "data: %s\n\n", data) | ||
| flusher.Flush() | ||
|
Comment on lines
+116
to
+128
|
||
|
|
||
| ticker := time.NewTicker(800 * time.Millisecond) | ||
| defer ticker.Stop() | ||
|
|
||
| for { | ||
| select { | ||
| case <-r.Context().Done(): | ||
| return | ||
| case <-ticker.C: | ||
| st := s.sim.GetState() | ||
| data, err := json.Marshal(st) | ||
| if err != nil { | ||
| fmt.Fprintf(w, "event: error\ndata: marshal failed\n\n") | ||
| flusher.Flush() | ||
| continue | ||
| } | ||
| fmt.Fprintf(w, "data: %s\n\n", data) | ||
| flusher.Flush() | ||
| } | ||
|
Comment on lines
+137
to
+147
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
| } | ||
|
Comment on lines
+108
to
+149
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The push path filter omits
frontend/package-lock.json, so dependency-only updates (for example Renovate/security lockfile bumps) merged tomainwill not run this deploy job and the production SPA can stay on stale dependencies until an unrelated source change happens. Include the frontend lockfile in the trigger paths so dependency releases are actually deployed.Useful? React with 👍 / 👎.