From 6506284912f85413806b094a5d1e95d390a03e6b Mon Sep 17 00:00:00 2001 From: BinarySandia04 Date: Fri, 10 Jul 2026 20:02:09 +0200 Subject: [PATCH] Whatever --- backend/package-lock.json | 48 ++++++ backend/package.json | 1 + backend/src/models/Site.js | 2 + backend/src/routes/sites.js | 126 ++++++++++++++- {publisher => frontend}/.gitignore | 0 {publisher => frontend}/README.md | 0 {publisher => frontend}/app/app.vue | 148 +++++++++++++----- .../app/assets/themes.scss | 0 .../app/components/AddSiteForm.vue | 0 frontend/app/components/FileTree.vue | 65 ++++++++ .../app/components/SettingsEmpty.vue | 0 .../app/components/SettingsPanel.vue | 98 +++++++++++- .../app/components/Sidebar.vue | 4 - .../app/components/SiteCard.vue | 0 frontend/app/components/TreeItem.vue | 97 ++++++++++++ {publisher => frontend}/nuxt.config.ts | 0 {publisher => frontend}/package-lock.json | 0 {publisher => frontend}/package.json | 0 {publisher => frontend}/public/favicon.ico | Bin {publisher => frontend}/public/robots.txt | 0 {publisher => frontend}/tsconfig.json | 0 21 files changed, 541 insertions(+), 48 deletions(-) rename {publisher => frontend}/.gitignore (100%) rename {publisher => frontend}/README.md (100%) rename {publisher => frontend}/app/app.vue (73%) rename {publisher => frontend}/app/assets/themes.scss (100%) rename {publisher => frontend}/app/components/AddSiteForm.vue (100%) create mode 100644 frontend/app/components/FileTree.vue rename {publisher => frontend}/app/components/SettingsEmpty.vue (100%) rename {publisher => frontend}/app/components/SettingsPanel.vue (68%) rename {publisher => frontend}/app/components/Sidebar.vue (93%) rename {publisher => frontend}/app/components/SiteCard.vue (100%) create mode 100644 frontend/app/components/TreeItem.vue rename {publisher => frontend}/nuxt.config.ts (100%) rename {publisher => frontend}/package-lock.json (100%) rename {publisher => frontend}/package.json (100%) rename {publisher => frontend}/public/favicon.ico (100%) rename {publisher => frontend}/public/robots.txt (100%) rename {publisher => frontend}/tsconfig.json (100%) diff --git a/backend/package-lock.json b/backend/package-lock.json index 34144b4..1f2a448 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -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", diff --git a/backend/package.json b/backend/package.json index 90f0d6d..9c0f1e0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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": { diff --git a/backend/src/models/Site.js b/backend/src/models/Site.js index e02c929..36afb32 100644 --- a/backend/src/models/Site.js +++ b/backend/src/models/Site.js @@ -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 } ); diff --git a/backend/src/routes/sites.js b/backend/src/routes/sites.js index 531f663..8a7c44b 100644 --- a/backend/src/routes/sites.js +++ b/backend/src/routes/sites.js @@ -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; diff --git a/publisher/.gitignore b/frontend/.gitignore similarity index 100% rename from publisher/.gitignore rename to frontend/.gitignore diff --git a/publisher/README.md b/frontend/README.md similarity index 100% rename from publisher/README.md rename to frontend/README.md diff --git a/publisher/app/app.vue b/frontend/app/app.vue similarity index 73% rename from publisher/app/app.vue rename to frontend/app/app.vue index abe8d21..0a06dc6 100644 --- a/publisher/app/app.vue +++ b/frontend/app/app.vue @@ -8,24 +8,34 @@ -
- +
+
+ - - -
+
+ + +
+ +
+ +
+
+ @@ -34,6 +44,7 @@ import { ref, onMounted, computed } from 'vue' import Sidebar from './components/Sidebar.vue' import SettingsPanel from './components/SettingsPanel.vue' import SettingsEmpty from './components/SettingsEmpty.vue' +import FileTree from './components/FileTree.vue' const API_BASE = import.meta.url.match(/^https?:\/\/[^/]+/) ? `${location.protocol}//${location.host}` @@ -62,6 +73,8 @@ function defaultSite(name: string) { themeColor: '#2563eb', targetWindow: '_blank', enabled: true, + gitRepoUrl: '', + gitBranch: 'main', } } @@ -77,7 +90,7 @@ function applyClass() { document.documentElement.classList.toggle('dark', darkMode.value) } -const sites = ref>([]) +const sites = ref>([]) const selectedId = ref(null) const pendingAdd = ref(false) const lastSaved = ref('') @@ -86,6 +99,7 @@ let newSites = new Map() let dirtyEdits = new Map() let loadTimer: ReturnType | null = null let lastSavedTimeout: ReturnType | null = null +let autoSaveTimer: ReturnType | null = null const selectedSite = computed(() => { return sites.value.find(s => s.id === selectedId.value) || null @@ -147,7 +161,6 @@ async function refreshSites() { async function saveSite(site: any) { try { - const serverSite = sites.value.find(s => s.id === site.id) const res = await api(`/${encodeURIComponent(site.id)}`, { method: 'PUT', body: JSON.stringify({ @@ -157,6 +170,8 @@ async function saveSite(site: any) { themeColor: site.themeColor, targetWindow: site.targetWindow, enabled: site.enabled, + gitRepoUrl: site.gitRepoUrl || '', + gitBranch: site.gitBranch || 'main', }), }) if (res.ok) { @@ -171,6 +186,8 @@ async function saveSite(site: any) { } async function bulkSave() { + if (autoSaveTimer) clearTimeout(autoSaveTimer) + autoSaveTimer = null const allEntries = [...newSites.entries(), ...dirtyEdits.entries()] for (const [, site] of allEntries) { await saveSite(site) @@ -188,12 +205,16 @@ async function bulkSave() { }, 3000) } -async function handleEditChanged(site: { id: string; name: string; url: string; description: string; themeColor: string; targetWindow: string; enabled: boolean }) { +async function handleEditChanged(site: { id: string; name: string; url: string; description: string; themeColor: string; targetWindow: string; enabled: boolean; gitRepoUrl?: string; gitBranch?: string }) { if (newSites.has(site.id)) { newSites.set(site.id, site) } else { dirtyEdits.set(site.id, site) } + if (autoSaveTimer) clearTimeout(autoSaveTimer) + autoSaveTimer = setTimeout(async () => { + await bulkSave() + }, 800) } function selectSite(site: { id: string; name: string; enabled: boolean }) { @@ -272,24 +293,27 @@ function openPreview() { diff --git a/publisher/app/components/SettingsEmpty.vue b/frontend/app/components/SettingsEmpty.vue similarity index 100% rename from publisher/app/components/SettingsEmpty.vue rename to frontend/app/components/SettingsEmpty.vue diff --git a/publisher/app/components/SettingsPanel.vue b/frontend/app/components/SettingsPanel.vue similarity index 68% rename from publisher/app/components/SettingsPanel.vue rename to frontend/app/components/SettingsPanel.vue index 12a6a1a..88ff2f8 100644 --- a/publisher/app/components/SettingsPanel.vue +++ b/frontend/app/components/SettingsPanel.vue @@ -42,6 +42,25 @@ +
+ + {{ lastGitMsg }} +
+ +
+ +
+ + +
+ +
+ + +
+
{{ lastSaved }} @@ -63,12 +82,19 @@ import { ref, computed, watch } from 'vue' const props = defineProps<{ - site: { name: string; url: string; description: string; themeColor: string; targetWindow: string; enabled: boolean } + site: { name: string; url: string; description: string; themeColor: string; targetWindow: string; enabled: boolean; gitRepoUrl?: string; gitBranch?: string } lastSaved: string siteId?: string }>() -const emit = defineEmits<{ delete: []; preview: []; changed: [{ name: string; url: string; description: string; themeColor: string; targetWindow: string; enabled: boolean, id: string }] }>() +const emit = defineEmits<{ delete: []; preview: []; changed: [{ name: string; url: string; description: string; themeColor: string; targetWindow: string; enabled: boolean, gitRepoUrl?: string; gitBranch?: string; id: string }] }>() + +const syncing = ref(false) +const lastGitMsg = ref('') + +const hasGit = computed(() => { + return !!(props.site.gitRepoUrl || props.siteId) +}) const editName = ref(props.site.name) const editUrl = ref(props.site.url) @@ -76,6 +102,8 @@ const editDescription = ref(props.site.description) const editThemeColor = ref(props.site.themeColor) const editTargetWindow = ref(props.site.targetWindow) const editEnabled = ref(props.site.enabled) +const editGitRepoUrl = ref(props.site.gitRepoUrl || '') +const editGitBranch = ref(props.site.gitBranch || 'main') const isDirty = computed(() => { return ( @@ -84,11 +112,13 @@ const isDirty = computed(() => { editDescription.value !== props.site.description || editThemeColor.value !== props.site.themeColor || editTargetWindow.value !== props.site.targetWindow || - editEnabled.value !== props.site.enabled + editEnabled.value !== props.site.enabled || + editGitRepoUrl.value !== (props.site.gitRepoUrl || '') || + editGitBranch.value !== (props.site.gitBranch || 'main') ) }) -watch([editName, editUrl, editDescription, editThemeColor, editTargetWindow, editEnabled], () => { +watch([editName, editUrl, editDescription, editThemeColor, editTargetWindow, editEnabled, editGitRepoUrl, editGitBranch], () => { emit('changed', { name: editName.value, url: editUrl.value, @@ -96,6 +126,8 @@ watch([editName, editUrl, editDescription, editThemeColor, editTargetWindow, edi themeColor: editThemeColor.value, targetWindow: editTargetWindow.value, enabled: editEnabled.value, + gitRepoUrl: editGitRepoUrl.value, + gitBranch: editGitBranch.value, id: props.siteId!, }) }) @@ -108,9 +140,30 @@ function handleSave() { themeColor: editThemeColor.value, targetWindow: editTargetWindow.value, enabled: editEnabled.value, + gitRepoUrl: editGitRepoUrl.value, + gitBranch: editGitBranch.value, id: props.siteId!, }) } + +async function handleSyncGit() { + if (!props.site.id || syncing.value) return + const API_BASE = window.location.origin + syncing.value = true + lastGitMsg.value = '' + try { + const res = await fetch(`${API_BASE}/api/sites/${encodeURIComponent(props.site.id)}/sync-git`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }) + const data = await res.json() + if (res.ok) { + lastGitMsg.value = data.cloned ? 'Cloned repository' : 'Pull succeeded' + } else { + lastGitMsg.value = data.error || 'Sync failed' + } + } catch (err) { + lastGitMsg.value = 'Sync failed' + } + syncing.value = false +} diff --git a/publisher/app/components/Sidebar.vue b/frontend/app/components/Sidebar.vue similarity index 93% rename from publisher/app/components/Sidebar.vue rename to frontend/app/components/Sidebar.vue index fe508ee..34cbd46 100644 --- a/publisher/app/components/Sidebar.vue +++ b/frontend/app/components/Sidebar.vue @@ -28,10 +28,6 @@ defineEmits<{ 'select-site': [site: { id: string; name: string; enabled: boolean diff --git a/publisher/nuxt.config.ts b/frontend/nuxt.config.ts similarity index 100% rename from publisher/nuxt.config.ts rename to frontend/nuxt.config.ts diff --git a/publisher/package-lock.json b/frontend/package-lock.json similarity index 100% rename from publisher/package-lock.json rename to frontend/package-lock.json diff --git a/publisher/package.json b/frontend/package.json similarity index 100% rename from publisher/package.json rename to frontend/package.json diff --git a/publisher/public/favicon.ico b/frontend/public/favicon.ico similarity index 100% rename from publisher/public/favicon.ico rename to frontend/public/favicon.ico diff --git a/publisher/public/robots.txt b/frontend/public/robots.txt similarity index 100% rename from publisher/public/robots.txt rename to frontend/public/robots.txt diff --git a/publisher/tsconfig.json b/frontend/tsconfig.json similarity index 100% rename from publisher/tsconfig.json rename to frontend/tsconfig.json