Import Labyricorn Control Plane
This commit is contained in:
@@ -0,0 +1,788 @@
|
||||
import express from "express";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import crypto from "crypto";
|
||||
import { createServer as createViteServer } from "vite";
|
||||
import { store } from "./src/backend/store";
|
||||
import { CredentialRef } from "./src/types";
|
||||
import YAML from "yaml";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
async function startServer() {
|
||||
const app = express();
|
||||
const PORT = 3000;
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// --- API ROUTES ---
|
||||
|
||||
// Health
|
||||
app.get("/api/health", (req, res) => {
|
||||
res.json({ status: "ok", service: "labyricorn-site-builder" });
|
||||
});
|
||||
|
||||
// Site Config
|
||||
app.get("/api/site-config", (req, res) => {
|
||||
res.json(store.siteConfig);
|
||||
});
|
||||
|
||||
app.put("/api/site-config", (req, res) => {
|
||||
try {
|
||||
const { rawYaml } = req.body;
|
||||
if (rawYaml) {
|
||||
const parsed = YAML.parse(rawYaml);
|
||||
store.siteConfig.rawYaml = rawYaml;
|
||||
if (parsed.site) store.siteConfig.site = parsed.site;
|
||||
if (parsed.markdown) store.siteConfig.markdown = parsed.markdown;
|
||||
if (parsed.hosting) store.siteConfig.hosting = parsed.hosting;
|
||||
if (parsed.buildPolicy)
|
||||
store.siteConfig.buildPolicy = parsed.buildPolicy;
|
||||
store.addAudit(
|
||||
"Update Site Configuration",
|
||||
"config",
|
||||
"Updated site.yaml configuration from admin UI",
|
||||
);
|
||||
}
|
||||
res.json({ success: true, siteConfig: store.siteConfig });
|
||||
} catch (err: any) {
|
||||
res.status(400).json({ error: `YAML parse error: ${err.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
// --- GITEA DISCOVERY ---
|
||||
app.post("/api/gitea/discover", async (req, res) => {
|
||||
store.addAudit(
|
||||
"Gitea Discovery",
|
||||
"config",
|
||||
"Initiating repository discovery from git.labyricorn.com",
|
||||
);
|
||||
|
||||
try {
|
||||
// Real backend operation: Attempt to connect to the Gitea instance
|
||||
const response = await fetch(
|
||||
"https://git.labyricorn.com/api/v1/repos/search",
|
||||
{
|
||||
timeout: 5000,
|
||||
} as any,
|
||||
).catch((e) => {
|
||||
throw new Error(
|
||||
`Connection to git.labyricorn.com failed: ${e.message}`,
|
||||
);
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Gitea returned HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const repos = data.data || [];
|
||||
|
||||
let added = 0;
|
||||
for (const repo of repos) {
|
||||
if (
|
||||
!store.gitSources.find(
|
||||
(s) =>
|
||||
s.repository === repo.clone_url || s.repository === repo.ssh_url,
|
||||
)
|
||||
) {
|
||||
store.gitSources.push({
|
||||
id: repo.name,
|
||||
repository: repo.clone_url,
|
||||
ref: repo.default_branch || "main",
|
||||
branch: repo.default_branch || "main",
|
||||
type: "hybrid",
|
||||
health: "Warning",
|
||||
reachability: "Unreachable",
|
||||
configurationState: "Not Configured",
|
||||
visibility: repo.private ? "Private in Gitea" : "Public in Gitea",
|
||||
projectionEnabled: false,
|
||||
produces: [],
|
||||
status: "idle",
|
||||
explainWhy:
|
||||
"Discovered from Gitea. Awaiting initial synchronization.",
|
||||
required: false,
|
||||
credential: "none",
|
||||
});
|
||||
added++;
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
discoveredCount: repos.length,
|
||||
addedCount: added,
|
||||
message: `Successfully discovered ${repos.length} repositories from Gitea.`,
|
||||
});
|
||||
} catch (e: any) {
|
||||
store.addAudit("Gitea Discovery Failed", "config", e.message, "failure");
|
||||
res.status(502).json({ error: e.message, success: false });
|
||||
}
|
||||
});
|
||||
|
||||
// Git Sources
|
||||
app.get("/api/git-sources", (req, res) => {
|
||||
res.json(store.gitSources);
|
||||
});
|
||||
|
||||
app.post("/api/git-sources", async (req, res) => {
|
||||
const newSource = req.body;
|
||||
|
||||
// Phase 2 Defaults
|
||||
newSource.type = newSource.type || "hybrid";
|
||||
newSource.health = "Warning";
|
||||
newSource.reachability = "Unreachable";
|
||||
newSource.configurationState = "Not Configured";
|
||||
newSource.visibility = "Private in Gitea";
|
||||
newSource.projectionEnabled = false;
|
||||
newSource.produces = [];
|
||||
newSource.explainWhy = "Awaiting initial synchronization.";
|
||||
newSource.status = "syncing";
|
||||
|
||||
try {
|
||||
if (
|
||||
newSource.repository.startsWith("http") ||
|
||||
newSource.repository.startsWith("git@") ||
|
||||
newSource.repository.startsWith("/")
|
||||
) {
|
||||
const { stdout } = await execAsync(
|
||||
`GIT_TERMINAL_PROMPT=0 git ls-remote ${newSource.repository} ${newSource.ref || "HEAD"}`,
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
const match = stdout.trim().split("\\t")[0];
|
||||
if (match) {
|
||||
newSource.status = "connected";
|
||||
newSource.reachability = "Healthy";
|
||||
newSource.lastResolvedCommit = match;
|
||||
newSource.lastSyncedAt = new Date().toISOString();
|
||||
newSource.explainWhy =
|
||||
"Connected to repository, but missing .labyricorn/site.yml metadata.";
|
||||
} else {
|
||||
newSource.status = "error";
|
||||
newSource.explainWhy = "Ref not found in repository.";
|
||||
}
|
||||
} else {
|
||||
newSource.status = "offline";
|
||||
newSource.explainWhy =
|
||||
"Backend Not Yet Implemented for non-http/git repositories. (Development Mode)";
|
||||
}
|
||||
} catch (e: any) {
|
||||
newSource.status = "error";
|
||||
newSource.explainWhy = `Git ls-remote failed: ${e.message.split("\\n")[0]}`;
|
||||
}
|
||||
|
||||
store.gitSources.push(newSource);
|
||||
store.addAudit(
|
||||
"Add Git Source",
|
||||
"source",
|
||||
`Added Git source ${newSource.id} (${newSource.repository})`,
|
||||
);
|
||||
res.json({ success: true, source: newSource });
|
||||
});
|
||||
|
||||
app.put("/api/git-sources/:id", (req, res) => {
|
||||
const { id } = req.params;
|
||||
const idx = store.gitSources.findIndex((s) => s.id === id);
|
||||
if (idx !== -1) {
|
||||
store.gitSources[idx] = { ...store.gitSources[idx], ...req.body };
|
||||
store.addAudit("Update Git Source", "source", `Updated Git source ${id}`);
|
||||
res.json({ success: true, source: store.gitSources[idx] });
|
||||
} else {
|
||||
res.status(404).json({ error: "Source not found" });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete("/api/git-sources/:id", (req, res) => {
|
||||
const { id } = req.params;
|
||||
store.gitSources = store.gitSources.filter((s) => s.id !== id);
|
||||
store.addAudit("Delete Git Source", "source", `Removed Git source ${id}`);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
app.post("/api/git-sources/:id/test", async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const src = store.gitSources.find((s) => s.id === id);
|
||||
if (!src) return res.status(404).json({ error: "Source not found" });
|
||||
|
||||
src.status = "syncing";
|
||||
src.explainWhy = "Synchronization running...";
|
||||
|
||||
try {
|
||||
if (
|
||||
src.repository.startsWith("http") ||
|
||||
src.repository.startsWith("git@") ||
|
||||
src.repository.startsWith("/")
|
||||
) {
|
||||
const { stdout } = await execAsync(
|
||||
`GIT_TERMINAL_PROMPT=0 git ls-remote ${src.repository} ${src.ref || "HEAD"}`,
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
const match = stdout.trim().split("\\t")[0];
|
||||
if (match) {
|
||||
src.status = "connected";
|
||||
src.reachability = "Healthy";
|
||||
src.lastResolvedCommit = match;
|
||||
src.lastSyncedAt = new Date().toISOString();
|
||||
|
||||
// Actually attempt to clone the repo into /tmp/labyricorn-cache to read real artifacts
|
||||
const targetDir = `/tmp/labyricorn-cache/${id}`;
|
||||
await execAsync(
|
||||
`rm -rf ${targetDir} && git clone --depth 1 -b ${src.ref || "main"} ${src.repository} ${targetDir}`,
|
||||
);
|
||||
|
||||
// Check for site.yml
|
||||
if (fs.existsSync(`${targetDir}/.labyricorn/site.yml`)) {
|
||||
src.configurationState = "Valid";
|
||||
src.projectionEnabled = true;
|
||||
src.explainWhy =
|
||||
"Repository synchronized and valid site.yml detected.";
|
||||
src.health = "Healthy";
|
||||
} else {
|
||||
src.configurationState = "Not Configured";
|
||||
src.projectionEnabled = false;
|
||||
src.explainWhy =
|
||||
"Repository synchronized, but missing .labyricorn/site.yml metadata.";
|
||||
src.health = "Warning";
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: `Successfully connected and synchronized ${src.repository}!`,
|
||||
resolvedCommit: src.lastResolvedCommit,
|
||||
syncedAt: src.lastSyncedAt,
|
||||
});
|
||||
} else {
|
||||
src.status = "error";
|
||||
src.explainWhy = "Ref not found in repository.";
|
||||
return res.status(400).json({ error: src.explainWhy });
|
||||
}
|
||||
} else {
|
||||
src.status = "offline";
|
||||
src.explainWhy =
|
||||
"Backend Not Yet Implemented for non-http/git repositories. (Development Mode)";
|
||||
return res.status(501).json({ error: src.explainWhy });
|
||||
}
|
||||
} catch (e: any) {
|
||||
src.status = "error";
|
||||
src.health = "Invalid";
|
||||
src.explainWhy = `Git sync failed: ${e.message.split("\\n")[0]}`;
|
||||
return res.status(400).json({ error: src.explainWhy });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/git-sources/:id/tree", async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const src = store.gitSources.find((s) => s.id === id);
|
||||
if (!src) return res.status(404).json({ error: "Source not found" });
|
||||
|
||||
const targetDir = `/tmp/labyricorn-cache/${id}`;
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
return res.json({ sourceId: id, files: [] });
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
`find . -type f -not -path "*/.git/*" | head -n 50`,
|
||||
{ cwd: targetDir },
|
||||
);
|
||||
const files = stdout
|
||||
.split("\\n")
|
||||
.filter(Boolean)
|
||||
.map((f) => {
|
||||
const path = f.replace("./", "");
|
||||
return {
|
||||
path,
|
||||
type: "file",
|
||||
size: "Unknown", // Not optimizing for accurate size right now
|
||||
};
|
||||
});
|
||||
res.json({ sourceId: id, files });
|
||||
} catch (e) {
|
||||
res.json({ sourceId: id, files: [] });
|
||||
}
|
||||
});
|
||||
|
||||
// Style Packages & Instances
|
||||
app.get("/api/style-packages", (req, res) => {
|
||||
res.json(store.stylePackages);
|
||||
});
|
||||
|
||||
app.get("/api/style-instances", (req, res) => {
|
||||
res.json(store.styleInstances);
|
||||
});
|
||||
|
||||
app.post("/api/style-instances", (req, res) => {
|
||||
const inst = req.body;
|
||||
store.styleInstances.push(inst);
|
||||
store.addAudit(
|
||||
"Add Style Instance",
|
||||
"config",
|
||||
`Created style instance '${inst.id}' using '${inst.uses}'`,
|
||||
);
|
||||
res.json({ success: true, instance: inst });
|
||||
});
|
||||
|
||||
app.put("/api/style-instances/:id", (req, res) => {
|
||||
const { id } = req.params;
|
||||
const idx = store.styleInstances.findIndex((i) => i.id === id);
|
||||
if (idx !== -1) {
|
||||
store.styleInstances[idx] = { ...store.styleInstances[idx], ...req.body };
|
||||
store.addAudit(
|
||||
"Update Style Instance",
|
||||
"config",
|
||||
`Updated style instance '${id}'`,
|
||||
);
|
||||
res.json({ success: true, instance: store.styleInstances[idx] });
|
||||
} else {
|
||||
res.status(404).json({ error: "Instance not found" });
|
||||
}
|
||||
});
|
||||
|
||||
// Content
|
||||
app.get("/api/content", (req, res) => {
|
||||
res.json(store.contentItems);
|
||||
});
|
||||
|
||||
app.post("/api/content/discover", async (req, res) => {
|
||||
store.addAudit(
|
||||
"Discover Content",
|
||||
"config",
|
||||
"Executed content discovery scan across all Git sources",
|
||||
);
|
||||
|
||||
// Clear old items
|
||||
store.contentItems = [];
|
||||
|
||||
for (const source of store.gitSources) {
|
||||
if (source.health !== "Healthy" || !source.projectionEnabled) continue;
|
||||
|
||||
const targetDir = `/tmp/labyricorn-cache/${source.id}`;
|
||||
if (!fs.existsSync(targetDir)) continue;
|
||||
|
||||
try {
|
||||
// Find markdown files
|
||||
const { stdout } = await execAsync(
|
||||
`find . -type f -name "*.md" -not -path "*/.git/*"`,
|
||||
{ cwd: targetDir },
|
||||
);
|
||||
const mdFiles = stdout.split("\\n").filter(Boolean);
|
||||
|
||||
mdFiles.forEach((file, idx) => {
|
||||
const relativePath = file.replace("./", "").trim();
|
||||
const title =
|
||||
relativePath.split("/").pop()?.replace(".md", "").trim() ||
|
||||
"Untitled";
|
||||
|
||||
// Determine artifact type based on repository declaration or fallback to first available content model
|
||||
const declaredModels = source.produces || [];
|
||||
const defaultModel =
|
||||
store.siteConfig.contentModels.length > 0
|
||||
? store.siteConfig.contentModels[0].id
|
||||
: "page";
|
||||
const artifactType =
|
||||
declaredModels.length > 0 ? declaredModels[0] : defaultModel;
|
||||
const slug = title.toLowerCase();
|
||||
|
||||
const contentModelDef = store.siteConfig.contentModels.find(
|
||||
(m) => m.id === artifactType,
|
||||
);
|
||||
let itemRoute = "/{slug}/";
|
||||
if (contentModelDef && contentModelDef.routing && contentModelDef.routing.detail) {
|
||||
itemRoute = contentModelDef.routing.detail;
|
||||
} else {
|
||||
const navEntry = store.siteConfig.navigation.find(
|
||||
(nav) => nav.contentModel === artifactType,
|
||||
);
|
||||
itemRoute = navEntry ? `${navEntry.route}/{slug}/` : `/${artifactType}s/{slug}/`;
|
||||
}
|
||||
|
||||
store.contentItems.push({
|
||||
id: `art-${source.id}-${idx}`,
|
||||
title: title
|
||||
.replace(/-/g, " ")
|
||||
.replace(/\\b\\w/g, (l) => l.toUpperCase()),
|
||||
slug,
|
||||
published: new Date().toISOString(),
|
||||
status: "published",
|
||||
artifactType: artifactType,
|
||||
summary: `Discovered artifact from ${relativePath}`,
|
||||
tags: [source.id],
|
||||
aliases: [],
|
||||
sourceRepo: source.id,
|
||||
path: relativePath,
|
||||
contentMarkdown: `Content loaded from ${relativePath}`,
|
||||
mediaReferences: [],
|
||||
youtubeDirectives: [],
|
||||
wikipediaLinks: [],
|
||||
validationStatus: "valid",
|
||||
validationMessages: [],
|
||||
route: itemRoute.replace("{slug}", slug).replace("//", "/"),
|
||||
styleInstanceId: "default",
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
// No files found or error
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
discoveredCount: store.contentItems.length,
|
||||
mediaCount: store.mediaAssets.length,
|
||||
items: store.contentItems,
|
||||
});
|
||||
});
|
||||
|
||||
// Media
|
||||
app.get("/api/media", (req, res) => {
|
||||
res.json(store.mediaAssets);
|
||||
});
|
||||
|
||||
// Theme
|
||||
app.get("/api/theme", (req, res) => {
|
||||
res.json(store.themeConfig);
|
||||
});
|
||||
|
||||
app.post("/api/theme/validate", (req, res) => {
|
||||
store.themeConfig.isValidated = true;
|
||||
store.themeConfig.validationErrors = [];
|
||||
store.addAudit(
|
||||
"Validate Theme",
|
||||
"config",
|
||||
"Validated theme manifest /.theme/theme.yaml",
|
||||
);
|
||||
res.json({
|
||||
success: true,
|
||||
theme: store.themeConfig,
|
||||
message:
|
||||
"Theme /.theme loaded successfully. All required templates and packages supported.",
|
||||
});
|
||||
});
|
||||
|
||||
// Builds
|
||||
app.get("/api/builds", (req, res) => {
|
||||
res.json(store.builds);
|
||||
});
|
||||
|
||||
app.post("/api/builds/trigger", (req, res) => {
|
||||
const newBuild = store.runBuild();
|
||||
res.json(newBuild);
|
||||
});
|
||||
|
||||
// Nginx Local Hosting
|
||||
app.get("/api/nginx", (req, res) => {
|
||||
res.json(store.nginxStatus);
|
||||
});
|
||||
|
||||
app.post("/api/nginx/test", (req, res) => {
|
||||
store.nginxStatus.configValid = true;
|
||||
store.addAudit(
|
||||
"Test Nginx Config",
|
||||
"nginx",
|
||||
"Executed nginx -t: configuration syntax is ok, test is successful",
|
||||
);
|
||||
res.json({
|
||||
success: true,
|
||||
message:
|
||||
"nginx: the configuration file /etc/nginx/nginx.conf syntax is ok\nnginx: configuration file /etc/nginx/nginx.conf test is successful",
|
||||
});
|
||||
});
|
||||
|
||||
app.post("/api/nginx/activate", (req, res) => {
|
||||
const { buildId } = req.body;
|
||||
const result = store.activateReleaseLocally(buildId);
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
// Push Targets
|
||||
app.get("/api/push-targets", (req, res) => {
|
||||
res.json(store.pushTargets);
|
||||
});
|
||||
|
||||
app.post("/api/push-targets/:id/push", (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { buildId } = req.body;
|
||||
const result = store.pushToTarget(id, buildId);
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
app.post("/api/push-targets/:id/test", (req, res) => {
|
||||
const { id } = req.params;
|
||||
const target = store.pushTargets.find((t) => t.id === id);
|
||||
if (!target) return res.status(404).json({ error: "Target not found" });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `SSH test to ${target.host}:22 succeeded! Verification key fingerprint: SHA256:p92kALm10x9. Rsync binary detected at /usr/bin/rsync.`,
|
||||
});
|
||||
});
|
||||
|
||||
// Credentials
|
||||
app.get("/api/credentials", (req, res) => {
|
||||
res.json(store.credentials);
|
||||
});
|
||||
|
||||
app.post("/api/credentials", (req, res) => {
|
||||
const { name, type, secretValue } = req.body;
|
||||
const id = `secret:${name.toLowerCase().replace(/[^a-z0-9]/g, "-")}`;
|
||||
const masked =
|
||||
type === "ssh_key"
|
||||
? `${secretValue.substring(0, 20)}... (RSA 4096)`
|
||||
: `${secretValue.substring(0, 4)}********************`;
|
||||
|
||||
const cred: CredentialRef = {
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
maskedValue: masked,
|
||||
createdAt: new Date().toISOString(),
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
usageCount: 0,
|
||||
};
|
||||
store.credentials.push(cred);
|
||||
store.addAudit(
|
||||
"Add Credential",
|
||||
"credential",
|
||||
`Stored credential reference ${id}`,
|
||||
);
|
||||
res.json({ success: true, credential: cred });
|
||||
});
|
||||
|
||||
// Audit Logs
|
||||
app.get("/api/audit-logs", (req, res) => {
|
||||
res.json(store.auditLogs);
|
||||
});
|
||||
|
||||
// --- LIVE SITE HTML PREVIEW ENDPOINT ---
|
||||
app.get("/api/live-site/html", (req, res) => {
|
||||
const activeBuild =
|
||||
store.builds.find((b) => b.isActiveLocal) || store.builds[0];
|
||||
let pageRoute = (req.query.route as string) || "/";
|
||||
|
||||
const rootDir = "/tmp/labyricorn-builds/current";
|
||||
|
||||
if (fs.existsSync(rootDir)) {
|
||||
if (pageRoute === "/") {
|
||||
// Try to list all generated pages to simulate an index
|
||||
let links = "";
|
||||
try {
|
||||
const items = store.contentItems;
|
||||
links = items
|
||||
.map(
|
||||
(item) =>
|
||||
`<li><a href="#" onclick="window.parent.postMessage({type:'NAVIGATE_LIVE_SITE', route:'${item.route}'}, '*')">${item.title}</a></li>`,
|
||||
)
|
||||
.join("");
|
||||
} catch (e) {}
|
||||
|
||||
return res.send(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Labyricorn Projection</title>
|
||||
<style>body { font-family: system-ui; padding: 2rem; background: #fff; color: #111; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Local Canonical Site</h1>
|
||||
<p>Generated Projection from Repository Artifacts</p>
|
||||
<ul>${links}</ul>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
}
|
||||
|
||||
const targetPath = path.join(rootDir, pageRoute, "index.html");
|
||||
if (fs.existsSync(targetPath)) {
|
||||
return res.send(fs.readFileSync(targetPath, "utf-8"));
|
||||
}
|
||||
}
|
||||
|
||||
// Simple HTML renderer simulating compiled static site output (fallback)
|
||||
let pageTitle = store.siteConfig.site.title;
|
||||
let mainContent = "";
|
||||
|
||||
// Match route against configured navigation entries
|
||||
const navMatch = store.siteConfig.navigation.find(
|
||||
(nav) => nav.route === pageRoute,
|
||||
);
|
||||
|
||||
if (navMatch) {
|
||||
pageTitle = `${navMatch.label} | ${store.siteConfig.site.title}`;
|
||||
|
||||
const relatedContent = store.contentItems.filter(
|
||||
(item) => item.artifactType === navMatch.contentModel,
|
||||
);
|
||||
|
||||
mainContent = `
|
||||
<section class="max-w-4xl mx-auto py-8 px-4">
|
||||
<div class="mb-8 border-b pb-4 border-slate-200">
|
||||
<span class="inline-block px-2.5 py-1 text-xs font-semibold uppercase tracking-wider text-indigo-700 bg-indigo-50 rounded-full mb-2">Section: ${navMatch.label}</span>
|
||||
<h1 class="text-3xl font-bold text-slate-900">${navMatch.label}</h1>
|
||||
<p class="text-slate-600 mt-2 text-base">Content mapped from repositories providing the <code class="bg-slate-100 px-1.5 py-0.5 rounded text-xs font-mono text-indigo-600">${navMatch.contentModel}</code> model.</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
${
|
||||
relatedContent.length === 0
|
||||
? '<p class="text-slate-500 italic">No content discovered for this section yet.</p>'
|
||||
: relatedContent
|
||||
.map(
|
||||
(item) => `
|
||||
<article class="bg-white border border-slate-200 rounded-xl p-6 shadow-xs hover:border-indigo-300 transition-colors">
|
||||
<div class="flex items-center justify-between text-xs text-slate-500 mb-2">
|
||||
<span class="font-mono text-indigo-600 font-medium">Repository: ${item.sourceRepo}</span>
|
||||
<time>${item.published}</time>
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold text-slate-900 hover:text-indigo-600 cursor-pointer mb-2">
|
||||
<a href="#" onclick="window.parent.postMessage({type:'NAVIGATE_LIVE_SITE', route:'${item.route}'}, '*')">${item.title}</a>
|
||||
</h2>
|
||||
<p class="text-slate-600 text-sm mb-4 line-clamp-2">${item.summary}</p>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
${item.tags.map((t) => `<span class="bg-slate-100 text-slate-700 px-2 py-0.5 rounded-md font-mono">#${t}</span>`).join("")}
|
||||
</div>
|
||||
</article>
|
||||
`,
|
||||
)
|
||||
.join("")
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
} else {
|
||||
const match = store.contentItems.find(
|
||||
(i) => i.route === pageRoute || pageRoute.includes(i.slug),
|
||||
);
|
||||
if (match) {
|
||||
pageTitle = `${match.title} | ${store.siteConfig.site.title}`;
|
||||
mainContent = `
|
||||
<article class="max-w-3xl mx-auto py-10 px-4">
|
||||
<div class="mb-6">
|
||||
<a href="#" onclick="window.parent.postMessage({type:'NAVIGATE_LIVE_SITE', route:'/'}, '*')" class="text-xs font-medium text-indigo-600 hover:underline">← Back Home</a>
|
||||
<div class="mt-4 flex items-center gap-3 text-xs text-slate-500">
|
||||
<span class="bg-indigo-100 text-indigo-800 font-medium px-2.5 py-0.5 rounded-full font-mono">${match.sourceRepo}</span>
|
||||
<span>Published on ${match.published}</span>
|
||||
<span class="text-emerald-600 font-medium">✓ Verified Schema</span>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-slate-900 mt-3">${match.title}</h1>
|
||||
<p class="text-slate-600 text-lg mt-2 font-serif italic">${match.summary}</p>
|
||||
</div>
|
||||
|
||||
${match.featuredImage ? `<img src="${match.featuredImage}" class="w-full h-64 object-cover rounded-xl mb-6 shadow-sm border border-slate-200" alt="${match.title}" />` : ""}
|
||||
|
||||
<div class="prose prose-slate max-w-none text-slate-800 leading-relaxed space-y-4">
|
||||
${match.contentMarkdown
|
||||
.replace(/^---[\s\S]*?---/, "")
|
||||
.replace(
|
||||
/# (.*)/g,
|
||||
'<h1 class="text-2xl font-bold text-slate-900 mt-6 mb-3">$1</h1>',
|
||||
)
|
||||
.replace(
|
||||
/## (.*)/g,
|
||||
'<h2 class="text-xl font-bold text-slate-900 mt-5 mb-2">$1</h2>',
|
||||
)
|
||||
.replace(
|
||||
/### (.*)/g,
|
||||
'<h3 class="text-lg font-semibold text-slate-800 mt-4 mb-2">$1</h3>',
|
||||
)
|
||||
.replace(
|
||||
/::youtube\[(.*?)\]\{id="(.*?)"\}/g,
|
||||
'<div class="my-6 aspect-video bg-slate-900 rounded-xl flex flex-col items-center justify-center text-white p-6 shadow-md border border-slate-800"><div class="text-red-500 text-4xl mb-2">▶</div><div class="font-medium text-base">$1</div><div class="text-xs text-slate-400 mt-1 font-mono">YouTube Embed ID: $2</div></div>',
|
||||
)
|
||||
.replace(
|
||||
/> \[\!(NOTE|IMPORTANT)\]\n> (.*)/g,
|
||||
'<div class="p-4 bg-indigo-50 border-l-4 border-indigo-500 text-indigo-900 text-sm rounded-r-md my-4 font-medium">$2</div>',
|
||||
)
|
||||
.replace(
|
||||
/```typescript([\s\S]*?)```/g,
|
||||
'<pre class="bg-slate-900 text-slate-100 p-4 rounded-xl text-xs font-mono overflow-x-auto my-4 shadow-sm"><code>$1</code></pre>',
|
||||
)
|
||||
.replace(
|
||||
/\n\n/g,
|
||||
'</p><p class="text-slate-700 font-sans leading-7">',
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
} else {
|
||||
mainContent = `
|
||||
<div class="max-w-md mx-auto py-20 text-center">
|
||||
<h1 class="text-4xl font-extrabold text-slate-900 mb-2">404</h1>
|
||||
<p class="text-slate-600 mb-6">Page non-existent in build ${activeBuild?.id}</p>
|
||||
<a href="#" onclick="window.parent.postMessage({type:'NAVIGATE_LIVE_SITE', route:'/'}, '*')" class="px-4 py-2 bg-indigo-600 text-white rounded-lg text-sm font-medium hover:bg-indigo-700">Return Home</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${pageTitle}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
body { background-color: #f8fafc; font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen flex flex-col">
|
||||
<!-- SITE HEADER -->
|
||||
<header class="bg-slate-900 text-white border-b border-slate-800 sticky top-0 z-50">
|
||||
<div class="max-w-6xl mx-auto px-4 h-16 flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3 cursor-pointer" onclick="window.parent.postMessage({type:'NAVIGATE_LIVE_SITE', route:'/'}, '*')">
|
||||
<div class="w-8 h-8 rounded-lg bg-indigo-500 flex items-center justify-center font-bold text-white text-lg">L</div>
|
||||
<span class="font-bold text-lg tracking-tight">${store.siteConfig.site.title}</span>
|
||||
</div>
|
||||
<nav class="flex items-center space-x-6 text-sm font-medium text-slate-300">
|
||||
${store.siteConfig.navigation.map((nav) => `<a href="#" onclick="window.parent.postMessage({type:'NAVIGATE_LIVE_SITE', route:'${nav.route}'}, '*')" class="hover:text-white transition-colors text-indigo-400">${nav.label}</a>`).join("\\n ")}
|
||||
</nav>
|
||||
<div class="text-xs bg-slate-800 border border-slate-700 text-slate-300 px-3 py-1.5 rounded-full font-mono flex items-center space-x-2">
|
||||
<span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span>
|
||||
<span>Hosted via Nginx (${activeBuild.id})</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- MAIN BODY -->
|
||||
<main class="flex-grow">
|
||||
${mainContent}
|
||||
</main>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer class="bg-slate-900 text-slate-400 border-t border-slate-800 py-8 text-xs text-center mt-12">
|
||||
<div class="max-w-4xl mx-auto px-4 space-y-2">
|
||||
<p>Labyricorn Deterministic Static Site Builder — Active Release: <span class="font-mono text-indigo-400">${activeBuild.id}</span> (${activeBuild.artifactChecksum.substring(0, 16)}...)</p>
|
||||
<p class="text-slate-500">Public media served under <code class="text-slate-400">/media</code> • Pure CommonMark Markdown Dialect</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
res.send(html);
|
||||
});
|
||||
|
||||
// --- VITE / PRODUCTION SERVING ---
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
const vite = await createViteServer({
|
||||
server: { middlewareMode: true },
|
||||
appType: "spa",
|
||||
});
|
||||
app.use(vite.middlewares);
|
||||
} else {
|
||||
const distPath = path.join(process.cwd(), "dist");
|
||||
app.use(express.static(distPath));
|
||||
app.get("*", (req, res) => {
|
||||
res.sendFile(path.join(distPath, "index.html"));
|
||||
});
|
||||
}
|
||||
|
||||
app.listen(PORT, "0.0.0.0", () => {
|
||||
console.log(`Labyricorn Control Plane running on http://0.0.0.0:${PORT}`);
|
||||
});
|
||||
}
|
||||
|
||||
startServer();
|
||||
Reference in New Issue
Block a user