The .page column was max-width 72rem but left-aligned, leaving a large
empty band on the right above ~1200px. margin-inline: auto centers it;
narrow widths are unchanged since the column fills the viewport anyway.
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The org/project/bucket/collection create forms required typing a slug
by hand. While the slug field is untouched (empty or still equal to the
derived value), naming the entity fills its slug; a manual slug edit
stops the derivation and is sanitized to slug form as you type.
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
stdin is the script pipe when run as `curl ... | bash`, so the -t 0
check silently skipped the domain question. Read from the controlling
terminal instead; no tty (CI/cron) still defaults to localhost.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
install.sh now asks one question on fresh installs — the URL the app is
opened on. Accepts https://primora.example.com (TLS at the user's proxy),
primora.example.com, or 192.168.1.50:8085; the port becomes NGINX_PORT.
The answer rewrites every public URL var and COOKIE_DOMAIN for real DNS
names. PUBLIC_URL env keeps it non-interactive; legacy DOMAIN/NGINX_PORT
still work; reruns over an existing .env skip the question and display
the URL from the file.
Also trust both URL schemes on the serving host in trustedOrigins —
external TLS termination makes nginx report http while browsers arrive
over https.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Self-hosted sign-up bounced back to the bootstrap screen for three
compounding reasons:
- better-auth's client resolves { data, error } on HTTP failure; the UI
ignored res.error and showed "Account created — signing you in", hiding
the real 403/429/500.
- trustedOrigins was a static localhost-only list. Browsing the instance
via LAN IP or custom port failed every auth POST once cookies existed.
Origins are now derived from the request's public host (forwarded by
nginx), so any origin serving the app works.
- hasMailTransport threw when core.settings was unreadable (fresh volume,
backend still migrating), so sign-up 500'd and the startup backfill
could crash-loop the service. Both now degrade to "no transport".
Verified live: LAN-style origin sign-up succeeds, missing core.settings
logs a warn instead of exiting, and the frontend shows real errors.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
SMTP-less self-hosted instances still marked every account
emailVerified=false and attempted a verification mail that could
never arrive. Now: no transport → accounts are created verified,
the send is skipped, and existing unverified rows are backfilled
on startup. .env.example stops defaulting SMTP_HOST to the
dev-only mailpit service.
Verified live: fresh signup returns emailVerified=true, legacy
unverified users flip on restart, sign-in unaffected.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
arm64 npm ci under QEMU SIGILLs on current node:20-alpine layers
(uncaught target signal 4) and wedges the build step — the v0.8.0
release stalled there twice. Build each platform natively and join
the arch tags with a manifests job.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
A "Set up with an AI agent" dialog that mints a write-scoped API key and
renders a copyable prompt handing a coding agent everything needed to wire
a repository into a Primora project: telemetry ingest for any language,
db-connections for tracked databases, MCP/CLI agent access, and
verification steps. Opens at the end of new-project onboarding; also
reachable from the project dashboard and Projects page settings.
Authored By: TDvorak <[email protected]>
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- Geist/Geist Mono via @fontsource inside the Vite bundle — the
Google Fonts <link> was blocked by the nginx CSP (style-src 'self'),
so the dashboard silently fell back and phoned home on self-hosted
instances. Fonts now serve from /assets under 'self' — CSP-clean,
airgapped, no console violation.
- mcp README: vault_exec described the old deny-list; rewritten for
the allow-list contract.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- POST /projects/:id/events accepts client-authored events restricted to
the custom.* namespace — system types can't be spoofed. They flow
through the same publishEvent fan-out: realtime, webhooks, and
event_pattern functions all fire.
- Presence: subscribe/unsubscribe bumps a per-project count and
broadcasts presence.update {online} to the realtime stream only —
deliberately not through publishEvent so webhooks and "*" functions
don't fire on connect noise. GET …/realtime/presence reads the count.
- @primora/client gains publish() and presence(); CLI gains events:send.
Verified live: presence 0->1 on subscribe, presence.update frame
delivered, custom event delivered to SSE + fired a custom.* function,
spoofed document.created rejected 400.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Each invoke runs in an ephemeral locked-down container — no network,
read-only rootfs, 128MiB/0.5CPU/64-pid caps, non-root, cap-drop ALL.
Source travels as base64 in the container env and materializes in the
tmpfs, so no bind mount is needed (Docker Desktop / remote daemons work).
Timeout force-removes the named container.
exec driver stays the default; the trust-model docs now describe both.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- tauri-plugin-updater + process: pubkey embedded in tauri.conf.json,
createUpdaterArtifacts, endpoint = GitHub latest.json manifest.
- Connect screen checks before auto-connect (bounded 5s) — a saved
URL would otherwise navigate away before the check resolves.
"Update & restart" downloads, verifies minisign, installs, relaunches.
- Release workflow signs updater artifacts with
TAURI_SIGNING_PRIVATE_KEY secrets, ships the .sig files, and assembles
latest.json from them. No key → no manifest → no update, never a
silent unsigned install.
- anchore/sbom-action emits CycloneDX SBOMs per GHCR image plus the
source tree, attached to every release.
Note: the signing keypair was generated locally
(~/.tauri/primora-updater.key); the private half must be added to repo
secrets before the next release for updates to activate.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
POST /buckets/:id/object-presigns mints SigV4 query-signed GET/PUT
URLs so clients move bytes directly against S3 — large transfers skip
the backend entirely.
- storage.Presigner optional interface; S3Store implements, local
driver returns a clear error (no external surface to sign).
- TTL 60s–1h (default 15m); download requires read role + existing
object row (no key probing); upload requires write role.
- S3_PUBLIC_ENDPOINT rewrites the client-facing host for internal
S3_ENDPOINTs — signatures bind the host, URLs are valid only where
minted.
- CLI: primora objects:presign <bucket> <key> [--upload] [--ttl].
Verified live against cloudserver: GET fetched the object with no
auth headers; PUT pushed 1 MB direct to S3; unsigned list → 403;
untracked-key download presign → 404.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Migration 00016: function_id on scheduled_jobs/inbound_hooks,
event_pattern on functions, 'event' in the run-trigger enum.
- Jobs deliver to a function instead of HTTP when function_id is set;
the run's status_code mirrors the process exit code. Clearing the
field returns the job to URL delivery.
- Inbound hooks gain mode "function" — received body becomes stdin
payload, acked immediately, run recorded async.
- event_pattern (document.*, *, exact type) fires the function detached
on matching publishEvent — never blocks realtime/webhook fan-out.
- One executeFunction path serves manual/schedule/hook/event; run rows
record the trigger source.
- Backend image now ships bun 1.3.14 so Functions work under compose.
Dashboard: job target picker, hook function mode, event-pattern field.
Verified live: job run → success (status_code 0), hook POST → run,
document.created → event run with correct payload.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
core.functions + core.function_runs: project-scoped code executed by a
runtime binary on the backend host (bun or deno per function). Payload
arrives on stdin + PRIMORA_PAYLOAD env; stdout/stderr captured per run
(capped), with status/exit code/duration. Invoke returns 502 + the
recorded run on failure so errors stay observable.
Dashboard gains a Functions page: code editor, JSON payload invoke,
live result, run history. Runner config via FUNCTIONS_RUNTIME /
FUNCTIONS_TIMEOUT_SECONDS / FUNCTIONS_MAX_OUTPUT_BYTES.
Verified live: bun ran code, stdin payload delivered, error path returns
502 with captured stderr; timeout kills at deadline.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
BACKEND_STORAGE_DRIVER=s3 swaps LocalStore for a stdlib-only SigV4
implementation — PutObject/GetObject/DeleteObject/CopyObject/ListObjectsV2
against any S3-compatible API (AWS, MinIO, Garage, R2). Storage becomes a
Store interface; storage_path records s3://bucket/key. Content-Length is
resolved by seeking or temp-file spooling — chunked uploads are rejected
by real S3 servers. Bucket auto-created on startup.
Verified live against zenko/cloudserver: bucket, put, get, copy, delete.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
HA scheduler: the job scheduler and alert evaluator hold session-scoped
PostgreSQL advisory locks on pinned pool connections — replicas scan for
due work exactly once, failover verified live with two replicas. Manual
and hook-triggered runs stay available on any replica.
Email surface: mailer renders registered templates (invitation); every
send is logged to core.email_log with status and error text. New
GET /projects/:id/emails endpoint; Members page shows an Email log card.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
core.inbound_hooks (migration 00013): POST /api/v1/hooks/<token> accepts
external events. event mode republishes as inbound.received through the
signed dispatcher + realtime; job mode triggers a scheduled-job run with
the received body as payload. Optional X-Primora-Signature HMAC verify.
Dashboard card on the Integrations page.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
core.alert_rules (migration 00012) — heartbeat_silence and error_spike
rules evaluated by a 60s backend ticker. Transition-based firing:
alert.fired on breach, alert.resolved on recovery, both first-class
webhook event types riding the signed dispatcher + realtime stream.
Dashboard manages rules on the Integrations page.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
createRealtimeClient opens the project SSE stream via fetch +
ReadableStream (EventSource cannot send X-API-Key), with capped
exponential-backoff reconnect and state callbacks. channel() filters by
event namespace — channel("documents").on("created", cb). Presence and
client-broadcast deliberately deferred.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
GET /collections/:id/documents accepts filter (field.op.value terms; eq neq
gt gte lt lte like in is; dotted paths index into document data) and order
(field.asc|desc). Whitelisted field/operator parsing with parameterized
values; plain lists keep the sqlc path. CLI gains `documents list
--collection --filter --order`; OpenAPI + generated client updated.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Release workflow now also fires on workflow_run when CI succeeds on
master; verify job skips unless package.json version lacks a matching
tag, so merges only release on a version bump. Tag pushes unchanged.
Authored By: TDvorak <[email protected]>
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Self-hosted instances are now email/password only — OAuth providers
(GitHub/Google/Discord/Microsoft) activate solely when PRIMORA_MANAGED=true,
reserved for the future hosted tier.
Sign-up lifecycle: the first public sign-up becomes the instance admin; after
bootstrap, sign-up closes by default. A better-auth databaseHook gates only
/sign-up/email, so admin-created users and OAuth flows bypass it. The login
page reads GET /instance/public to hide the sign-up tab, show the bootstrap
notice, and render only the providers the instance offers.
Instance settings live in core.settings (migration 00010) and resolve
in-app > env > built-in default, cached 15s. Platform admins (better-auth
user.role) manage sign-up, mail transport, and rate limits via
GET/PUT/DELETE /instance/settings or the new Settings → Instance tab. Secret
values are AES-256-GCM encrypted at rest with PRIMORA_ENCRYPTION_KEY, never
returned by the API, and decrypted by the auth service at send time.
Auth service reads the same settings table for the sign-up gate and mail
config, so in-app changes take effect without a restart.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Bump version to 0.6.0 and cut the changelog — phase 7 automation
(scheduled jobs, realtime SSE, automation UI, jobs CLI/MCP), security
hardening, and the new CI image pipeline + install.sh.
Authored By: TDvorak <[email protected]>
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
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>
- Remove managed connection seeding: platform-postgres/dragonfly carried
the backend's own DATABASE_URL/DRAGONFLY_URL into tenant projects where
any member could run arbitrary DBX queries against core tables
(Codex P1). NewPlatformService loses the seeds param; firstPartyDBSeeds
deleted.
- Seal db_connections passwords with the existing AES-256-GCM encryptor
(enc:v1:<b64> marker); decrypted only in dbxArgsFor. Legacy plaintext
values still read, but migration 00008 strips them (Codex P1).
- Migration 00008: delete is_managed rows, drop plaintext password keys.
- dbxConnectionName uses the full project UUID — the 8-hex prefix could
collide and alias another project's credentials (Codex P2).
- ci.yml triggers on master, not the nonexistent main/develop branches.
- demo-mode stubs renamed to user-managed connections; CHANGELOG notes
under Unreleased.
Authored By: TDvorak <[email protected]>
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- core.integrations (migration 00005) with AES-256-GCM-encrypted
credentials (PRIMORA_ENCRYPTION_KEY, hex/base64, dev fallback);
has_credentials serialization only — secrets never leave the server
- /projects/:id/integrations list/create/delete/test; health check hits
the stored base URL with a 5s timeout and records status
- Rybbit connector: GET …/integrations/:id/analytics proxies
/api/organizations + /api/sites/* server-side into a normalized shape
- core.webhooks + core.webhook_deliveries; CRUD, deliveries, test, and
POST …/deploy-markers. Events: issue.created (new fingerprint on
ingest), deploy.marker, webhook.test. X-Primora-Signature HMAC-SHA256
over the raw body; in-process dispatcher, 3 attempts, quadratic
backoff, 10s timeout, pending-scan on boot
- HTTPS enforced for public targets; HTTP allowed for loopback/private/
self-hosted sinks
- Integrations page (connectors + webhooks tabs, inline analytics,
delivery log, secret-shown-once), demo-mode stubs
- 12 new primora_* MCP tools
- workspace 0.4.0
Authored By: TDvorak <[email protected]>
Port IMS telemetry into Primora conventions:
- core.components/core.events (migration 00004 + sqlc) scoped to projects
- POST /api/v1/ingest — single or <=500-event batches via project prm_
keys (X-Primora-Key alias), auto-registering components, permissive CORS
- Issues: fingerprint-grouped errors with count/severity/first+last seen
- Reads: events w/ filters + before-cursor, components, stats (1h/24h/7d/30d
bucketed series + component health + totals + metric names), metric
series (avg/p50/p95/max)
- GET .../telemetry/stream — project-scoped SSE through an in-process hub
(buffered subs, drop-on-slow, 25s keepalives); ?api_key=/?token= auth
for EventSource
- Telemetry page: health/issues/events/metrics/integrate tabs, live badge,
390px-clean; demo stubs
- @primora/client SDK (createPrimoraClient: capture*/heartbeat/installAuto,
queued keepalive flushes)
- MCP: primora_list_issues/list_events/telemetry_stats/metric_series
- IMS README deprecation note pointing here
Also fixes workspace pins to 0.3.0 — npm ci in the frontend image was
resolving the unpublished 0.2.0 spec from the registry.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>