diff --git a/Backend/src/index.ts b/Backend/src/index.ts index 56f7379..85482d0 100644 --- a/Backend/src/index.ts +++ b/Backend/src/index.ts @@ -29,9 +29,16 @@ export const PORT = env.PORT; export { prisma }; -const CORS_DOMAINS = env.CORS_URLS.split(",").map((s) => s.trim()); +let CORS_DOMAINS = env.CORS_URLS.split(",").map((s) => s.trim()); CORS_DOMAINS.push(UI_URL); CORS_DOMAINS.push(REACT_APP_API_URL.replace(/\/+$/, "")); + +// If an origin is "*", clear the list and add a star +// ? This is for PR-Previews especially, which have a dynamic origin and cannot be enumerated in advance +if (CORS_DOMAINS.includes("*")) { + CORS_DOMAINS = ["*"]; +} + initCorsDomains(CORS_DOMAINS); if (env.NODE_ENV !== "test") { @@ -86,7 +93,12 @@ server.notFound(async (ctr) => { // Serve a matching build file (assets, favicons, etc.) if present. const file = resolveStaticFile(path); - if (file) return ctr.status(200, "OK").printFile(file, { addTypes: true }); + if (file) { + // rjweb's type map has no entry for .webmanifest, and browsers want the + // PWA manifest served as application/manifest+json. + if (file.endsWith(".webmanifest")) ctr.headers.set("Content-Type", "application/manifest+json"); + return ctr.status(200, "OK").printFile(file, { addTypes: !file.endsWith(".webmanifest") }); + } // SPA fallback — hand any other route to the React app. if (hasUiIndex) return ctr.status(200, "OK").printFile(uiIndexPath, { addTypes: true }); diff --git a/Backend/src/lib/middlewares/cors.ts b/Backend/src/lib/middlewares/cors.ts index c39657f..a5cea5f 100644 --- a/Backend/src/lib/middlewares/cors.ts +++ b/Backend/src/lib/middlewares/cors.ts @@ -29,6 +29,26 @@ export const corsMiddleware = new Middleware<{}, {}>("Custom CORS", "1.0.0") const origin = ctr.headers.get("origin"); + // if the list is only a single star, allow all origins + if (CORS_DOMAINS.length === 1 && CORS_DOMAINS[0] === "*") { + if (origin) { + ctr.headers.set("Access-Control-Allow-Origin", origin); + ctr.headers.set("Vary", "Origin"); + ctr.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH"); + ctr.headers.set( + "Access-Control-Allow-Headers", + ctr.headers.get("access-control-request-headers") || "content-type, x-api-key", + ); + ctr.headers.set("Access-Control-Allow-Credentials", "true"); + } + if (ctr.url.method === "OPTIONS") { + ctr.headers.set("Access-Control-Max-Age", "86400"); + ctr.headers.set("Content-Length", "0"); + return end(ctr.status(ctr.$status.NO_CONTENT).print("")); + } + return; + } + if (origin && !CORS_DOMAINS.includes(origin)) { // Agent/API traffic (no browser origin) is unaffected; only browser // requests from disallowed origins are blocked. diff --git a/README.md b/README.md index ad6dee5..9a85b11 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,8 @@ Backend/ migrations/ UI/ React + Tailwind frontend (Vite), builds to UI/build/ Dockerfile multi-stage build (UI then backend) -docker-compose.yml backend + Postgres +docker-compose.yml backend (pre-built image) + Postgres +docker-compose.local.yml same stack, built from this checkout example.env .gitea/workflows/deploy.yml ``` @@ -155,6 +156,26 @@ docker compose up -d # Open http://localhost:5000 — the first account to register becomes admin. ``` +### Building the image locally + +To run from this checkout instead of the pre-built image — no `.env` needed, every +variable falls back to a working localhost default: + +```bash +docker compose -f docker-compose.local.yml up -d --build +``` + +BuildKit builds both Dockerfile stages in parallel, which can exceed Docker +Desktop's memory limit during `pnpm install`. If the build fails with "cannot +allocate memory", warm the first stage on its own and retry: + +```bash +docker build --target build -t patchpass-build . +``` + +> The local compose file ships a placeholder `INSTANCE_SECRET`. Override it before +> using the instance for anything real. + --- ## Development setup diff --git a/UI/index.html b/UI/index.html index 0593d03..cdbca07 100644 --- a/UI/index.html +++ b/UI/index.html @@ -2,9 +2,27 @@ - - + + + + + + + + + + + + + + + PatchPass diff --git a/UI/public/apple-touch-icon.png b/UI/public/apple-touch-icon.png new file mode 100644 index 0000000..7f14922 Binary files /dev/null and b/UI/public/apple-touch-icon.png differ diff --git a/UI/public/icon-192.png b/UI/public/icon-192.png new file mode 100644 index 0000000..d4a96fc Binary files /dev/null and b/UI/public/icon-192.png differ diff --git a/UI/public/icon-512.png b/UI/public/icon-512.png new file mode 100644 index 0000000..87aaac6 Binary files /dev/null and b/UI/public/icon-512.png differ diff --git a/UI/public/icon.svg b/UI/public/icon.svg new file mode 100644 index 0000000..859e4e0 --- /dev/null +++ b/UI/public/icon.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/UI/public/manifest.webmanifest b/UI/public/manifest.webmanifest new file mode 100644 index 0000000..640ae06 --- /dev/null +++ b/UI/public/manifest.webmanifest @@ -0,0 +1,24 @@ +{ + "name": "PatchPass", + "short_name": "PatchPass", + "description": "A human approval layer for AI agents. Review changes, approve intent, let agents proceed.", + "start_url": "/", + "scope": "/", + "display": "standalone", + "display_override": ["standalone", "minimal-ui"], + "orientation": "any", + "background_color": "#0a0e17", + "theme_color": "#0a0e17", + "categories": ["developer", "productivity", "utilities"], + "icons": [ + { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, + { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, + { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }, + { "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any" } + ], + "shortcuts": [ + { "name": "Awaiting review", "url": "/", "description": "Requests waiting for your review" }, + { "name": "All requests", "url": "/requests", "description": "Full change request history" }, + { "name": "Agents", "url": "/agents", "description": "Manage your agents and API keys" } + ] +} diff --git a/UI/src/components/AgentModals.tsx b/UI/src/components/AgentModals.tsx index ad78967..e5632fb 100644 --- a/UI/src/components/AgentModals.tsx +++ b/UI/src/components/AgentModals.tsx @@ -5,6 +5,7 @@ import type { Agent } from "../api/types"; import { Modal } from "./Modal"; import { Button, Input, Textarea } from "./ui"; import { CodeBlock } from "./CodeBlock"; +import { ScrollRow } from "./ScrollRow"; // ── Create / edit form ─────────────────────────────────────────────────────────── @@ -47,9 +48,7 @@ export function AgentFormModal({ icon_url: iconUrl.trim() || null, max_pending_requests: maxPending, }; - const saved = editing - ? await agentsApi.update(agent!.id, payload) - : await agentsApi.create(payload); + const saved = editing ? await agentsApi.update(agent!.id, payload) : await agentsApi.create(payload); toast.success(editing ? "Agent updated" : "Agent created"); onSaved(saved, !editing); onClose(); @@ -68,17 +67,17 @@ export function AgentFormModal({ title={editing ? "Edit agent" : "New agent"} footer={ <> - - } >
- setName(e.target.value)} autoFocus /> + setName(e.target.value)} />