diff --git a/API.md b/API.md index 87787a3..298faea 100644 --- a/API.md +++ b/API.md @@ -13,6 +13,10 @@ Default bind: `127.0.0.1:9911` | GET | `/` | Web UI (served from `frontend/index.html`) | | GET | `/health` | Health check | | GET | `/status` | All apps with routes, container status and running operation (what the UI polls) | +| GET | `/integrations` | Forgejo connection (`configured`, `url`, `has_token`, `user`) and the SSH deploy public key | +| POST | `/integrations/forgejo` | `{"token": "..."}` — verify against Forgejo and store; `""` disconnects | +| GET | `/forgejo/repos?q=` | Search repositories visible to the stored token (public ones without) | +| GET | `/forgejo/branches?repo=owner/name` | Branch names of a Forgejo repository | ### Apps — Read @@ -26,7 +30,8 @@ Default bind: `127.0.0.1:9911` | GET | `/apps//logs?tail=N` | Fetch last N log lines (default 100) | | GET | `/apps//backups` | List available backups | | GET | `/apps//backups/` | Download backup zip | -| GET | `/apps//repo` | Git source info (URL, branch, deployed commit, local changes) | +| GET | `/apps//env` | Environment variables: `{"vars": [{"key", "value"}], "inject": true}` | +| GET | `/apps//repo` | Git source info (URL, web URL, provider, branch, deployed commit, local changes, deploy key for ssh) | | GET | `/apps//repo?fetch=1` | Same, plus fetches the remote and reports `behind` / `remote` | | GET | `/apps//volumes` | Volumes the file browser can open | | GET | `/apps//volume/files?vol=&path=` | List a folder in a volume | @@ -50,28 +55,37 @@ Default bind: `127.0.0.1:9911` | POST | `/apps//restore` | Restore from backup | | POST | `/apps//remove` | Remove app | | POST | `/apps//repo-pull` | Git apps: fetch branch, hard-reset checkout to it, redeploy | +| POST | `/apps//env` | Replace environment variables: `{"vars": [...], "inject": true, "deploy": false}` | | POST | `/apps//volume-clear` | Stop the app and empty its default data folder | Write operations are serialised per app. While one runs, another write to the same app returns `409` with `{"ok": false, "error": "...", "busy": "deploy"}`. `deploy` returns the compose output in `stdout` (or `stderr` on failure). -### Create app (git repository) +### Create app (git repository, with environment variables) ```json { "name": "blog", "routes": [{"domain": "blog.srazka.com", "upstream": "127.0.0.1:18090"}], "auth": true, - "source_type": "github", - "github_url": "https://git.srazka.com/reudy-net/blog.git", - "github_branch": "", - "github_pat": "" + "source_type": "git", + "repo_url": "https://git.srazka.com/reudy-net/blog.git", + "repo_branch": "", + "use_forgejo_token": true, + "env": [{"key": "DATABASE_URL", "value": "postgres://..."}], + "env_inject": true } ``` -Any http(s) git host works. An empty branch uses the repository's default branch. -The compose file must be at the repository root. +- `repo_url` may be `https://…`, `ssh://git@host:port/owner/repo.git` or + `git@host:owner/repo.git`. ssh URLs use the panel's deploy key. +- `repo_token` sets an https token explicitly; `use_forgejo_token` uses the + token stored in Settings (only for URLs on the configured Forgejo host). +- An empty branch uses the repository's default branch. The compose file must + be at the repository root. +- The older `source_type: "github"` with `github_url` / `github_branch` / + `github_pat` is still accepted. ### Sync response (`repo-pull`) diff --git a/README.md b/README.md index cf6dc4f..5f9cc49 100644 --- a/README.md +++ b/README.md @@ -114,13 +114,49 @@ panelctl remove whoami ### Git-backed apps -Apps created from a repository (GitHub, Forgejo/Gitea or any https git host) -are cloned to `stacks//repo`. **Sync** fetches the configured branch and +Apps created from a repository (Forgejo, GitHub or any git host, over https or +ssh) are cloned to `stacks//repo`. + +**Forgejo.** `panel.nix` points the panel at the local Forgejo +(`PANEL_FORGEJO_URL`, `PANEL_FORGEJO_API_URL`, `PANEL_FORGEJO_SSH_URL`, taken +from `forgejo.nix`). In the panel's **Settings** you can connect a Forgejo +access token (read access to repositories and user). With it, the new-app +dialog lists your repositories and branches, and private ones are cloned over +https with the token. Without it, public repositories are listed and private +ones are cloned over ssh with the deploy key. Commit and compare links point at +Forgejo. The token is stored in `state/panel/forgejo-token` (mode 0600). + +**SSH / deploy key.** The panel generates an ed25519 key pair in +`state/panel/ssh/` the first time it is needed. Its public half is shown in +Settings (and next to ssh URLs); add it as a read-only deploy key to a +repository — or to your Forgejo account for access to all repositories — to +clone `ssh://git@git.srazka.com:14922/owner/repo.git` style URLs. **Sync** fetches the configured branch and hard-resets the checkout to it before redeploying, so the repository is the source of truth: compose edits made in the panel are discarded on the next sync (the UI warns about this). An access token for a private repository is stored in the clone's `.git/config`; use a read-only token. +### Environment variables + +Each app can have environment variables (the **Environment** tab, or when +creating the app; `.env` text can be pasted in). They are stored in +`state/env/.env` as `KEY=VALUE` lines (mode 0600) — outside the repository +and stack directory, so git syncs never touch them — and `panelctl` passes them +to every compose command: + +- They are always available for `${VAR}` interpolation in the compose file. + The UI points out variables the compose file uses without a default that + aren't set. +- With **Pass to every container** (the default), `deploy`/`restart` also + generate `stacks//.panel-env.yaml`, a compose override that lists the + keys under every service's `environment:`. Compose reads the values from its + own environment, so they are never quoted into YAML, and they take + precedence over values set in the compose file. + +Values must be single-line. Names that would change how podman/compose run +(`PATH`, `HOME`, `XDG_*`, `DOCKER_*`, `COMPOSE_*`, `PODMAN_*`, …) are rejected. +Changes apply on the next deploy. Backups do not include variables. + ### Concurrency `panel-api` handles requests concurrently, so a long deploy never blocks status diff --git a/frontend/index.html b/frontend/index.html index a7dfc4c..298ffb1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -450,6 +450,45 @@ } .segmented button.active { background: var(--surface); color: var(--text); box-shadow: var(--shadow); } + /* ── Repo picker ── */ + .picker-list { + margin-top: 6px; max-height: 232px; overflow: auto; + border: 1px solid var(--border); border-radius: 8px; + } + .picker-item { + display: flex; align-items: center; gap: 10px; width: 100%; + background: none; border: 0; border-top: 1px solid var(--border); + padding: 8px 10px; text-align: left; cursor: pointer; color: var(--text); font: inherit; + } + .picker-item:first-child { border-top: 0; } + .picker-item:hover, .picker-item:focus-visible { background: var(--surface-2); outline: none; } + .picker-item.selected { background: var(--accent-soft); } + .picker-item .icon { color: var(--muted); } + .picker-name { font-weight: 600; font-size: 13.5px; } + .picker-desc { color: var(--muted); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .picker-main { flex: 1; min-width: 0; } + + /* ── Settings ── */ + .settings-section + .settings-section { margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--border); } + .settings-section h3 { font-size: 14px; margin-bottom: 4px; } + .keybox { + margin-top: 10px; display: flex; gap: 8px; align-items: flex-start; + background: var(--surface-2); border-radius: 8px; padding: 10px 12px; + font: 12px/1.5 var(--mono); word-break: break-all; + } + .keybox code { flex: 1; background: none; padding: 0; } + + /* ── Environment editor ── */ + .env-row { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 3fr) 28px; gap: 6px; align-items: center; margin-bottom: 6px; } + .env-row .input { height: 32px; padding: 5px 9px; font: 13px var(--mono); } + .env-head { font-size: 11.5px; color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: .04em; margin-bottom: 4px; } + .env-details summary { cursor: pointer; list-style: none; } + .env-details summary::-webkit-details-marker { display: none; } + .env-details summary::before { content: "▸"; display: inline-block; width: 14px; color: var(--muted); transition: transform .15s; } + .env-details[open] summary::before { transform: rotate(90deg); } + .missing-vars { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; } + .chip.sm { height: 24px; padding: 0 9px; font: 12px var(--mono); } + /* ── Responsive ── */ @media (max-width: 760px) { .hide-sm { display: none !important; } @@ -467,6 +506,10 @@ .route-row .arrow { display: none; } .route-row .route-path { grid-column: 1 / 3; } .route-head { display: none; } + .env-row { grid-template-columns: minmax(0, 1fr) 28px; } + .env-row > :nth-child(2) { grid-column: 1; } + .env-row > :nth-child(3) { grid-column: 2; grid-row: 1; } + .env-head { display: none; } .file-row { grid-template-columns: 20px minmax(0, 1fr) auto; } .file-row > :nth-child(3), .file-row > :nth-child(4) { display: none; } .logs { height: 320px; } @@ -485,6 +528,7 @@
+ @@ -538,7 +582,7 @@
- +

@@ -549,24 +593,56 @@