24 Commits
Author SHA1 Message Date
Story Crater Bot b6767e247c fix(deps,ci): update module path to forgejo.riotpiao.com/rock/homelab-frontend, switch to GITHUB_TOKEN
Build / Build and push image (push) Failing after 12s
CI / Test, vet, build (push) Successful in 2m18s
2026-08-21 20:46:45 -07:00
Story Crater Bot 6cf5c3e5a9 fix(server): stop WriteTimeout from killing in-progress LLM streams
Build / Build and push image (push) Successful in 34s
CI / Test, vet, build (push) Successful in 2m16s
http.Server.WriteTimeout is an absolute deadline over the whole
response, not an inactivity timeout -- 15s was cutting off SSE streams
from the reasoning model mid-generation, surfacing to clients as a
"terminated" error well before the model finished. Switch to
ReadHeaderTimeout (protects against slow headers without capping body
duration) and raise WriteTimeout to match the edge nginx Ingress's
proxy-read/send-timeout of 3600s.
2026-08-21 20:06:10 -07:00
Story Crater Bot 0aaf4116f1 fix(ci): target the golang runner, generic docker runner retired
Build / Build and push image (push) Successful in 1m20s
CI / Test, vet, build (push) Successful in 2m17s
2026-08-21 16:53:05 -07:00
Story Crater Bot fb1a97e8d0 Add Temporal worker and test client programs
Creates:
- cmd/worker/main.go: Worker that registers workflows and activities
- cmd/test-workflow/main.go: Test client to trigger workflows

Adds go.temporal.io/sdk dependency to go.mod.
2026-08-21 16:49:44 -07:00
Story Crater Bot 9127076f1b test: retry after host network config
Build / Build and push image (push) Successful in 34s
CI / Test, vet, build (push) Successful in 1m36s
2026-08-21 16:15:15 -07:00
Story Crater Bot 3d63df9ba8 fix(ci): drop ineffective --network host option, runner sets it globally now
Build / Build and push image (push) Failing after 14s
CI / Test, vet, build (push) Failing after 12m47s
2026-08-21 16:14:16 -07:00
Story Crater Bot aef122b854 test: retry docker build connect flake
Build / Build and push image (push) Failing after 14s
CI / Test, vet, build (push) Successful in 1m38s
2026-08-21 16:07:40 -07:00
Story Crater Bot bc26ab9340 test: retry after egress NetworkPolicy fix
Build / Build and push image (push) Failing after 16s
CI / Test, vet, build (push) Successful in 1m37s
2026-08-21 16:06:01 -07:00
Story Crater Bot f06edf6a54 test: retry after runner config.yaml rollout
Build / Build and push image (push) Failing after 28s
CI / Test, vet, build (push) Successful in 1m43s
2026-08-21 15:59:10 -07:00
Story Crater Bot 7b265b8338 fix(ci): mount /docker-certs/client into the build job container
Build / Build and push image (push) Failing after 13s
CI / Test, vet, build (push) Successful in 1m49s
2026-08-21 15:55:23 -07:00
Story Crater Bot 0e6ebe7353 test: retry build with FORGEJO_RIOTPIAO_PAT secret set
Build / Build and push image (push) Failing after 14s
CI / Test, vet, build (push) Successful in 1m53s
2026-08-21 14:08:12 -07:00
Story Crater Bot 7ebbf2bd03 fix: install node in job containers so actions/checkout can exec
Build / Build and push image (push) Failing after 15s
CI / Test, vet, build (push) Successful in 1m45s
actions/checkout@v4 is a JS action; Forgejo Actions runs it via node, which
neither golang:1.25-bookworm nor docker:27-cli ship. Every run since the
.gitea/workflows move has failed identically: 'exec: node: executable file
not found in $PATH' during the checkout step, before any real job step ran.

Verified locally against both exact images before pushing:
- golang:1.25-bookworm: apt-get install nodejs ca-certificates git -> node
  v18.20.4, git 2.39.5.
- docker:27-cli: apk add nodejs git -> node v22.23.2, git 2.47.2, and
  actions/checkout's dist/index.js actually executes afterward (confirmed
  by invoking it directly) instead of failing on a missing binary.
- The Dockerfile itself builds clean end to end and the resulting image
  serves /healthz, so checkout was the only remaining blocker in the path.
2026-08-21 13:49:24 -07:00
Story Crater Bot dac1a5da4b fix: move workflows from .forgejo/workflows to .gitea/workflows
Build / Build and push image (push) Failing after 30s
CI / Test, vet, build (push) Failing after 29s
.forgejo/workflows/*.yaml never creates an action_run on push on this
Forgejo instance (1.27.0) -- confirmed directly in the action_run table
across multiple probe pushes. .gitea/workflows fires immediately with an
identical spec. This is why build.yaml never once executed.
2026-08-21 11:36:34 -07:00
Story Crater Bot e71034c3ef test: probe .gitea/workflows path
ProbeGitea / noop (push) Successful in 59s
2026-08-21 11:34:17 -07:00
Story Crater Bot 31ed81a737 test: probe .yml extension detection 2026-08-21 11:16:37 -07:00
Story Crater Bot 4d33b1db9b test: live log capture 2026-08-20 22:47:09 -07:00
Story Crater Bot dcbc72b8ae test: verify runner picks up task post-DNS-recovery 2026-08-20 22:44:07 -07:00
Story Crater Bot f3f71ea90d test: verify forgejo actions after global enable 2026-08-20 22:12:01 -07:00
Story Crater Bot ef87f44f4e test: verify stage 1 build workflow
Pushing empty commit to trigger the build job and confirm:
- CI job runs (vet, test, build)
- Build job runs (build and push with short SHA tag)
- Image lands in forgejo.riotpiao.com/rock/api-gateway:<short-sha>
2026-08-20 21:32:16 -07:00
Story Crater Bot d3a9d3966c ci: split workflows into ci.yaml (verify) and build.yaml (push)
- ci.yaml: vet, test, govulncheck only
- build.yaml: build and push to registry with commit short SHA tag
- No manifest write-back, no git push from CI
- Enabled by Stage 1 (B, C1)
2026-08-20 21:30:40 -07:00
Story Crater Bot a949707aaf deploy: bump gateway to v0.1.1 with llm-client label 2026-08-20 00:10:27 -07:00
Story Crater Bot 2aabd4288b fix: label gateway pods llm-client=true so upstreams accept them
llm-serving-default-deny admits only llm-client=true pods on 8080; without the
label every dispatch timed out with 502.
2026-08-20 00:06:23 -07:00
Story Crater Bot 500eb74577 fix: match namespaces by kubernetes.io/metadata.name in netpol egress
kube-system and llm-serving carry only the automatic label, so DNS and
upstream egress matched nothing and every dispatch failed with 502.
2026-08-20 00:04:17 -07:00
Story Crater Bot d7362985f9 deploy: bump gateway image to v0.1.0 2026-08-19 23:57:21 -07:00
32 changed files with 664 additions and 120 deletions
-85
View File
@@ -1,85 +0,0 @@
# Forgejo Actions CI. Note the path: Forgejo reads .forgejo/workflows/, not
# .github/workflows/. The remote for this repo is git.riotpiao.com, so a GitHub
# workflow here would never run.
#
# runs-on: docker matches the only label the cluster runner declares
# (talos-runner, labels: [docker]).
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: forgejo.riotpiao.com
IMAGE: forgejo.riotpiao.com/rock/api-gateway
jobs:
verify:
name: Test, vet, build
runs-on: docker
container:
image: golang:1.25-bookworm
steps:
- uses: actions/checkout@v4
- name: go vet
run: go vet ./...
# The race detector needs cgo, so this cannot run with CGO_ENABLED=0.
- name: go test -race
run: go test ./... -race
- name: Static build
run: CGO_ENABLED=0 go build -trimpath -o gateway ./cmd/gateway
- name: govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
continue-on-error: true
image:
name: Build and push image
runs-on: docker
needs: verify
# Only publish from main. PRs get the verify job and nothing else, so an
# untrusted branch can never push a tag the cluster might pull.
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
container:
image: docker:27-cli
# The runner's dind sidecar shares the pod network and the mTLS cert
# emptyDir, so the daemon is reachable on localhost with the client certs
# dind generated at startup.
options: --network host
env:
DOCKER_HOST: tcp://localhost:2376
DOCKER_TLS_VERIFY: "1"
DOCKER_CERT_PATH: /docker-certs/client
steps:
- uses: actions/checkout@v4
- name: Registry login
run: |
echo "${FORGEJO_PAT}" | docker login "${REGISTRY}" \
--username rock --password-stdin
env:
FORGEJO_PAT: ${{ secrets.FORGEJO_RIOTPIAO_PAT }}
# SHA tags only. 6.1 requires them, and :latest makes an Argo rollout
# non-deterministic — the same tag can resolve to different bits.
- name: Build
run: |
docker build \
--build-arg "VERSION=${GITHUB_SHA}" \
-t "${IMAGE}:${GITHUB_SHA}" \
.
- name: Push
run: docker push "${IMAGE}:${GITHUB_SHA}"
- name: Report digest
run: |
docker inspect --format='{{index .RepoDigests 0}}' "${IMAGE}:${GITHUB_SHA}"
+87
View File
@@ -0,0 +1,87 @@
# Forgejo Actions build — push image on main only.
# Tag is commit short SHA: unique, immutable, maps to exactly one commit.
# No write-back, no git push — ArgoCD Image Updater pulls new builds autonomously.
# Enabled by Stage 1 (B, C1).
name: Build
on:
push:
branches: [main]
env:
REGISTRY: forgejo.riotpiao.com
IMAGE: forgejo.riotpiao.com/rock/api-gateway
jobs:
build:
name: Build and push image
# golang, not a retired generic "docker" runner -- this repo is Go, and
# every runner now carries its own dind sidecar to build/push that
# repo's images. `container.image` below overrides the runner's own
# default (golang:1.25-bookworm) with docker:27-cli for this job only.
runs-on: golang
container:
image: docker:27-cli
# No `options: --network host` here -- act_runner ignores that per-job
# override and always decides the job container's network from its own
# config.yaml (container.network), which defaults to an isolated
# per-job bridge. Confirmed live: with that default, DOCKER_HOST=
# tcp://localhost:2376 resolved to the job container itself, not dind,
# so every command past `docker login` (which never touches DOCKER_HOST
# -- it only talks to the registry) failed with "Cannot connect to the
# Docker daemon". host networking is set once, for every job, in the
# runner's own Helm chart.
#
# The mTLS certs dind generates at startup are a separate gap: they
# live in an emptyDir mounted into the runner/dind containers, not into
# containers a workflow spins up. Job containers get no bind mounts at
# all unless the path is in the runner's container.valid_volumes
# allowlist (empty by default -- this exact mount was rejected until
# the runner's Helm chart added a config.yaml scoping valid_volumes to
# exactly this path).
volumes:
- /docker-certs/client:/docker-certs/client:ro
env:
DOCKER_HOST: tcp://localhost:2376
DOCKER_TLS_VERIFY: "1"
DOCKER_CERT_PATH: /docker-certs/client
steps:
# actions/checkout@v4 is a JS action -- Forgejo Actions execs it with
# `node`, which docker:27-cli (Alpine) doesn't ship. Without this the
# checkout step fails with "exec: node: executable file not found in
# $PATH" before any of the job's own steps run. Verified locally:
# `apk add --no-cache nodejs git` in this exact image gets node v22 +
# git 2.47, and the checkout action's dist/index.js then actually
# executes (confirmed by running it directly) instead of failing on a
# missing binary.
- name: install node (required by JS-based actions)
run: apk add --no-cache nodejs git
- uses: actions/checkout@v4
- name: Get short SHA
id: sha
run: |
SHORT_SHA=$(git rev-parse --short HEAD)
echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT
- name: Registry login
run: |
echo "${GITHUB_TOKEN}" | docker login "${REGISTRY}" \
--username rock --password-stdin
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build
run: |
docker build \
--build-arg "VERSION=${{ steps.sha.outputs.short_sha }}" \
-t "${IMAGE}:${{ steps.sha.outputs.short_sha }}" \
.
- name: Push
run: docker push "${IMAGE}:${{ steps.sha.outputs.short_sha }}"
- name: Report digest
run: |
docker inspect --format='{{index .RepoDigests 0}}' "${IMAGE}:${{ steps.sha.outputs.short_sha }}"
+56
View File
@@ -0,0 +1,56 @@
# Forgejo Actions CI — verification only (vet, test, build).
# Build and push happens in build.yaml on main push.
#
# Path is .gitea/workflows/, not .forgejo/workflows/ or .github/workflows/.
# Verified live against this instance (Forgejo 1.27.0, forgejo.riotpiao.com)
# on 2026-08-21: a .forgejo/workflows/*.yaml file never creates an action_run
# row on push, not once, for any repo -- confirmed both from application logs
# (silent, no error) and directly in the action_run table. A .gitea/workflows
# file with an identical job spec fires immediately. .github/workflows also
# gets scanned (that's how the old, dead ubuntu-latest CI on this repo and on
# kmsvc-manage both got action_run rows despite matching no runner) -- so
# .forgejo/workflows/ specifically appears unsupported on this instance/version,
# not workflow detection being off in general.
#
# runs-on: golang -- the generic "docker" runner was retired in favor of
# per-language runners (golang/node/rust), each with its own dind sidecar.
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
verify:
name: Test, vet, build
runs-on: golang
container:
image: golang:1.25-bookworm
steps:
# actions/checkout@v4 is a JS action -- Forgejo Actions execs it with
# `node`, which golang:1.25-bookworm doesn't ship. Without this the
# checkout step fails with "exec: node: executable file not found in
# $PATH" before any of the job's own steps run. Same fix already in use
# in kmsvc-manage's ci.yaml; carried over here.
- name: install node (required by JS-based actions)
run: apt-get update && apt-get install -y --no-install-recommends nodejs ca-certificates git
- uses: actions/checkout@v4
- name: go vet
run: go vet ./...
# The race detector needs cgo, so this cannot run with CGO_ENABLED=0.
- name: go test -race
run: go test ./... -race
- name: Static build
run: CGO_ENABLED=0 go build -trimpath -o gateway ./cmd/gateway
- name: govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
continue-on-error: true
+3 -3
View File
@@ -9,9 +9,9 @@ import (
"os/signal" "os/signal"
"syscall" "syscall"
"github.com/Riotpiaole/homelab-frontend/internal/config" "forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
"github.com/Riotpiaole/homelab-frontend/internal/proxy" "forgejo.riotpiao.com/rock/homelab-frontend/internal/proxy"
"github.com/Riotpiaole/homelab-frontend/internal/server" "forgejo.riotpiao.com/rock/homelab-frontend/internal/server"
) )
func main() { func main() {
+88
View File
@@ -0,0 +1,88 @@
package main
import (
"context"
"flag"
"fmt"
"log"
"time"
"go.temporal.io/sdk/client"
"forgejo.riotpiao.com/rock/homelab-frontend/internal/workflow"
)
func main() {
hostPort := flag.String("host", "temporal.temporal:7233", "Temporal server host:port")
namespace := flag.String("namespace", "production", "Temporal namespace")
taskQueue := flag.String("queue", "worker-production", "Task queue")
workflowType := flag.String("workflow", "HelloWorldWorkflow", "Workflow type")
workflowID := flag.String("id", "", "Workflow ID (auto-generated if not set)")
flag.Parse()
// Auto-generate ID
if *workflowID == "" {
*workflowID = fmt.Sprintf("test-%s-%d", *workflowType, time.Now().Unix())
}
log.Printf("Connecting to Temporal at %s (namespace: %s)", *hostPort, *namespace)
// Connect to Temporal
c, err := client.Dial(client.Options{
HostPort: *hostPort,
Namespace: *namespace,
})
if err != nil {
log.Fatalf("Failed to connect: %v", err)
}
defer c.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Start workflow
log.Printf("Starting %s (ID: %s)", *workflowType, *workflowID)
var run client.WorkflowRun
switch *workflowType {
case "HelloWorldWorkflow":
run, err = c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
ID: *workflowID,
TaskQueue: *taskQueue,
}, workflow.HelloWorldWorkflow, "World")
case "GreeterWorkflow":
run, err = c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
ID: *workflowID,
TaskQueue: *taskQueue,
}, workflow.GreeterWorkflow, "Alice")
case "ProcessOrderWorkflow":
run, err = c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
ID: *workflowID,
TaskQueue: *taskQueue,
}, workflow.ProcessOrderWorkflow, "ORDER-12345")
default:
log.Fatalf("Unknown workflow type: %s", *workflowType)
}
if err != nil {
log.Fatalf("Failed to start workflow: %v", err)
}
log.Printf("✓ Workflow submitted")
log.Printf(" Run ID: %s", run.GetRunID())
log.Printf(" Workflow ID: %s", *workflowID)
log.Printf(" Watch at: http://localhost:8080/namespaces/%s/workflows/%s", *namespace, *workflowID)
// Try to get result
ctx2, cancel2 := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel2()
var result string
if err := run.Get(ctx2, &result); err != nil {
log.Printf("⏳ Workflow executing (or error): %v", err)
} else {
log.Printf("✓ Result: %s", result)
}
}
+63
View File
@@ -0,0 +1,63 @@
package main
import (
"flag"
"log"
"os"
"os/signal"
"syscall"
"go.temporal.io/sdk/client"
"go.temporal.io/sdk/worker"
"forgejo.riotpiao.com/rock/homelab-frontend/internal/workflow"
)
func main() {
hostPort := flag.String("host", "temporal.temporal:7233", "Temporal server host:port")
namespace := flag.String("namespace", "production", "Temporal namespace")
taskQueue := flag.String("queue", "worker-production", "Task queue")
flag.Parse()
// Connect to Temporal server
c, err := client.Dial(client.Options{
HostPort: *hostPort,
Namespace: *namespace,
})
if err != nil {
log.Fatalf("Failed to connect to Temporal: %v", err)
}
defer c.Close()
log.Printf("Connected to Temporal at %s (namespace: %s)", *hostPort, *namespace)
// Create worker
w := worker.New(c, *taskQueue, worker.Options{})
// Register workflows
w.RegisterWorkflow(workflow.HelloWorldWorkflow)
w.RegisterWorkflow(workflow.GreeterWorkflow)
w.RegisterWorkflow(workflow.ProcessOrderWorkflow)
// Register activities
w.RegisterActivity(workflow.GreetActivity)
w.RegisterActivity(workflow.ValidateOrderActivity)
w.RegisterActivity(workflow.ProcessPaymentActivity)
w.RegisterActivity(workflow.NotifyCustomerActivity)
log.Printf("Starting worker on task queue: %s", *taskQueue)
// Handle graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
log.Println("Shutting down worker...")
w.Stop()
}()
// Run worker (blocks)
if err := w.Run(worker.InterruptCh()); err != nil {
log.Fatalf("Worker error: %v", err)
}
}
+32 -3
View File
@@ -1,5 +1,34 @@
module github.com/Riotpiaole/homelab-frontend module forgejo.riotpiao.com/rock/homelab-frontend
go 1.25.0 go 1.25.4
require gopkg.in/yaml.v3 v3.0.1 // indirect require (
go.temporal.io/sdk v1.48.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
github.com/nexus-rpc/nexus-proto-annotations v0.1.0 // indirect
github.com/nexus-rpc/sdk-go v0.7.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/robfig/cron v1.2.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.10.0 // indirect
go.temporal.io/api v1.63.4 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
google.golang.org/grpc v1.82.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)
+118
View File
@@ -1,3 +1,121 @@
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw=
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/nexus-rpc/nexus-proto-annotations v0.1.0 h1:2fELd+9sqUtNu6Fg//pw8YFsxOvp8vZ8hfP0nHhNI80=
github.com/nexus-rpc/nexus-proto-annotations v0.1.0/go.mod h1:n3UjF1bPCW8llR8tHvbxJ+27yPWrhpo8w/Yg1IOuY0Y=
github.com/nexus-rpc/sdk-go v0.7.0 h1:38NrfY5rLnZAiMMs2ZfCKI/CSDzdfJG+27iAgfA8bUI=
github.com/nexus-rpc/sdk-go v0.7.0/go.mod h1:FHdPfVQwRuJFZFTF0Y2GOAxCrbIBNrcPna9slkGKPYk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.temporal.io/api v1.63.4 h1:p4dVIAP3dJop0MfcyH9QSzjU7+V/ttLDhxFhSRUar58=
go.temporal.io/api v1.63.4/go.mod h1:SrlW2JMwVlDP4nRWSNznUFqnSHd+YeMDS1BkYo63HCQ=
go.temporal.io/sdk v1.48.0 h1:WDctKDVuh0Z8Nf7euAyqs/EwcPg1JTIIq1Fut8Tq118=
go.temporal.io/sdk v1.48.0/go.mod h1:SHv3+fLzD0GGZAwf0xNSvu8UmO1nFgG9WBSYoowApIk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec=
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+1 -1
View File
@@ -3,7 +3,7 @@ package config_test
import ( import (
"testing" "testing"
"github.com/Riotpiaole/homelab-frontend/internal/config" "forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
) )
// TestLoadRoutesMissingAuthRequired tests that the auth-required flag is not a silent default: // TestLoadRoutesMissingAuthRequired tests that the auth-required flag is not a silent default:
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/Riotpiaole/homelab-frontend/internal/config" "forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
) )
// TestLoadRoutesValidConfig tests that a valid configuration loads correctly. // TestLoadRoutesValidConfig tests that a valid configuration loads correctly.
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/Riotpiaole/homelab-frontend/internal/config" "forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
) )
// TestLoadIntegration tests the full Load function with CONFIG_PATH env var // TestLoadIntegration tests the full Load function with CONFIG_PATH env var
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/Riotpiaole/homelab-frontend/internal/config" "forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
) )
// TestBodyBasedDispatch verifies that /v1/chat/completions routes based on model field. // TestBodyBasedDispatch verifies that /v1/chat/completions routes based on model field.
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/Riotpiaole/homelab-frontend/internal/config" "forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
) )
// TestBodySizeCapExact verifies that a body exactly at the cap is accepted. // TestBodySizeCapExact verifies that a body exactly at the cap is accepted.
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/Riotpiaole/homelab-frontend/internal/config" "forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
) )
// TestEmbeddingsPassthroughNoRewrite verifies /v1/embeddings is not rewritten // TestEmbeddingsPassthroughNoRewrite verifies /v1/embeddings is not rewritten
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/Riotpiaole/homelab-frontend/internal/config" "forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
) )
// TestHeaderHygiene verifies that headers are properly filtered and forwarded. // TestHeaderHygiene verifies that headers are properly filtered and forwarded.
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/Riotpiaole/homelab-frontend/internal/config" "forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
) )
// ModelListResponse represents the response shape for GET /v1/models // ModelListResponse represents the response shape for GET /v1/models
+2 -2
View File
@@ -13,8 +13,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/Riotpiaole/homelab-frontend/internal/config" "forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
"github.com/Riotpiaole/homelab-frontend/internal/logging" "forgejo.riotpiao.com/rock/homelab-frontend/internal/logging"
) )
// Handler is a reverse proxy that routes requests to configured upstreams. // Handler is a reverse proxy that routes requests to configured upstreams.
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/Riotpiaole/homelab-frontend/internal/config" "forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
) )
func TestProxyBasic(t *testing.T) { func TestProxyBasic(t *testing.T) {
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"github.com/Riotpiaole/homelab-frontend/internal/config" "forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
) )
// RouteRequest determines which upstream should handle the request. // RouteRequest determines which upstream should handle the request.
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/Riotpiaole/homelab-frontend/internal/config" "forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
) )
// TestSSEUnbuffered verifies that SSE events stream to the client without buffering. // TestSSEUnbuffered verifies that SSE events stream to the client without buffering.
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/Riotpiaole/homelab-frontend/internal/config" "forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
) )
// TestConnectTimeout verifies that connections fail at the configured timeout. // TestConnectTimeout verifies that connections fail at the configured timeout.
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/Riotpiaole/homelab-frontend/internal/config" "forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
) )
// OpenAI-style tool definition // OpenAI-style tool definition
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/Riotpiaole/homelab-frontend/internal/config" "forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
) )
// RFC 9457 Problem Details // RFC 9457 Problem Details
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/Riotpiaole/homelab-frontend/internal/server" "forgejo.riotpiao.com/rock/homelab-frontend/internal/server"
) )
// TestHealthEndpoints verifies health endpoint behavior. // TestHealthEndpoints verifies health endpoint behavior.
+11 -5
View File
@@ -21,11 +21,17 @@ type Server struct {
func New(listenAddr string, shutdownTimeout time.Duration, handler http.Handler) *Server { func New(listenAddr string, shutdownTimeout time.Duration, handler http.Handler) *Server {
return &Server{ return &Server{
httpServer: &http.Server{ httpServer: &http.Server{
Addr: listenAddr, Addr: listenAddr,
Handler: handler, Handler: handler,
ReadTimeout: 15 * time.Second, // ReadHeaderTimeout (not ReadTimeout) and a long WriteTimeout: both
WriteTimeout: 15 * time.Second, // ReadTimeout and WriteTimeout are absolute deadlines covering the
IdleTimeout: 60 * time.Second, // whole request/response body, not inactivity timeouts -- a 15s
// WriteTimeout here was killing in-progress LLM SSE streams (proxy.go's
// outbound transport deliberately avoids this same mistake). Mirrors
// the edge nginx Ingress's proxy-read/send-timeout of 3600s.
ReadHeaderTimeout: 15 * time.Second,
WriteTimeout: 1 * time.Hour,
IdleTimeout: 60 * time.Second,
}, },
shutdownTimeout: shutdownTimeout, shutdownTimeout: shutdownTimeout,
healthChecker: NewHealthChecker(false, false), healthChecker: NewHealthChecker(false, false),
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/Riotpiaole/homelab-frontend/internal/server" "forgejo.riotpiao.com/rock/homelab-frontend/internal/server"
) )
// TestGracefulShutdown verifies that: // TestGracefulShutdown verifies that:
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/Riotpiaole/homelab-frontend/internal/config" "forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
) )
// startForTest binds the stub on an ephemeral port so the suite does not fight // startForTest binds the stub on an ephemeral port so the suite does not fight
+56
View File
@@ -0,0 +1,56 @@
package workflow
import (
"context"
"fmt"
"time"
"go.temporal.io/sdk/workflow"
)
// HelloWorldWorkflow is a simple hello world workflow
func HelloWorldWorkflow(ctx workflow.Context, name string) (string, error) {
opts := workflow.ActivityOptions{
StartToCloseTimeout: time.Minute,
}
ctx = workflow.WithActivityOptions(ctx, opts)
var result string
if err := workflow.ExecuteActivity(ctx, GreetActivity, name).Get(ctx, &result); err != nil {
return "", err
}
return result, nil
}
// GreetActivity greets someone
func GreetActivity(ctx context.Context, name string) (string, error) {
return fmt.Sprintf("Hello, %s!", name), nil
}
// ValidateOrderActivity validates an order
func ValidateOrderActivity(ctx context.Context, orderID string) (bool, error) {
// Simulate validation
if orderID == "" {
return false, nil
}
return true, nil
}
// ProcessPaymentActivity processes payment
func ProcessPaymentActivity(ctx context.Context, orderID string) (string, error) {
// Simulate payment processing
return fmt.Sprintf("payment-%s", orderID[:min(len(orderID), 3)]), nil
}
// NotifyCustomerActivity sends notification
func NotifyCustomerActivity(ctx context.Context, orderID string) (string, error) {
// Simulate notification
return fmt.Sprintf("notified for order %s", orderID), nil
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
+120
View File
@@ -0,0 +1,120 @@
package workflow
import (
"log"
"os"
"os/signal"
"syscall"
"time"
"go.temporal.io/sdk/client"
"go.temporal.io/sdk/worker"
"go.temporal.io/sdk/temporal"
"go.temporal.io/sdk/workflow"
)
// WorkerConfig holds worker configuration
type WorkerConfig struct {
HostPort string
Namespace string
TaskQueue string
}
// NewWorker creates and starts a Temporal worker
func NewWorker(cfg WorkerConfig) error {
// Connect to Temporal server
c, err := client.Dial(client.Options{
HostPort: cfg.HostPort,
Namespace: cfg.Namespace,
})
if err != nil {
return err
}
defer c.Close()
log.Printf("Connected to Temporal at %s (namespace: %s)", cfg.HostPort, cfg.Namespace)
// Create worker
w := worker.New(c, cfg.TaskQueue, worker.Options{})
// Register workflows
w.RegisterWorkflow(HelloWorldWorkflow)
w.RegisterWorkflow(GreeterWorkflow)
w.RegisterWorkflow(ProcessOrderWorkflow)
// Register activities
w.RegisterActivity(GreetActivity)
w.RegisterActivity(ValidateOrderActivity)
w.RegisterActivity(ProcessPaymentActivity)
w.RegisterActivity(NotifyCustomerActivity)
// Start worker (blocks until signal received)
log.Printf("Starting worker on task queue: %s", cfg.TaskQueue)
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
if err := w.Run(worker.InterruptCh()); err != nil {
log.Fatal(err)
}
}()
// Wait for shutdown signal
<-sigChan
log.Println("Shutting down worker...")
w.Stop()
return nil
}
// ProcessOrderWorkflow demonstrates multi-step workflow with activities
func ProcessOrderWorkflow(ctx workflow.Context, orderID string) (string, error) {
opts := workflow.ActivityOptions{
StartToCloseTimeout: 5 * time.Minute,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: time.Second,
BackoffCoefficient: 2.0,
MaximumInterval: time.Minute,
MaximumAttempts: 3,
},
}
ctx = workflow.WithActivityOptions(ctx, opts)
// Step 1: Validate order
var validated bool
if err := workflow.ExecuteActivity(ctx, ValidateOrderActivity, orderID).Get(ctx, &validated); err != nil {
return "", err
}
if !validated {
return "", temporal.NewApplicationError("invalid order", "InvalidOrder")
}
// Step 2: Process payment
var paymentID string
if err := workflow.ExecuteActivity(ctx, ProcessPaymentActivity, orderID).Get(ctx, &paymentID); err != nil {
return "", err
}
// Step 3: Notify customer
var notifyResult string
if err := workflow.ExecuteActivity(ctx, NotifyCustomerActivity, orderID).Get(ctx, &notifyResult); err != nil {
return "", err
}
return paymentID, nil
}
// GreeterWorkflow is a multi-step workflow
func GreeterWorkflow(ctx workflow.Context, name string) (string, error) {
opts := workflow.ActivityOptions{
StartToCloseTimeout: 5 * time.Minute,
}
ctx = workflow.WithActivityOptions(ctx, opts)
var result string
if err := workflow.ExecuteActivity(ctx, GreetActivity, name).Get(ctx, &result); err != nil {
return "", err
}
return result, nil
}
+4
View File
@@ -21,6 +21,10 @@ spec:
labels: labels:
app: api-gateway app: api-gateway
component: gateway component: gateway
# Required by the llm-serving-default-deny NetworkPolicy, which admits
# only pods labelled llm-client=true (from any namespace) on port 8080.
# Without it every upstream dial times out and dispatch returns 502.
llm-client: "true"
annotations: annotations:
prometheus.io/scrape: "true" prometheus.io/scrape: "true"
prometheus.io/port: "8080" prometheus.io/port: "8080"
+1 -1
View File
@@ -17,7 +17,7 @@ resources:
# kustomize edit set image forgejo.riotpiao.com/rock/api-gateway=:<sha> # kustomize edit set image forgejo.riotpiao.com/rock/api-gateway=:<sha>
images: images:
- name: forgejo.riotpiao.com/rock/api-gateway - name: forgejo.riotpiao.com/rock/api-gateway
newTag: v0.0.0 newTag: v0.1.1
commonLabels: commonLabels:
app: api-gateway app: api-gateway
+6 -4
View File
@@ -34,15 +34,17 @@ spec:
- to: - to:
- namespaceSelector: - namespaceSelector:
matchLabels: matchLabels:
name: kube-system kubernetes.io/metadata.name: kube-system
ports: ports:
- protocol: UDP - protocol: UDP
port: 53 port: 53
- protocol: TCP
port: 53
# Allow to upstreams (LLM services in llm-serving namespace) # Allow to upstreams (LLM services in llm-serving namespace)
- to: - to:
- namespaceSelector: - namespaceSelector:
matchLabels: matchLabels:
name: llm-serving kubernetes.io/metadata.name: llm-serving
ports: ports:
- protocol: TCP - protocol: TCP
port: 80 port: 80
@@ -54,7 +56,7 @@ spec:
- to: - to:
- namespaceSelector: - namespaceSelector:
matchLabels: matchLabels:
name: llm-serving kubernetes.io/metadata.name: llm-serving
ports: ports:
- protocol: TCP - protocol: TCP
port: 8080 port: 8080
@@ -62,7 +64,7 @@ spec:
- to: - to:
- namespaceSelector: - namespaceSelector:
matchLabels: matchLabels:
name: atlas kubernetes.io/metadata.name: atlas
ports: ports:
- protocol: TCP - protocol: TCP
port: 8080 port: 8080