This commit is contained in:
2026-07-10 20:02:09 +02:00
parent ea2a0b3e36
commit 6506284912
21 changed files with 541 additions and 48 deletions

View File

@@ -13,12 +13,28 @@
"dotenv": "^17.4.2",
"express": "^5.2.1",
"mongoose": "^9.7.4",
"simple-git": "^3.36.0",
"socket.io": "^4.8.3"
},
"devDependencies": {
"nodemon": "^3.1.14"
}
},
"node_modules/@kwsites/file-exists": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
"integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==",
"license": "MIT",
"dependencies": {
"debug": "^4.1.1"
}
},
"node_modules/@kwsites/promise-deferred": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz",
"integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==",
"license": "MIT"
},
"node_modules/@mongodb-js/saslprep": {
"version": "1.4.12",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.12.tgz",
@@ -28,6 +44,21 @@
"sparse-bitfield": "^3.0.3"
}
},
"node_modules/@simple-git/args-pathspec": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz",
"integrity": "sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==",
"license": "MIT"
},
"node_modules/@simple-git/argv-parser": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz",
"integrity": "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==",
"license": "MIT",
"dependencies": {
"@simple-git/args-pathspec": "^1.0.3"
}
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
@@ -1406,6 +1437,23 @@
"integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
"license": "MIT"
},
"node_modules/simple-git": {
"version": "3.36.0",
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz",
"integrity": "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==",
"license": "MIT",
"dependencies": {
"@kwsites/file-exists": "^1.1.1",
"@kwsites/promise-deferred": "^1.1.1",
"@simple-git/args-pathspec": "^1.0.3",
"@simple-git/argv-parser": "^1.1.0",
"debug": "^4.4.0"
},
"funding": {
"type": "github",
"url": "https://github.com/steveukx/git-js?sponsor=1"
}
},
"node_modules/simple-update-notifier": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",

View File

@@ -16,6 +16,7 @@
"dotenv": "^17.4.2",
"express": "^5.2.1",
"mongoose": "^9.7.4",
"simple-git": "^3.36.0",
"socket.io": "^4.8.3"
},
"devDependencies": {

View File

@@ -9,6 +9,8 @@ const siteSchema = new mongoose.Schema(
themeColor: { type: String, default: "#2563eb" },
targetWindow: { type: String, default: "_blank", enum: ["_blank", "_self"] },
enabled: { type: Boolean, default: true },
gitRepoUrl: { type: String, default: "", trim: true },
gitBranch: { type: String, default: "main", trim: true },
},
{ timestamps: true }
);

View File

@@ -1,8 +1,17 @@
const express = require("express");
const Site = require("../models/Site");
const fs = require("fs");
const path = require("path");
const simpleGit = require("simple-git");
const router = express.Router();
const SITES_GIT_DIR = path.join(__dirname, "../../sites-git-data");
if (!fs.existsSync(SITES_GIT_DIR)) {
fs.mkdirSync(SITES_GIT_DIR, { recursive: true });
}
// GET /api/sites - List all sites
router.get("/", async (req, res) => {
try {
@@ -16,8 +25,8 @@ router.get("/", async (req, res) => {
// POST /api/sites - Create a new site
router.post("/", async (req, res) => {
try {
const { id, name, url, description, themeColor, targetWindow, enabled } = req.body;
const site = new Site({ id, name, url, description, themeColor, targetWindow, enabled });
const { id, name, url, description, themeColor, targetWindow, enabled, gitRepoUrl, gitBranch } = req.body;
const site = new Site({ id, name, url, description, themeColor, targetWindow, enabled, gitRepoUrl: gitRepoUrl || "", gitBranch: gitBranch || "main" });
await site.save();
res.status(201).json(site);
} catch (err) {
@@ -28,9 +37,10 @@ router.post("/", async (req, res) => {
// PUT /api/sites/:id - Update a site
router.put("/:id", async (req, res) => {
try {
const { name, url, description, themeColor, targetWindow, enabled } = req.body;
const updates = { name, url, description, themeColor, targetWindow, enabled };
const site = await Site.findOneAndUpdate({ id: req.params.id }, updates, { new: true, runValidators: true });
const { name, url, description, themeColor, targetWindow, enabled, gitRepoUrl, gitBranch } = req.body;
console.log("[PUT /sites] id:", req.params.id, "body:", JSON.stringify({name,url,description,themeColor,targetWindow,enabled,gitRepoUrl,gitBranch}));
const updates = { name, url, description, themeColor, targetWindow, enabled, gitRepoUrl: gitRepoUrl || "", gitBranch: gitBranch || "main" };
const site = await Site.findOneAndUpdate({ id: req.params.id }, { $set: updates }, { new: true, runValidators: true });
if (!site) return res.status(404).json({ error: "Site not found" });
res.json(site);
} catch (err) {
@@ -43,10 +53,116 @@ router.delete("/:id", async (req, res) => {
try {
const site = await Site.findOneAndDelete({ id: req.params.id });
if (!site) return res.status(404).json({ error: "Site not found" });
const siteGitDir = path.join(SITES_GIT_DIR, site.id);
if (fs.existsSync(siteGitDir)) {
fs.rmSync(siteGitDir, { recursive: true, force: true });
}
res.json({ message: "Site deleted", id: site.id });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// GET /api/sites/:id/git-log - Get git commit log for a site's repository
router.get("/:id/git-log", async (req, res) => {
try {
const site = await Site.findOne({ id: req.params.id });
if (!site) return res.status(404).json({ error: "Site not found" });
if (!site.gitRepoUrl) return res.status(400).json({ error: "No git repo URL configured" });
const siteGitDir = path.join(SITES_GIT_DIR, site.id);
try {
const git = simpleGit(siteGitDir);
const logs = await git.log();
res.json({ commits: logs.all || [] });
} catch (gitErr) {
res.status(500).json({ error: "Failed to read git log. The repository may need to be cloned first. Use the sync endpoint." });
}
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// POST /api/sites/:id/sync-git - Clone or pull the git repository for a site
router.post("/:id/sync-git", async (req, res) => {
try {
const site = await Site.findOne({ id: req.params.id });
if (!site) return res.status(404).json({ error: "Site not found" });
if (!site.gitRepoUrl) return res.status(400).json({ error: "No git repo URL configured" });
const siteGitDir = path.join(SITES_GIT_DIR, site.id);
const git = simpleGit(siteGitDir);
try {
await git.pull();
res.json({ message: "Repository pulled successfully", commits: (await git.log()).all || [] });
} catch (pullErr) {
if (pullErr.message.includes("repo does not exist") || pullErr.message.includes("No local restart")) {
const branch = site.gitBranch || "main";
const gitInstance = simpleGit();
await gitInstance.clone(site.gitRepoUrl, siteGitDir);
try {
await gitInstance.checkout(branch);
} catch (e) {
// If branch doesn't exist, stay on default branch
}
const logs = await gitInstance.log();
res.json({ message: "Repository cloned successfully", cloned: true, commits: logs.all || [] });
} else {
res.status(500).json({ error: pullErr.message });
}
}
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// GET /api/sites/:id/files - Get file tree for a site's synced repository
router.get("/:id/files", async (req, res) => {
try {
const site = await Site.findOne({ id: req.params.id });
if (!site) return res.status(404).json({ error: "Site not found" });
const siteGitDir = path.join(SITES_GIT_DIR, site.id);
if (!fs.existsSync(siteGitDir)) {
return res.json({ tree: [], message: "Repository not synced yet. Use Sync Git first." });
}
function buildTree(dir, relativePath = "") {
const entries = fs.readdirSync(dir, { withFileTypes: true });
const result = [];
// Folders first, then files
const dirs = entries.filter(e => e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name));
const files = entries.filter(e => !e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name));
for (const d of dirs) {
result.push({
name: d.name,
path: relativePath ? `${relativePath}/${d.name}` : d.name,
type: "folder",
children: buildTree(path.join(dir, d.name), relativePath ? `${relativePath}/${d.name}` : d.name),
});
}
for (const f of files) {
result.push({
name: f.name,
path: relativePath ? `${relativePath}/${f.name}` : f.name,
type: "file",
});
}
return result;
}
const tree = buildTree(siteGitDir);
res.json({ tree });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
module.exports = router;