mirror of
https://github.com/Nystik-gh/ignis.git
synced 2026-06-17 04:35:53 +00:00
add status bar indicator for headless sync
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
const { Plugin } = require("obsidian");
|
const { Plugin } = require("obsidian");
|
||||||
const { HeadlessSyncSettingTab } = require("./settings-tab");
|
const { HeadlessSyncSettingTab } = require("./settings-tab");
|
||||||
const { WsListener } = require("./ws-listener");
|
const { WsListener } = require("./ws-listener");
|
||||||
|
const { initSyncStatusBar } = require("./sync-status-bar");
|
||||||
const api = require("./api");
|
const api = require("./api");
|
||||||
|
|
||||||
class IgnisHeadlessSyncPlugin extends Plugin {
|
class IgnisHeadlessSyncPlugin extends Plugin {
|
||||||
@@ -8,11 +9,7 @@ class IgnisHeadlessSyncPlugin extends Plugin {
|
|||||||
this.wsListener = new WsListener();
|
this.wsListener = new WsListener();
|
||||||
this.wsListener.start();
|
this.wsListener.start();
|
||||||
|
|
||||||
this.wsListener.on("sync-status", (payload) => {
|
this._syncStatusBarCleanup = initSyncStatusBar(this, this.wsListener);
|
||||||
if (payload.vaultId === this.app.vault.getName()) {
|
|
||||||
console.log("[ignis-headless-sync] Status update:", payload.status);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.addSettingTab(new HeadlessSyncSettingTab(this.app, this));
|
this.addSettingTab(new HeadlessSyncSettingTab(this.app, this));
|
||||||
|
|
||||||
@@ -53,12 +50,15 @@ class IgnisHeadlessSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onunload() {
|
onunload() {
|
||||||
|
if (this._syncStatusBarCleanup) {
|
||||||
|
this._syncStatusBarCleanup();
|
||||||
|
this._syncStatusBarCleanup = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.wsListener) {
|
if (this.wsListener) {
|
||||||
this.wsListener.stop();
|
this.wsListener.stop();
|
||||||
this.wsListener = null;
|
this.wsListener = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[ignis-headless-sync] Unloaded");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -234,41 +234,73 @@ class HeadlessSyncSettingTab extends PluginSettingTab {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log viewer
|
// Log viewer (collapsible)
|
||||||
await this.renderLogs(containerEl, vaultId);
|
await this.renderLogs(containerEl, vaultId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async renderLogs(containerEl, vaultId) {
|
async renderLogs(containerEl, vaultId) {
|
||||||
containerEl.createEl("h3", { text: "Recent logs" });
|
const details = containerEl.createEl("details", {
|
||||||
|
cls: "ignis-log-details",
|
||||||
|
});
|
||||||
|
|
||||||
|
details.createEl("summary", { text: "Sync logs" });
|
||||||
|
|
||||||
|
const logBox = details.createEl("pre", { cls: "ignis-log-terminal" });
|
||||||
|
const codeEl = logBox.createEl("code");
|
||||||
|
|
||||||
let logsData;
|
let logsData;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logsData = await api.getLogs(vaultId, 50);
|
logsData = await api.getLogs(vaultId, 50);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
containerEl.createEl("p", {
|
codeEl.textContent = `Failed to load logs: ${e.message}`;
|
||||||
text: `Failed to load logs: ${e.message}`,
|
|
||||||
cls: "mod-warning",
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const logContainer = containerEl.createDiv("ignis-log-viewer");
|
|
||||||
|
|
||||||
if (logsData.logs.length === 0) {
|
if (logsData.logs.length === 0) {
|
||||||
logContainer.createEl("p", {
|
codeEl.textContent = "No log entries yet.";
|
||||||
text: "No log entries yet.",
|
|
||||||
cls: "setting-item-description",
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
for (const entry of logsData.logs) {
|
const lines = logsData.logs.map((entry) => {
|
||||||
const time = new Date(entry.timestamp).toLocaleTimeString();
|
const time = new Date(entry.timestamp).toLocaleTimeString();
|
||||||
logContainer.createEl("div", {
|
return `[${time}] ${entry.line}`;
|
||||||
text: `[${time}] ${entry.line}`,
|
});
|
||||||
cls: "ignis-log-entry",
|
|
||||||
});
|
codeEl.textContent = lines.join("\n");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logBox.scrollTop = logBox.scrollHeight;
|
||||||
|
|
||||||
|
// Live updates via WebSocket
|
||||||
|
const wsListener = this.plugin.wsListener;
|
||||||
|
|
||||||
|
if (!wsListener) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onLog = (payload) => {
|
||||||
|
if (payload.vaultId !== vaultId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const time = new Date().toLocaleTimeString();
|
||||||
|
const line = `[${time}] ${payload.line}`;
|
||||||
|
|
||||||
|
if (codeEl.textContent === "No log entries yet.") {
|
||||||
|
codeEl.textContent = line;
|
||||||
|
} else {
|
||||||
|
codeEl.textContent += "\n" + line;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNearBottom =
|
||||||
|
logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 50;
|
||||||
|
|
||||||
|
if (isNearBottom) {
|
||||||
|
logBox.scrollTop = logBox.scrollHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
wsListener.on("sync-log", onLog);
|
||||||
|
this._logCleanup = () => wsListener.off("sync-log", onLog);
|
||||||
}
|
}
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
@@ -277,6 +309,11 @@ class HeadlessSyncSettingTab extends PluginSettingTab {
|
|||||||
this._cancelWait = null;
|
this._cancelWait = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._logCleanup) {
|
||||||
|
this._logCleanup();
|
||||||
|
this._logCleanup = null;
|
||||||
|
}
|
||||||
|
|
||||||
super.hide();
|
super.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
274
server/plugins/headless-sync/plugin/src/sync-status-bar.js
Normal file
274
server/plugins/headless-sync/plugin/src/sync-status-bar.js
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
const { setIcon } = require("obsidian");
|
||||||
|
const api = require("./api");
|
||||||
|
|
||||||
|
const TOOLTIP_MAP = {
|
||||||
|
running: "Syncing...",
|
||||||
|
synced: "Synced",
|
||||||
|
stopped: "Sync stopped",
|
||||||
|
error: "Sync error",
|
||||||
|
};
|
||||||
|
|
||||||
|
function initSyncStatusBar(plugin, wsListener) {
|
||||||
|
const vaultId = plugin.app.vault.getName();
|
||||||
|
const item = plugin.addStatusBarItem();
|
||||||
|
item.addClass("ignis-sync-statusbar");
|
||||||
|
item.style.display = "none";
|
||||||
|
|
||||||
|
const iconEl = item.createEl("span", { cls: "ignis-sync-icon" });
|
||||||
|
setIcon(iconEl, "refresh-cw");
|
||||||
|
|
||||||
|
let popoverEl = null;
|
||||||
|
let popoverOpen = false;
|
||||||
|
let currentStatus = "stopped";
|
||||||
|
let outsideClickHandler = null;
|
||||||
|
|
||||||
|
function updateState(status, error) {
|
||||||
|
currentStatus = status;
|
||||||
|
|
||||||
|
iconEl.className = "ignis-sync-icon";
|
||||||
|
|
||||||
|
if (status === "running") {
|
||||||
|
iconEl.addClass("ignis-sync-syncing");
|
||||||
|
iconEl.addClass("ignis-sync-spinning");
|
||||||
|
} else if (status === "error") {
|
||||||
|
iconEl.addClass("ignis-sync-error");
|
||||||
|
} else if (status === "stopped") {
|
||||||
|
iconEl.addClass("ignis-sync-stopped");
|
||||||
|
} else {
|
||||||
|
iconEl.addClass("ignis-sync-synced");
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltip = error || TOOLTIP_MAP[status] || status;
|
||||||
|
item.setAttribute("aria-label", tooltip);
|
||||||
|
item.setAttribute("data-tooltip-position", "top");
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPopover(text) {
|
||||||
|
if (popoverEl) {
|
||||||
|
const span = popoverEl.querySelector(".ignis-sync-popover-filename");
|
||||||
|
|
||||||
|
if (span) {
|
||||||
|
span.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
popoverEl = item.createEl("div", { cls: "ignis-sync-popover" });
|
||||||
|
popoverEl.createEl("span", {
|
||||||
|
text: text,
|
||||||
|
cls: "ignis-sync-popover-filename",
|
||||||
|
});
|
||||||
|
|
||||||
|
popoverOpen = true;
|
||||||
|
|
||||||
|
outsideClickHandler = (e) => {
|
||||||
|
if (!item.contains(e.target)) {
|
||||||
|
hidePopover();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener("click", outsideClickHandler, true);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hidePopover() {
|
||||||
|
if (popoverEl) {
|
||||||
|
popoverEl.remove();
|
||||||
|
popoverEl = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outsideClickHandler) {
|
||||||
|
document.removeEventListener("click", outsideClickHandler, true);
|
||||||
|
outsideClickHandler = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
popoverOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncatePath(path, maxLen) {
|
||||||
|
if (path.length <= maxLen) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\u2026" + path.slice(-(maxLen - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPopoverText(prefix, path) {
|
||||||
|
return `${prefix}: ${truncatePath(path, 46 - prefix.length)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePopoverText(text) {
|
||||||
|
if (!popoverOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const span = popoverEl?.querySelector(".ignis-sync-popover-filename");
|
||||||
|
|
||||||
|
if (span) {
|
||||||
|
span.textContent = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractFileActivity(line) {
|
||||||
|
// Downloading/Downloaded path
|
||||||
|
let match = line.match(/^(?:Downloading|Downloaded)\s+(.+)$/);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
return { prefix: "Syncing", path: match[1].trim() };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deleting path
|
||||||
|
match = line.match(/^Deleting\s+(.+)$/);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
return { prefix: "Deleting", path: match[1].trim() };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push: path (updated)
|
||||||
|
match = line.match(/^Push:\s+(.+?)\s+\(updated\)$/);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
return { prefix: "Syncing", path: match[1].trim() };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push: path (deleted)
|
||||||
|
match = line.match(/^Push:\s+(.+?)\s+\(deleted\)$/);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
return { prefix: "Deleting", path: match[1].trim() };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFullySynced(line) {
|
||||||
|
return /Fully synced/i.test(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click toggles popover
|
||||||
|
item.addEventListener("click", () => {
|
||||||
|
if (popoverOpen) {
|
||||||
|
hidePopover();
|
||||||
|
} else {
|
||||||
|
showPopover(TOOLTIP_MAP[currentStatus] || currentStatus);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for status updates
|
||||||
|
const onStatus = (payload) => {
|
||||||
|
if (payload.vaultId !== vaultId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
item.style.display = "";
|
||||||
|
|
||||||
|
// "running" from server means the process is alive, but we refine
|
||||||
|
// the visual state based on log activity.
|
||||||
|
if (payload.status === "running") {
|
||||||
|
updateState("synced");
|
||||||
|
} else {
|
||||||
|
updateState(payload.status, payload.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
wsListener.on("sync-status", onStatus);
|
||||||
|
|
||||||
|
// Debounce the transition to "synced" state to avoid flickering
|
||||||
|
// during rapid delete cycles (Fully synced -> Deleting -> Fully synced).
|
||||||
|
let syncedTimer = null;
|
||||||
|
|
||||||
|
function deferSynced() {
|
||||||
|
if (syncedTimer) {
|
||||||
|
clearTimeout(syncedTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
syncedTimer = setTimeout(() => {
|
||||||
|
syncedTimer = null;
|
||||||
|
updateState("synced");
|
||||||
|
updatePopoverText("Synced");
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelDeferredSynced() {
|
||||||
|
if (syncedTimer) {
|
||||||
|
clearTimeout(syncedTimer);
|
||||||
|
syncedTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for log lines
|
||||||
|
const onLog = (payload) => {
|
||||||
|
if (payload.vaultId !== vaultId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFullySynced(payload.line)) {
|
||||||
|
deferSynced();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activity = extractFileActivity(payload.line);
|
||||||
|
|
||||||
|
if (activity) {
|
||||||
|
cancelDeferredSynced();
|
||||||
|
updateState("running");
|
||||||
|
updatePopoverText(formatPopoverText(activity.prefix, activity.path));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
wsListener.on("sync-log", onLog);
|
||||||
|
|
||||||
|
// Fetch initial state
|
||||||
|
api
|
||||||
|
.getVaults()
|
||||||
|
.then((data) => {
|
||||||
|
const vaults = data.vaults || [];
|
||||||
|
const vault = vaults.find((v) => v.vaultId === vaultId);
|
||||||
|
|
||||||
|
if (vault) {
|
||||||
|
item.style.display = "";
|
||||||
|
updateState(vault.status, vault.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
|
// Poll WebSocket state to detect server disconnect/reconnect
|
||||||
|
let wasDisconnected = false;
|
||||||
|
|
||||||
|
const wsCheckInterval = setInterval(() => {
|
||||||
|
const ws = window.__ignisWs;
|
||||||
|
const disconnected = !ws || ws.readyState !== WebSocket.OPEN;
|
||||||
|
|
||||||
|
if (disconnected && currentStatus === "running") {
|
||||||
|
updateState("error", "Server connection lost");
|
||||||
|
wasDisconnected = true;
|
||||||
|
} else if (!disconnected && wasDisconnected) {
|
||||||
|
wasDisconnected = false;
|
||||||
|
|
||||||
|
api
|
||||||
|
.getVaults()
|
||||||
|
.then((data) => {
|
||||||
|
const vaults = data.vaults || [];
|
||||||
|
const vault = vaults.find((v) => v.vaultId === vaultId);
|
||||||
|
|
||||||
|
if (vault) {
|
||||||
|
updateState(vault.status, vault.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
// Return cleanup function
|
||||||
|
return () => {
|
||||||
|
clearInterval(wsCheckInterval);
|
||||||
|
cancelDeferredSynced();
|
||||||
|
wsListener.off("sync-status", onStatus);
|
||||||
|
wsListener.off("sync-log", onLog);
|
||||||
|
hidePopover();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { initSyncStatusBar };
|
||||||
@@ -38,3 +38,97 @@
|
|||||||
.ignis-vault-connect-options .setting-item {
|
.ignis-vault-connect-options .setting-item {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ignis-sync-statusbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ignis-sync-icon svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ignis-sync-spinning svg {
|
||||||
|
animation: ignis-spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ignis-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ignis-sync-synced svg {
|
||||||
|
color: var(--color-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ignis-sync-syncing svg {
|
||||||
|
color: var(--interactive-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ignis-sync-error svg {
|
||||||
|
color: var(--color-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ignis-sync-stopped svg {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ignis-sync-popover {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
right: 0;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
background: var(--background-modifier-message);
|
||||||
|
color: var(--text-normal);
|
||||||
|
font-size: var(--font-ui-smaller);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
white-space: nowrap;
|
||||||
|
box-shadow: var(--shadow-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ignis-log-details {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ignis-log-details summary {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: var(--font-ui-small);
|
||||||
|
padding: 4px 0;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ignis-log-details summary:hover {
|
||||||
|
color: var(--text-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ignis-log-terminal {
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #d4d4d4;
|
||||||
|
font-family: var(--font-monospace);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
max-height: 250px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
margin-top: 8px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ignis-log-terminal code {
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ function mountRoutes(router, plugin) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/unlink", (req, res) => {
|
router.post("/unlink", async (req, res) => {
|
||||||
const ctx = plugin.getCtx();
|
const ctx = plugin.getCtx();
|
||||||
const syncManager = plugin.getSyncManager();
|
const syncManager = plugin.getSyncManager();
|
||||||
const { vaultId } = req.body;
|
const { vaultId } = req.body;
|
||||||
@@ -126,7 +126,7 @@ function mountRoutes(router, plugin) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
syncManager.unlinkVault(vaultId);
|
await syncManager.unlinkVault(vaultId);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ctx.log(`Failed to unlink vault: ${e.message}`);
|
ctx.log(`Failed to unlink vault: ${e.message}`);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { spawnOb } = require("./ob-cli");
|
const { spawnOb, runCommand } = require("./ob-cli");
|
||||||
|
|
||||||
const MAX_LOG_ENTRIES = 200;
|
const MAX_LOG_ENTRIES = 200;
|
||||||
|
|
||||||
@@ -142,6 +142,7 @@ class SyncManager {
|
|||||||
if (line.trim()) {
|
if (line.trim()) {
|
||||||
this.addLog(state, line.trim());
|
this.addLog(state, line.trim());
|
||||||
state.lastActivity = new Date().toISOString();
|
state.lastActivity = new Date().toISOString();
|
||||||
|
this.broadcastLog(vaultId, line.trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -213,7 +214,7 @@ class SyncManager {
|
|||||||
return this.getState(vaultId);
|
return this.getState(vaultId);
|
||||||
}
|
}
|
||||||
|
|
||||||
unlinkVault(vaultId) {
|
async unlinkVault(vaultId) {
|
||||||
const state = this.states.get(vaultId);
|
const state = this.states.get(vaultId);
|
||||||
|
|
||||||
if (!state) {
|
if (!state) {
|
||||||
@@ -224,6 +225,14 @@ class SyncManager {
|
|||||||
state._process.kill("SIGTERM");
|
state._process.kill("SIGTERM");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tell ob to disconnect from the remote vault and clear its stored config
|
||||||
|
try {
|
||||||
|
await runCommand(["sync-unlink", "--path", state.vaultPath]);
|
||||||
|
this.ctx.log(`ob sync-unlink completed for ${vaultId}`);
|
||||||
|
} catch (e) {
|
||||||
|
this.ctx.log(`ob sync-unlink failed for ${vaultId}: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
this.states.delete(vaultId);
|
this.states.delete(vaultId);
|
||||||
this.saveStates();
|
this.saveStates();
|
||||||
this.ctx.log(`Unlinked vault ${vaultId}`);
|
this.ctx.log(`Unlinked vault ${vaultId}`);
|
||||||
@@ -280,6 +289,24 @@ class SyncManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
broadcastLog(vaultId, line) {
|
||||||
|
if (!this.ctx.wss || !this.ctx.wss.clients) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = JSON.stringify({
|
||||||
|
channel: "plugin:headless-sync",
|
||||||
|
type: "sync-log",
|
||||||
|
payload: { vaultId, line },
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const client of this.ctx.wss.clients) {
|
||||||
|
if (client.readyState === 1) {
|
||||||
|
client.send(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
broadcastStatus(vaultId) {
|
broadcastStatus(vaultId) {
|
||||||
const state = this.getState(vaultId);
|
const state = this.getState(vaultId);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user