Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8acc9e439 |
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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,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",
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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.",
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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]" : ""}`}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user