feat(web,ui): per-project git provider API base URL overrides

Per provider (github|gitlab|gitea) a project override stored in
~/.config/openchamber/projects/<projectId>.json under gitProviders wins
over the global settings.json value (precedence: project override > global
> built-in default). Server forge routes resolve the override per request
directory (worktree-aware via git-common-dir + containment + path fallback,
60s TTL cache); the override host is also accepted for remote parsing and
client detection. New GET/PUT /api/projects/:projectId/git-providers route;
client openchamberConfig preserves the server-owned gitProviders key;
Projects page gains a Git provider API base URLs section; detection store
hydrates per-project overrides (memory-only, server-authoritative).
This commit is contained in:
2026-08-17 09:57:54 +00:00
parent 697925ee0d
commit 66edc74fac
42 changed files with 1765 additions and 87 deletions
+7 -3
View File
@@ -40,6 +40,8 @@ interface OpenChamberConfig {
projectActions?: OpenChamberProjectAction[];
projectActionsPrimaryId?: string;
draftStarters?: DraftStarterRef[];
/** Written by the server via the git-providers route; the client must preserve it. */
gitProviders?: unknown;
}
type OpenChamberProjectActionPlatform = 'macos' | 'linux' | 'windows';
@@ -620,9 +622,10 @@ async function readOpenChamberConfig(project: ProjectRef): Promise<OpenChamberCo
/**
* Write the per-user config for a project.
*
* Server owns `version` and `scheduledTasks` keys; client reads them via their
* dedicated route and never round-trips them through this config write path to
* avoid a read-then-write race clobbering a concurrent server update.
* Server owns `version`, `scheduledTasks`, and `gitProviders` keys; client
* reads them via their dedicated routes and never round-trips them through
* this config write path to avoid a read-then-write race clobbering a
* concurrent server update.
*/
async function writeOpenChamberConfig(
project: ProjectRef,
@@ -661,6 +664,7 @@ async function writeOpenChamberConfig(
const serverOwned: Record<string, unknown> = {};
if (existing.version !== undefined) serverOwned.version = existing.version;
if (existing.scheduledTasks !== undefined) serverOwned.scheduledTasks = existing.scheduledTasks;
if (existing.gitProviders !== undefined) serverOwned.gitProviders = existing.gitProviders;
const content = JSON.stringify({
...existing,