mirror of
https://github.com/Dvorinka/Primora.git
synced 2026-10-28 15:51:23 +00:00
Scheduled jobs (migration 00009): cron expressions + @every descriptors, in-process scheduler (15s scan, missed-run collapse), signed HTTP POST delivery (X-Primora-Signature), AES-256-GCM sealed secrets returned once, run history with status/duration/http-code, manual execution, retention pruning. Viewer reads, admin/developer writes. Domain events: publishEvent fans document/object/issue/deploy/job mutations to realtime subscribers and matching webhooks through one path. Webhook event filter extended to job.run, document.*, object.*. Realtime: GET /projects/:id/realtime/stream SSE with 25s keepalives and ?token=/?api_key= auth for EventSource clients. Automation dashboard page: Schedules tab (CRUD, run history, manual run, secret-once banner) + Live events tab (live badge, pause/clear). Demo mode parity. primora jobs:* CLI group, six primora_*_job* MCP tools, AutomationService in the generated client. Fixes folded in: gzip middleware buffered SSE flushes (broke EventSource on both streams — */stream now bypasses compression); event payloads double-marshaled json.RawMessage as base64; compose hardcoded localhost trusted origins so non-default ports broke sign-in; frontend auth base URL baked an absolute localhost instead of resolving /auth on origin. Verified live: stack up on :8085, signed delivery HMAC-checked at sink, @every 30s schedule firing, run history rendering, document.created landing in the live events feed. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
58 lines
1.2 KiB
Go
58 lines
1.2 KiB
Go
package middleware
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"io"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
var gzipPool = sync.Pool{
|
|
New: func() any {
|
|
w, _ := gzip.NewWriterLevel(io.Discard, gzip.DefaultCompression)
|
|
return w
|
|
},
|
|
}
|
|
|
|
type gzipWriter struct {
|
|
gin.ResponseWriter
|
|
writer *gzip.Writer
|
|
}
|
|
|
|
func (g *gzipWriter) Write(data []byte) (int, error) {
|
|
// Compressed output never matches a Content-Length set by a handler —
|
|
// drop it so net/http falls back to chunked encoding.
|
|
g.Header().Del("Content-Length")
|
|
return g.writer.Write(data)
|
|
}
|
|
|
|
func Compression() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if !strings.Contains(c.Request.Header.Get("Accept-Encoding"), "gzip") {
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
// Skip compression for small responses or streaming — gzip buffers
|
|
// writes, which breaks SSE flush semantics.
|
|
if c.Request.Method == "HEAD" ||
|
|
c.Request.URL.Path == "/api/v1/health/liveness" ||
|
|
strings.HasSuffix(c.Request.URL.Path, "/stream") {
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
gz := gzipPool.Get().(*gzip.Writer)
|
|
defer gzipPool.Put(gz)
|
|
gz.Reset(c.Writer)
|
|
defer gz.Close()
|
|
|
|
c.Header("Content-Encoding", "gzip")
|
|
c.Header("Vary", "Accept-Encoding")
|
|
c.Writer = &gzipWriter{ResponseWriter: c.Writer, writer: gz}
|
|
c.Next()
|
|
}
|
|
}
|