Author SHA1 Message Date
Labyricorn c8acc9e439 Gate unwired source management 2026-07-25 10:27:39 -07:00
11 changed files with 193 additions and 32 deletions
@@ -22,6 +22,11 @@ allow-listed, its API token comes from the process environment, requests use a
bounded timeout, and callers are rate-limited. Discovered entries are merged bounded timeout, and callers are rate-limited. Discovered entries are merged
atomically into inventory and begin with projection disabled. atomically into inventory and begin with projection disabled.
Until inventory is wired into `BuildInputLoader`, mutation, discovery,
connectivity-test, and content-scan endpoints return
`409 E_SOURCE_MANAGEMENT_NOT_WIRED`. The UI publishes the resolved build source,
labels inventory read-only, and disables the corresponding controls.
Source-management UX may add review and configuration workflows later, but any Source-management UX may add review and configuration workflows later, but any
accepted change must be committed to the Git-owned configuration before it can accepted change must be committed to the Git-owned configuration before it can
affect a release. affect a release.
+1 -1
View File
@@ -68,7 +68,7 @@ Validation failures produce a failed build record but never publish a partial re
## Current boundaries ## Current boundaries
- Theme packages are presentation data only; Node, shell, WASM, package-manager scripts, custom Liquid tags, and custom filters are not executed. - Theme packages are presentation data only; Node, shell, WASM, package-manager scripts, custom Liquid tags, and custom filters are not executed.
- The reference loader resolves the configured site-definition repository and supports separately pinned source snapshots through the typed boundary; source-management UX remains follow-up work governed by [ADR 0001](.pandaos/adrs/0001-git-source-management-boundary.md). - The reference loader resolves the configured site-definition repository and supports separately pinned source snapshots through the typed boundary. Repository inventory is explicitly read-only: its mutation and content-scan APIs return `409 E_SOURCE_MANAGEMENT_NOT_WIRED`, and the UI disables those actions until inventory feeds `BuildInputLoader`; see [ADR 0001](.pandaos/adrs/0001-git-source-management-boundary.md).
- Remote rsync deployment is not implemented. Push and connection-test endpoints fail closed with HTTP 501, and no configuration field can enable execution; see [ADR 0002](.pandaos/adrs/0002-remote-publication-not-implemented.md). - Remote rsync deployment is not implemented. Push and connection-test endpoints fail closed with HTTP 501, and no configuration field can enable execution; see [ADR 0002](.pandaos/adrs/0002-remote-publication-not-implemented.md).
See [docs/OPERATIONS.md](docs/OPERATIONS.md) for snapshot, release, promotion, failure, and rollback procedures. See [docs/OPERATIONS.md](docs/OPERATIONS.md) for snapshot, release, promotion, failure, and rollback procedures.
+1 -1
View File
@@ -11,7 +11,7 @@
"preview": "vite preview", "preview": "vite preview",
"clean": "rm -rf dist server.js", "clean": "rm -rf dist server.js",
"lint": "tsc --noEmit", "lint": "tsc --noEmit",
"test": "node --import tsx --test src/backend/buildEngine.test.ts src/backend/giteaDiscovery.test.ts src/backend/releaseLifecycle.test.ts src/backend/repositorySnapshot.test.ts" "test": "node --import tsx --test src/backend/buildEngine.test.ts src/backend/giteaDiscovery.test.ts src/backend/releaseLifecycle.test.ts src/backend/repositorySnapshot.test.ts src/backend/sourceManagement.test.ts"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.14", "@tailwindcss/vite": "^4.1.14",
+21
View File
@@ -14,6 +14,7 @@ import { ConfigAndAuditTab } from "./components/ConfigAndAuditTab";
import { import {
GitSource, GitSource,
SourceManagementCapability,
StyleConfigPackage, StyleConfigPackage,
StyleConfigInstance, StyleConfigInstance,
ContentItem, ContentItem,
@@ -27,11 +28,26 @@ import {
SiteConfig, SiteConfig,
} from "./types"; } from "./types";
const sourceManagementFallback: SourceManagementCapability = {
protocol: "labyricorn-source-management/v1",
buildIntegration: "not-wired",
mutationsEnabled: false,
buildSource: {
sourceId: "site-definition",
repository: "unresolved",
commit: null,
},
message:
"Source-management status is unavailable; mutations remain disabled.",
};
export default function App() { export default function App() {
const [activeTab, setActiveTab] = useState<string>("dashboard"); const [activeTab, setActiveTab] = useState<string>("dashboard");
const [siteConfig, setSiteConfig] = useState<SiteConfig | null>(null); const [siteConfig, setSiteConfig] = useState<SiteConfig | null>(null);
const [sources, setSources] = useState<GitSource[]>([]); const [sources, setSources] = useState<GitSource[]>([]);
const [sourceManagement, setSourceManagement] =
useState<SourceManagementCapability>(sourceManagementFallback);
const [stylePackages, setStylePackages] = useState<StyleConfigPackage[]>([]); const [stylePackages, setStylePackages] = useState<StyleConfigPackage[]>([]);
const [styleInstances, setStyleInstances] = useState<StyleConfigInstance[]>( const [styleInstances, setStyleInstances] = useState<StyleConfigInstance[]>(
[], [],
@@ -53,6 +69,7 @@ export default function App() {
const [ const [
cfgRes, cfgRes,
srcRes, srcRes,
srcMgmtRes,
pkgRes, pkgRes,
instRes, instRes,
cntRes, cntRes,
@@ -66,6 +83,7 @@ export default function App() {
] = await Promise.all([ ] = await Promise.all([
fetch("/api/site-config"), fetch("/api/site-config"),
fetch("/api/git-sources"), fetch("/api/git-sources"),
fetch("/api/source-management"),
fetch("/api/style-packages"), fetch("/api/style-packages"),
fetch("/api/style-instances"), fetch("/api/style-instances"),
fetch("/api/content"), fetch("/api/content"),
@@ -80,6 +98,7 @@ export default function App() {
if (cfgRes.ok) setSiteConfig(await cfgRes.json()); if (cfgRes.ok) setSiteConfig(await cfgRes.json());
if (srcRes.ok) setSources(await srcRes.json()); if (srcRes.ok) setSources(await srcRes.json());
if (srcMgmtRes.ok) setSourceManagement(await srcMgmtRes.json());
if (pkgRes.ok) setStylePackages(await pkgRes.json()); if (pkgRes.ok) setStylePackages(await pkgRes.json());
if (instRes.ok) setStyleInstances(await instRes.json()); if (instRes.ok) setStyleInstances(await instRes.json());
if (cntRes.ok) setContentItems(await cntRes.json()); if (cntRes.ok) setContentItems(await cntRes.json());
@@ -238,6 +257,7 @@ export default function App() {
<GitSourcesTab <GitSourcesTab
sources={sources} sources={sources}
credentials={credentials} credentials={credentials}
sourceManagement={sourceManagement}
onAddSource={handleAddGitSource} onAddSource={handleAddGitSource}
onTestSource={handleTestGitSource} onTestSource={handleTestGitSource}
/> />
@@ -247,6 +267,7 @@ export default function App() {
<ContentTab <ContentTab
contentItems={contentItems} contentItems={contentItems}
mediaAssets={mediaAssets} mediaAssets={mediaAssets}
sourceManagement={sourceManagement}
onTriggerDiscovery={handleTriggerContentDiscovery} onTriggerDiscovery={handleTriggerContentDiscovery}
/> />
)} )}
+2 -1
View File
@@ -2,6 +2,7 @@ import express from "express";
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import { cachedRepositoryPath, listRepositoryFiles } from "../gitOperations"; import { cachedRepositoryPath, listRepositoryFiles } from "../gitOperations";
import { requireSourceManagementBuildIntegration } from "../sourceManagement";
import { store } from "../store"; import { store } from "../store";
const router = express.Router(); const router = express.Router();
@@ -43,7 +44,7 @@ router.get("/content", (_req, res) => {
res.json(store.contentItems); res.json(store.contentItems);
}); });
router.post("/content/discover", (_req, res) => { router.post("/content/discover", requireSourceManagementBuildIntegration, (_req, res) => {
store.addAudit( store.addAudit(
"Discover Content", "Discover Content",
"config", "config",
+13 -5
View File
@@ -8,6 +8,10 @@ import {
synchronizeRepository, synchronizeRepository,
validateRepository, validateRepository,
} from "../gitOperations"; } from "../gitOperations";
import {
requireSourceManagementBuildIntegration,
sourceManagementCapability,
} from "../sourceManagement";
import { store } from "../store"; import { store } from "../store";
import { import {
fetchGiteaRepositories, fetchGiteaRepositories,
@@ -23,7 +27,11 @@ let discoveryInFlight: Promise<ReturnType<typeof toGitSource>[]> | null = null;
const errorMessage = (error: unknown): string => const errorMessage = (error: unknown): string =>
error instanceof Error ? error.message : String(error); error instanceof Error ? error.message : String(error);
router.post("/gitea/discover", async (req, res) => { router.get("/source-management", (_req, res) => {
res.json(sourceManagementCapability(store.siteConfig));
});
router.post("/gitea/discover", requireSourceManagementBuildIntegration, async (req, res) => {
const limit = discoveryLimiter.take(req.ip || req.socket.remoteAddress || "unknown"); const limit = discoveryLimiter.take(req.ip || req.socket.remoteAddress || "unknown");
if (!limit.allowed) { if (!limit.allowed) {
res.setHeader("Retry-After", String(limit.retryAfter)); res.setHeader("Retry-After", String(limit.retryAfter));
@@ -75,7 +83,7 @@ router.get("/git-sources", (_req, res) => {
res.json(store.gitSources); res.json(store.gitSources);
}); });
router.post("/git-sources", async (req, res) => { router.post("/git-sources", requireSourceManagementBuildIntegration, async (req, res) => {
const body = req.body as Partial<GitSource>; const body = req.body as Partial<GitSource>;
if ( if (
typeof body.id !== "string" || typeof body.id !== "string" ||
@@ -136,7 +144,7 @@ router.post("/git-sources", async (req, res) => {
return res.json({ success: true, source }); return res.json({ success: true, source });
}); });
router.put("/git-sources/:id", (req, res) => { router.put("/git-sources/:id", requireSourceManagementBuildIntegration, (req, res) => {
const index = store.gitSources.findIndex((source) => source.id === req.params.id); const index = store.gitSources.findIndex((source) => source.id === req.params.id);
if (index === -1) return res.status(404).json({ error: "Source not found" }); if (index === -1) return res.status(404).json({ error: "Source not found" });
store.gitSources[index] = { ...store.gitSources[index], ...req.body }; store.gitSources[index] = { ...store.gitSources[index], ...req.body };
@@ -148,7 +156,7 @@ router.put("/git-sources/:id", (req, res) => {
return res.json({ success: true, source: store.gitSources[index] }); return res.json({ success: true, source: store.gitSources[index] });
}); });
router.delete("/git-sources/:id", (req, res) => { router.delete("/git-sources/:id", requireSourceManagementBuildIntegration, (req, res) => {
store.gitSources = store.gitSources.filter( store.gitSources = store.gitSources.filter(
(source) => source.id !== req.params.id, (source) => source.id !== req.params.id,
); );
@@ -160,7 +168,7 @@ router.delete("/git-sources/:id", (req, res) => {
res.json({ success: true }); res.json({ success: true });
}); });
router.post("/git-sources/:id/test", async (req, res) => { router.post("/git-sources/:id/test", requireSourceManagementBuildIntegration, async (req, res) => {
const source = store.gitSources.find((item) => item.id === req.params.id); const source = store.gitSources.find((item) => item.id === req.params.id);
if (!source) return res.status(404).json({ error: "Source not found" }); if (!source) return res.status(404).json({ error: "Source not found" });
source.status = "syncing"; source.status = "syncing";
+49
View File
@@ -0,0 +1,49 @@
import assert from "node:assert/strict";
import { Request, Response } from "express";
import test from "node:test";
import {
requireSourceManagementBuildIntegration,
SOURCE_MANAGEMENT_BUILD_INTEGRATION,
sourceManagementCapability,
} from "./sourceManagement";
test("source management reports a fail-closed not-wired build boundary", () => {
const capability = sourceManagementCapability({
sourceId: "site-definition",
repository: "https://git.example.test/site.git",
commit: "a".repeat(40),
});
assert.equal(SOURCE_MANAGEMENT_BUILD_INTEGRATION, false);
assert.equal(capability.buildIntegration, "not-wired");
assert.equal(capability.mutationsEnabled, false);
assert.equal(capability.buildSource.commit, "a".repeat(40));
assert.match(capability.message, /Builds use only/);
});
test("source-management mutation middleware rejects direct API calls", () => {
let status = 0;
let payload: unknown;
const response = {
status(code: number) {
status = code;
return this;
},
json(body: unknown) {
payload = body;
return this;
},
} as Response;
requireSourceManagementBuildIntegration(
{} as Request,
response,
() => assert.fail("disabled source management must not call next"),
);
assert.equal(status, 409);
assert.deepEqual(payload, {
code: "E_SOURCE_MANAGEMENT_NOT_WIRED",
success: false,
message:
"Source-management mutations are disabled because repository inventory does not feed BuildInputLoader.",
});
});
+31
View File
@@ -0,0 +1,31 @@
import { RequestHandler } from "express";
import { SiteConfig, SourceManagementCapability } from "../types";
export const SOURCE_MANAGEMENT_BUILD_INTEGRATION = false as const;
export const sourceManagementCapability = (
siteConfig: Pick<SiteConfig, "sourceId" | "repository" | "commit">,
): SourceManagementCapability => ({
protocol: "labyricorn-source-management/v1",
buildIntegration: "not-wired",
mutationsEnabled: SOURCE_MANAGEMENT_BUILD_INTEGRATION,
buildSource: {
sourceId: siteConfig.sourceId ?? "site-definition",
repository: siteConfig.repository ?? "unresolved",
commit: siteConfig.commit ?? null,
},
message:
"Repository inventory is read-only. Builds use only the Git-owned site-definition snapshot; discovery, add, test, and content-scan actions are not wired to build inputs.",
});
export const requireSourceManagementBuildIntegration: RequestHandler = (
_req,
res,
) => {
res.status(409).json({
code: "E_SOURCE_MANAGEMENT_NOT_WIRED",
success: false,
message:
"Source-management mutations are disabled because repository inventory does not feed BuildInputLoader.",
});
};
+19 -7
View File
@@ -15,17 +15,19 @@ import {
Layers, Layers,
Image as ImageIcon, Image as ImageIcon,
} from "lucide-react"; } from "lucide-react";
import { ContentItem, MediaAsset } from "../types"; import { ContentItem, MediaAsset, SourceManagementCapability } from "../types";
interface ContentTabProps { interface ContentTabProps {
contentItems: ContentItem[]; contentItems: ContentItem[];
mediaAssets: MediaAsset[]; mediaAssets: MediaAsset[];
sourceManagement: SourceManagementCapability;
onTriggerDiscovery: () => Promise<void>; onTriggerDiscovery: () => Promise<void>;
} }
export const ContentTab: React.FC<ContentTabProps> = ({ export const ContentTab: React.FC<ContentTabProps> = ({
contentItems, contentItems,
mediaAssets, mediaAssets,
sourceManagement,
onTriggerDiscovery, onTriggerDiscovery,
}) => { }) => {
const [selectedItemId, setSelectedItemId] = useState<string>( const [selectedItemId, setSelectedItemId] = useState<string>(
@@ -50,6 +52,7 @@ export const ContentTab: React.FC<ContentTabProps> = ({
item.path.toLowerCase().includes(searchTerm.toLowerCase()) || item.path.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.tags.some((t) => t.toLowerCase().includes(searchTerm.toLowerCase())), item.tags.some((t) => t.toLowerCase().includes(searchTerm.toLowerCase())),
); );
const discoveryEnabled = sourceManagement.mutationsEnabled;
const handleScan = async () => { const handleScan = async () => {
setIsScanning(true); setIsScanning(true);
@@ -69,18 +72,17 @@ export const ContentTab: React.FC<ContentTabProps> = ({
</span> </span>
</div> </div>
<p className="text-xs text-neutral-400 mt-1"> <p className="text-xs text-neutral-400 mt-1">
Discovers artifacts across repositories, parses front matter, Shows artifacts resolved from the immutable site-definition
validates YouTube directives, Wikipedia links, raw HTML policy, and snapshot used by the deterministic build engine.
media mappings under{" "}
<code className="text-[#C5A059] font-mono">/media</code>.
</p> </p>
</div> </div>
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<button <button
onClick={handleScan} onClick={handleScan}
disabled={isScanning} disabled={!discoveryEnabled || isScanning}
className="px-4 py-2 bg-[#C5A059] hover:bg-[#b38f4a] text-black font-semibold rounded-sm text-xs transition-all flex items-center space-x-1.5 uppercase tracking-wider" title={discoveryEnabled ? undefined : sourceManagement.message}
className="px-4 py-2 bg-[#C5A059] hover:bg-[#b38f4a] text-black font-semibold rounded-sm text-xs transition-all flex items-center space-x-1.5 uppercase tracking-wider disabled:opacity-40 disabled:cursor-not-allowed"
> >
<RefreshCw <RefreshCw
className={`w-3.5 h-3.5 ${isScanning ? "animate-spin" : ""}`} className={`w-3.5 h-3.5 ${isScanning ? "animate-spin" : ""}`}
@@ -90,6 +92,16 @@ export const ContentTab: React.FC<ContentTabProps> = ({
</div> </div>
</div> </div>
{!discoveryEnabled && (
<div className="p-4 rounded-sm border border-amber-500/30 bg-amber-950/30 text-amber-200 text-xs flex items-start space-x-3">
<AlertTriangle className="w-4 h-4 text-amber-400 shrink-0 mt-0.5" />
<p>
Live inventory scans are disabled because their results do not feed
BuildInputLoader. This view reflects the content resolved from the
pinned build source.
</p>
</div>
)}
{/* SUB-TABS: MARKDOWN VS MEDIA */} {/* SUB-TABS: MARKDOWN VS MEDIA */}
<div className="flex border-b border-[#1F1F1F] space-x-6 text-xs font-semibold"> <div className="flex border-b border-[#1F1F1F] space-x-6 text-xs font-semibold">
<button <button
+39 -17
View File
@@ -5,6 +5,7 @@ import {
FileText, FileText,
CheckCircle2, CheckCircle2,
XCircle, XCircle,
AlertTriangle,
RefreshCw, RefreshCw,
Plus, Plus,
Key, Key,
@@ -15,11 +16,12 @@ import {
ChevronRight, ChevronRight,
Code, Code,
} from "lucide-react"; } from "lucide-react";
import { GitSource, CredentialRef } from "../types"; import { GitSource, CredentialRef, SourceManagementCapability } from "../types";
interface GitSourcesTabProps { interface GitSourcesTabProps {
sources: GitSource[]; sources: GitSource[];
credentials: CredentialRef[]; credentials: CredentialRef[];
sourceManagement: SourceManagementCapability;
onAddSource: (src: Partial<GitSource>) => void; onAddSource: (src: Partial<GitSource>) => void;
onTestSource: (id: string) => Promise<any>; onTestSource: (id: string) => Promise<any>;
} }
@@ -27,6 +29,7 @@ interface GitSourcesTabProps {
export const GitSourcesTab: React.FC<GitSourcesTabProps> = ({ export const GitSourcesTab: React.FC<GitSourcesTabProps> = ({
sources, sources,
credentials, credentials,
sourceManagement,
onAddSource, onAddSource,
onTestSource, onTestSource,
}) => { }) => {
@@ -35,13 +38,7 @@ export const GitSourcesTab: React.FC<GitSourcesTabProps> = ({
); );
const [treeFiles, setTreeFiles] = useState< const [treeFiles, setTreeFiles] = useState<
Array<{ path: string; type: string; size: string }> Array<{ path: string; type: string; size: string }>
>([ >([]);
{ path: ".devlogs/pack-validation.md", type: "file", size: "2.8 KB" },
{ path: ".devlogs/schema-refactor.md", type: "file", size: "1.9 KB" },
{ path: "media/screenshots/header.webp", type: "file", size: "142 KB" },
{ path: "src/engine.ts", type: "file", size: "14.2 KB" },
{ path: "package.json", type: "file", size: "1.1 KB" },
]);
const [isTesting, setIsTesting] = useState<string | null>(null); const [isTesting, setIsTesting] = useState<string | null>(null);
const [testResult, setTestResult] = useState<{ const [testResult, setTestResult] = useState<{
id: string; id: string;
@@ -127,7 +124,9 @@ export const GitSourcesTab: React.FC<GitSourcesTabProps> = ({
setNewType("hybrid"); setNewType("hybrid");
}; };
const selectedSource = sources.find((s) => s.id === selectedSourceId); const selectedSource =
sources.find((source) => source.id === selectedSourceId) ?? sources[0];
const mutationsEnabled = sourceManagement.mutationsEnabled;
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -143,16 +142,17 @@ export const GitSourcesTab: React.FC<GitSourcesTabProps> = ({
</span> </span>
</div> </div>
<p className="text-xs text-neutral-400 mt-1"> <p className="text-xs text-neutral-400 mt-1">
Connect standard SSH, HTTPS, or local Git repositories. Repositories Read-only view of the repository snapshot currently resolved by the
without .labyricorn/site.yml are ignored during build. deterministic build loader.
</p> </p>
</div> </div>
<div className="flex space-x-2 self-start md:self-auto"> <div className="flex space-x-2 self-start md:self-auto">
<button <button
onClick={handleDiscover} onClick={handleDiscover}
disabled={isDiscovering} disabled={!mutationsEnabled || isDiscovering}
className="px-4 py-2 bg-[#1A1A1A] hover:bg-[#2A2A2A] text-white font-semibold rounded-sm text-xs transition-all flex items-center space-x-1.5 uppercase tracking-wider border border-[#2A2A2A]" title={mutationsEnabled ? undefined : sourceManagement.message}
className="px-4 py-2 bg-[#1A1A1A] hover:bg-[#2A2A2A] text-white font-semibold rounded-sm text-xs transition-all flex items-center space-x-1.5 uppercase tracking-wider border border-[#2A2A2A] disabled:opacity-40 disabled:cursor-not-allowed"
> >
<RefreshCw <RefreshCw
className={`w-4 h-4 ${isDiscovering ? "animate-spin" : ""}`} className={`w-4 h-4 ${isDiscovering ? "animate-spin" : ""}`}
@@ -161,7 +161,9 @@ export const GitSourcesTab: React.FC<GitSourcesTabProps> = ({
</button> </button>
<button <button
onClick={() => setShowAddModal(true)} onClick={() => setShowAddModal(true)}
className="px-4 py-2 bg-[#C5A059] hover:bg-[#b38f4a] text-black font-semibold rounded-sm text-xs transition-all flex items-center space-x-1.5 uppercase tracking-wider" disabled={!mutationsEnabled}
title={mutationsEnabled ? undefined : sourceManagement.message}
className="px-4 py-2 bg-[#C5A059] hover:bg-[#b38f4a] text-black font-semibold rounded-sm text-xs transition-all flex items-center space-x-1.5 uppercase tracking-wider disabled:opacity-40 disabled:cursor-not-allowed"
> >
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
<span>Add Repository</span> <span>Add Repository</span>
@@ -169,6 +171,25 @@ export const GitSourcesTab: React.FC<GitSourcesTabProps> = ({
</div> </div>
</div> </div>
{!mutationsEnabled && (
<div className="p-4 rounded-sm border border-amber-500/30 bg-amber-950/30 text-amber-200 text-xs flex items-start space-x-3">
<AlertTriangle className="w-4 h-4 text-amber-400 shrink-0 mt-0.5" />
<div>
<span className="font-bold block uppercase tracking-wider">
Source management is not wired to builds
</span>
<p className="mt-1 text-amber-100/80">
Discovery, add, connectivity test, and content-scan actions are
disabled. The next build reads only{" "}
<code className="font-mono">
{sourceManagement.buildSource.repository}@
{sourceManagement.buildSource.commit ?? "unresolved"}
</code>{" "}
through BuildInputLoader.
</p>
</div>
</div>
)}
{/* TEST RESULT NOTIFICATION */} {/* TEST RESULT NOTIFICATION */}
{testResult && ( {testResult && (
<div <div
@@ -205,7 +226,7 @@ export const GitSourcesTab: React.FC<GitSourcesTabProps> = ({
{/* LEFT COLUMN: SOURCES LIST (5 COLS) */} {/* LEFT COLUMN: SOURCES LIST (5 COLS) */}
<div className="lg:col-span-5 space-y-3"> <div className="lg:col-span-5 space-y-3">
<div className="text-[10px] font-bold text-neutral-500 uppercase tracking-widest px-1"> <div className="text-[10px] font-bold text-neutral-500 uppercase tracking-widest px-1">
Configured Repositories ({sources.length}) Resolved Build Repositories ({sources.length})
</div> </div>
{sources.map((source) => { {sources.map((source) => {
@@ -294,8 +315,9 @@ export const GitSourcesTab: React.FC<GitSourcesTabProps> = ({
e.stopPropagation(); e.stopPropagation();
handleTest(source.id); handleTest(source.id);
}} }}
disabled={isTesting === source.id} disabled={!mutationsEnabled || isTesting === source.id}
className="px-2.5 py-1 bg-[#1A1A1A] hover:bg-[#262626] border border-[#2A2A2A] rounded-xs text-neutral-300 text-[11px] font-medium transition-colors flex items-center space-x-1" title={mutationsEnabled ? undefined : sourceManagement.message}
className="px-2.5 py-1 bg-[#1A1A1A] hover:bg-[#262626] border border-[#2A2A2A] rounded-xs text-neutral-300 text-[11px] font-medium transition-colors flex items-center space-x-1 disabled:opacity-40 disabled:cursor-not-allowed"
> >
<RefreshCw <RefreshCw
className={`w-3 h-3 ${isTesting === source.id ? "animate-spin text-[#C5A059]" : ""}`} className={`w-3 h-3 ${isTesting === source.id ? "animate-spin text-[#C5A059]" : ""}`}
+12
View File
@@ -24,6 +24,18 @@ export interface GitSource {
isMountedLocal?: boolean; isMountedLocal?: boolean;
} }
export interface SourceManagementCapability {
protocol: "labyricorn-source-management/v1";
buildIntegration: "not-wired";
mutationsEnabled: boolean;
buildSource: {
sourceId: string;
repository: string;
commit: string | null;
};
message: string;
}
export interface StyleConfigPackage { export interface StyleConfigPackage {
id: string; id: string;
name: string; name: string;