diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index e0e5350..6f425f7 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -125,6 +125,9 @@ jobs: - name: Install dependencies run: npm ci + - name: Prepare Linux desktop patch bundle + run: npm run prepare:linux-desktop-bundle + - name: Build injector linux package run: npm run dist:linux diff --git a/.gitignore b/.gitignore index da938ad..41b1f11 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,17 @@ cache/ # Local app copy / extraction workspace app_do_not_edit/ +do_not_edit/* +!do_not_edit/fluxer/ +do_not_edit/fluxer/* +!do_not_edit/fluxer/fluxer_desktop/ +!do_not_edit/fluxer/fluxer_desktop/** +do_not_edit/fluxer/.git/ +do_not_edit/fluxer/fluxer_desktop/node_modules/ +do_not_edit/fluxer/fluxer_desktop/dist/* +!do_not_edit/fluxer/fluxer_desktop/dist/main/ +do_not_edit/fluxer/fluxer_desktop/dist/main/* +!do_not_edit/fluxer/fluxer_desktop/dist/main/index.js MyPlugins/ MainFluxerApp/ # Logs diff --git a/bridge-nw/app.js b/bridge-nw/app.js index f2feebb..6366091 100644 --- a/bridge-nw/app.js +++ b/bridge-nw/app.js @@ -12,12 +12,61 @@ const startBtn = document.getElementById("startBtn"); const stopBtn = document.getElementById("stopBtn"); const healthBtn = document.getElementById("healthBtn"); const probeBtn = document.getElementById("probeBtn"); +const nerdModeToggleEl = document.getElementById("nerdModeToggle"); +const updateIntervalInputEl = document.getElementById("updateIntervalInput"); const statusEl = document.getElementById("status"); const logEl = document.getElementById("log"); let bridgeProc = null; let tray = null; let isQuitting = false; +let healthMonitorTimer = null; +const SETTINGS_FILE = path.join(getAppDataHome(), APP_NAME, "data", "bridge-settings.json"); +const DEFAULT_UPDATE_INTERVAL_MS = 10000; + +function normalizeUpdateIntervalMs(value) { + const n = Number.parseInt(String(value || ""), 10); + if (!Number.isFinite(n)) return DEFAULT_UPDATE_INTERVAL_MS; + return Math.max(1000, Math.min(60000, Math.round(n))); +} + +function ensureBridgeDataDir() { + fs.mkdirSync(path.dirname(SETTINGS_FILE), { recursive: true }); +} + +function loadBridgeSettings() { + try { + if (!fs.existsSync(SETTINGS_FILE)) { + return { nerdModeEnabled: false, updateIntervalMs: DEFAULT_UPDATE_INTERVAL_MS }; + } + const parsed = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf8")); + return { + nerdModeEnabled: Boolean(parsed && parsed.nerdModeEnabled), + updateIntervalMs: normalizeUpdateIntervalMs(parsed && parsed.updateIntervalMs) + }; + } catch (_) { + return { nerdModeEnabled: false, updateIntervalMs: DEFAULT_UPDATE_INTERVAL_MS }; + } +} + +function saveBridgeSettings(nextSettings) { + ensureBridgeDataDir(); + const payload = { + nerdModeEnabled: Boolean(nextSettings && nextSettings.nerdModeEnabled), + updateIntervalMs: normalizeUpdateIntervalMs(nextSettings && nextSettings.updateIntervalMs) + }; + fs.writeFileSync(SETTINGS_FILE, `${JSON.stringify(payload, null, 2)}\n`, "utf8"); + return payload; +} + +function applyBridgeSettingsToUi(settings) { + if (nerdModeToggleEl) { + nerdModeToggleEl.checked = Boolean(settings && settings.nerdModeEnabled); + } + if (updateIntervalInputEl) { + updateIntervalInputEl.value = String(normalizeUpdateIntervalMs(settings && settings.updateIntervalMs)); + } +} function getBundleRoot() { try { @@ -44,6 +93,12 @@ function getTrayIconPath() { function resolveNodeExec() { const npmNode = String(process.env.npm_node_execpath || "").trim(); if (npmNode && fs.existsSync(npmNode)) return npmNode; + const runtimeNodeCandidates = [ + path.join(getBridgeRuntimeRoot(), "node"), + path.join(getBridgeRuntimeRoot(), "node.exe") + ]; + const runtimeNode = runtimeNodeCandidates.find((p) => fs.existsSync(p)); + if (runtimeNode) return runtimeNode; if (process.platform === "win32") { const localNodeCandidates = [ path.join(path.resolve(process.cwd(), ".."), "node.exe"), @@ -112,6 +167,26 @@ function getBundledBridgeExePath() { return candidates.find((p) => fs.existsSync(p)) || ""; } +function getBundledNodePath() { + const bundleRoot = getBundleRoot(); + const candidates = process.platform === "win32" + ? [ + path.join(bundleRoot, "node.exe"), + path.join(bundleRoot, "bridge-nw", "node.exe"), + path.join(bundleRoot, "package.nw", "node.exe"), + path.join(path.dirname(process.execPath), "node.exe"), + path.join(path.dirname(process.execPath), "package.nw", "node.exe") + ] + : [ + path.join(bundleRoot, "node"), + path.join(bundleRoot, "bridge-nw", "node"), + path.join(bundleRoot, "package.nw", "node"), + path.join(path.dirname(process.execPath), "node"), + path.join(path.dirname(process.execPath), "package.nw", "node") + ]; + return candidates.find((p) => fs.existsSync(p)) || ""; +} + function materializeBridgeRuntimeFiles() { const sourceBridgeScript = getBundledBridgeScriptPath(); if (!sourceBridgeScript) return ""; @@ -138,6 +213,16 @@ function materializeBridgeRuntimeFiles() { fs.copyFileSync(bundledEnv, path.join(runtimeBridgeEnvDir, ".env")); } + const bundledNode = getBundledNodePath(); + if (bundledNode) { + const runtimeNodeName = process.platform === "win32" ? "node.exe" : "node"; + const runtimeNodePath = path.join(runtimeRoot, runtimeNodeName); + fs.copyFileSync(bundledNode, runtimeNodePath); + try { + if (process.platform !== "win32") fs.chmodSync(runtimeNodePath, 0o755); + } catch (_) {} + } + return runtimeBridgeScript; } @@ -151,6 +236,10 @@ function log(line) { logEl.scrollTop = logEl.scrollHeight; } +function statusIsRunning() { + return String(statusEl.textContent || "").toLowerCase().includes("running"); +} + function httpJson(url) { return new Promise((resolve, reject) => { const req = http.get(url, (res) => { @@ -170,6 +259,51 @@ function httpJson(url) { }); } +async function isBridgeHealthy() { + try { + const health = await httpJson(`${BRIDGE_URL}/health`); + return Boolean(health && health.ok); + } catch (_) { + return false; + } +} + +function stopHealthMonitor() { + if (healthMonitorTimer) { + clearInterval(healthMonitorTimer); + healthMonitorTimer = null; + } +} + +function startHealthMonitor() { + stopHealthMonitor(); + healthMonitorTimer = setInterval(async () => { + if (!bridgeProc) { + stopHealthMonitor(); + return; + } + const healthy = await isBridgeHealthy(); + if (healthy) { + setStatus("running"); + return; + } + if (!statusIsRunning()) { + setStatus("starting..."); + } + }, 3000); +} + +async function waitForBridgeReady(maxWaitMs = 12000) { + const startAt = Date.now(); + while (Date.now() - startAt < maxWaitMs) { + if (!bridgeProc) return false; + const healthy = await isBridgeHealthy(); + if (healthy) return true; + await new Promise((resolve) => setTimeout(resolve, 300)); + } + return false; +} + function startBridge() { if (bridgeProc) { log("Bridge already running in this app session."); @@ -201,20 +335,39 @@ function startBridge() { windowsHide: false }); setStatus("starting..."); - bridgeProc.stdout.on("data", (chunk) => log(String(chunk).trimEnd())); + bridgeProc.stdout.on("data", (chunk) => { + const text = String(chunk || "").trimEnd(); + log(text); + if (/Listening on http:\/\/127\.0\.0\.1:\d+/i.test(text)) { + setStatus("running"); + } + }); bridgeProc.stderr.on("data", (chunk) => log(String(chunk).trimEnd())); bridgeProc.on("error", (err) => { + stopHealthMonitor(); log(`Bridge launch failed: ${err && err.message ? err.message : String(err)}`); bridgeProc = null; setStatus("failed"); }); bridgeProc.on("exit", (code) => { + stopHealthMonitor(); log(`Bridge exited with code ${code}`); bridgeProc = null; setStatus("stopped"); }); log("Bridge process started."); + startHealthMonitor(); + waitForBridgeReady().then((ready) => { + if (!bridgeProc) return; + if (ready) { + setStatus("running"); + return; + } + setStatus("starting..."); + log("Bridge did not report healthy within 12s. Use Check Health for details."); + }); } catch (err) { + stopHealthMonitor(); log(`Start handler error: ${err && err.message ? err.message : String(err)}`); setStatus("failed"); } @@ -227,6 +380,7 @@ function stopBridge() { } try { bridgeProc.kill(); + stopHealthMonitor(); log("Sent stop signal to bridge process."); } catch (err) { log(`Failed to stop bridge: ${err.message}`); @@ -237,6 +391,12 @@ async function checkHealth() { try { const health = await httpJson(`${BRIDGE_URL}/health`); setStatus(health && health.ok ? "running" : "degraded"); + if (health && (typeof health.nerdModeEnabled === "boolean" || health.updateIntervalMs != null)) { + applyBridgeSettingsToUi({ + nerdModeEnabled: health.nerdModeEnabled, + updateIntervalMs: health.updateIntervalMs + }); + } log(`Health: ${JSON.stringify(health)}`); } catch (err) { setStatus("offline"); @@ -262,8 +422,28 @@ startBtn.addEventListener("click", startBridge); stopBtn.addEventListener("click", stopBridge); healthBtn.addEventListener("click", checkHealth); probeBtn.addEventListener("click", probeNowPlaying); +if (nerdModeToggleEl) { + nerdModeToggleEl.addEventListener("change", () => { + const settings = saveBridgeSettings({ + nerdModeEnabled: Boolean(nerdModeToggleEl.checked), + updateIntervalMs: updateIntervalInputEl ? updateIntervalInputEl.value : DEFAULT_UPDATE_INTERVAL_MS + }); + log(`Nerd Mode ${settings.nerdModeEnabled ? "enabled" : "disabled"}.`); + }); +} +if (updateIntervalInputEl) { + updateIntervalInputEl.addEventListener("change", () => { + const settings = saveBridgeSettings({ + nerdModeEnabled: nerdModeToggleEl ? Boolean(nerdModeToggleEl.checked) : false, + updateIntervalMs: updateIntervalInputEl.value + }); + applyBridgeSettingsToUi(settings); + log(`Update interval set to ${settings.updateIntervalMs}ms.`); + }); +} window.addEventListener("beforeunload", () => { + stopHealthMonitor(); if (bridgeProc) { try { bridgeProc.kill(); @@ -356,5 +536,6 @@ function setupTrayBehavior() { } setStatus("idle"); +applyBridgeSettingsToUi(loadBridgeSettings()); log("Bridge NW app ready."); setupTrayBehavior(); diff --git a/bridge-nw/index.html b/bridge-nw/index.html index a30edb8..d62e74c 100644 --- a/bridge-nw/index.html +++ b/bridge-nw/index.html @@ -11,6 +11,10 @@ button { background: #1f2430; color: #e7e9ee; border: 1px solid #2e3544; border-radius: 8px; padding: 8px 12px; cursor: pointer; } button:hover { background: #2a3142; } .status { background: #121622; border: 1px solid #2e3544; border-radius: 8px; padding: 10px; margin-bottom: 12px; } + .toggleRow { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; padding: 10px 12px; background: #121622; border: 1px solid #2e3544; border-radius: 8px; } + .toggleRow label { display: flex; align-items: center; gap: 8px; cursor: pointer; } + .toggleHint { color: #9aa4b4; font-size: 12px; } + .settingInput { background: #0b0d14; color: #e7e9ee; border: 1px solid #2e3544; border-radius: 6px; padding: 6px 8px; width: 120px; } pre { background: #0b0d14; border: 1px solid #2e3544; border-radius: 8px; padding: 12px; height: 460px; overflow: auto; white-space: pre-wrap; } code { color: #8ad1ff; } @@ -24,6 +28,18 @@

BetterFluxer Bridge (NW.js)

+
+ + When nothing is playing, publish system stats instead. +
+
+ + + Suggested poll interval for status sync. +
Status: idle

   
diff --git a/bridge-nw/scripts/local-bridge.js b/bridge-nw/scripts/local-bridge.js
index bdb6d07..94a04f4 100644
--- a/bridge-nw/scripts/local-bridge.js
+++ b/bridge-nw/scripts/local-bridge.js
@@ -31,13 +31,20 @@ const BRIDGE_BASE_DIR = getBridgeBaseDir();
 const DATA_DIR = path.join(BRIDGE_BASE_DIR, "data");
 const TOKEN_FILE = path.join(DATA_DIR, "bridge-token.txt");
 const CACHE_FILE = path.join(DATA_DIR, "bridge-cache.json");
+const SETTINGS_FILE = path.join(DATA_DIR, "bridge-settings.json");
 
 const HOST = "127.0.0.1";
 const PORT = Number.parseInt(process.env.BF_BRIDGE_PORT || "21864", 10);
-const BRIDGE_VERSION = "2026-03-07-rpc-tuna";
+const BRIDGE_VERSION_BY_OS = {
+  win32: "2026-03-10-rpc-win",
+  linux: "2026-03-10-rpc-linux",
+  darwin: "2026-03-10-rpc-mac"
+};
+const BRIDGE_VERSION = BRIDGE_VERSION_BY_OS[process.platform] || "2026-03-10-rpc";
 const REQUEST_TIMEOUT_MS = Number.parseInt(process.env.BF_BRIDGE_TIMEOUT_MS || "12000", 10);
 const DEFAULT_TTL_SECONDS = Number.parseInt(process.env.BF_BRIDGE_DEFAULT_TTL || "120", 10);
 const MAX_TTL_SECONDS = Number.parseInt(process.env.BF_BRIDGE_MAX_TTL || "1800", 10);
+const DEFAULT_UPDATE_INTERVAL_MS = Number.parseInt(process.env.BF_BRIDGE_UPDATE_INTERVAL_MS || "10000", 10);
 const WINDOWS_RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
 const WINDOWS_RUN_VALUE = "BetterFluxerBridge";
 const LINUX_BIN_DIR = path.join(os.homedir(), ".local", "bin");
@@ -111,6 +118,12 @@ function normalizeTtlSeconds(input) {
   return Math.max(1, Math.min(MAX_TTL_SECONDS, n));
 }
 
+function normalizeUpdateIntervalMs(input) {
+  const n = Number.parseInt(String(input || ""), 10);
+  if (!Number.isFinite(n) || n <= 0) return DEFAULT_UPDATE_INTERVAL_MS;
+  return Math.max(1000, Math.min(60000, Math.round(n)));
+}
+
 function readJsonBody(req) {
   return new Promise((resolve) => {
     let raw = "";
@@ -148,6 +161,21 @@ function saveCache(cache) {
   fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf8");
 }
 
+function loadBridgeSettings() {
+  if (!fs.existsSync(SETTINGS_FILE)) {
+    return { nerdModeEnabled: false, updateIntervalMs: DEFAULT_UPDATE_INTERVAL_MS };
+  }
+  try {
+    const parsed = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf8"));
+    return {
+      nerdModeEnabled: Boolean(parsed && parsed.nerdModeEnabled),
+      updateIntervalMs: normalizeUpdateIntervalMs(parsed && parsed.updateIntervalMs)
+    };
+  } catch (_) {
+    return { nerdModeEnabled: false, updateIntervalMs: DEFAULT_UPDATE_INTERVAL_MS };
+  }
+}
+
 function parseArgv(argv) {
   const out = {};
   for (const item of argv || []) {
@@ -387,6 +415,41 @@ function normalizeNowPlayingPayload(payload, source) {
   };
 }
 
+function formatDurationShort(totalSeconds) {
+  const sec = Math.max(0, Math.floor(Number(totalSeconds) || 0));
+  const hours = Math.floor(sec / 3600);
+  const minutes = Math.floor((sec % 3600) / 60);
+  const seconds = sec % 60;
+  if (hours > 0) return `${hours}h ${minutes}m`;
+  if (minutes > 0) return `${minutes}m ${seconds}s`;
+  return `${seconds}s`;
+}
+
+function buildNerdModeNowPlaying() {
+  const totalMem = os.totalmem();
+  const freeMem = os.freemem();
+  const usedMem = Math.max(0, totalMem - freeMem);
+  const memPercent = totalMem > 0 ? Math.round((usedMem / totalMem) * 100) : 0;
+  const loadAvg = os.loadavg();
+  const cpuCount = Array.isArray(os.cpus()) ? os.cpus().length : 0;
+  const hostname = String(os.hostname() || "").trim();
+  const details = `CPU ${Number(loadAvg[0] || 0).toFixed(2)} | RAM ${memPercent}%`;
+  const state = `${hostname || process.platform} | up ${formatDurationShort(os.uptime())}`;
+  return normalizeNowPlayingPayload(
+    {
+      source: "nerd-mode",
+      kind: "system",
+      title: "Nerd Mode",
+      details,
+      state,
+      appId: hostname || process.platform,
+      playbackStatus: "idle",
+      name: `System Stats (${cpuCount} cores)`
+    },
+    "nerd-mode"
+  );
+}
+
 function makeRpcFrame(op, payloadObject) {
   const payload = Buffer.from(JSON.stringify(payloadObject || {}), "utf8");
   const frame = Buffer.allocUnsafe(8 + payload.length);
@@ -418,6 +481,28 @@ function decodeRpcFrames(buffer, onFrame) {
   return buffer.slice(cursor);
 }
 
+function isIgnorableSocketWriteError(error) {
+  const code = String((error && error.code) || "");
+  return code === "EPIPE" || code === "ECONNRESET" || code === "ERR_STREAM_DESTROYED";
+}
+
+function safeSocketWrite(socket, frame) {
+  if (!socket || socket.destroyed) return false;
+  try {
+    socket.write(frame, (error) => {
+      if (error && !isIgnorableSocketWriteError(error)) {
+        console.warn("[BetterFluxer Bridge] RPC socket write error:", String(error.message || error));
+      }
+    });
+    return true;
+  } catch (error) {
+    if (!isIgnorableSocketWriteError(error)) {
+      console.warn("[BetterFluxer Bridge] RPC socket write failed:", String((error && error.message) || error || "unknown"));
+    }
+    return false;
+  }
+}
+
 function sendJson(res, status, payload, origin) {
   if (origin) res.setHeader("Access-Control-Allow-Origin", origin);
   res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization,X-BetterFluxer-Token");
@@ -553,7 +638,7 @@ function handleDiscordRpcMessage(message, state, socket) {
     state.lastRpcActivityAt = Date.now();
     console.log(`[BetterFluxer Bridge] RPC activity captured: ${formatNowPlayingForLog(payload)}`);
     if (socket && !socket.destroyed) {
-      socket.write(makeRpcFrame(1, { cmd: "SET_ACTIVITY", data: {}, evt: null, nonce }));
+      safeSocketWrite(socket, makeRpcFrame(1, { cmd: "SET_ACTIVITY", data: {}, evt: null, nonce }));
     }
     return;
   }
@@ -562,32 +647,116 @@ function handleDiscordRpcMessage(message, state, socket) {
     state.lastRpcActivity = null;
     state.lastRpcActivityAt = Date.now();
     if (socket && !socket.destroyed) {
-      socket.write(makeRpcFrame(1, { cmd: "CLEAR_ACTIVITY", data: {}, evt: null, nonce }));
+      safeSocketWrite(socket, makeRpcFrame(1, { cmd: "CLEAR_ACTIVITY", data: {}, evt: null, nonce }));
     }
     return;
   }
 
   if (socket && !socket.destroyed && nonce != null && cmd) {
-    socket.write(makeRpcFrame(1, { cmd, data: {}, evt: null, nonce }));
+    safeSocketWrite(socket, makeRpcFrame(1, { cmd, data: {}, evt: null, nonce }));
+  }
+}
+
+function getLinuxDiscordIpcDirs() {
+  const dirs = [];
+  const pushDir = (value) => {
+    const v = String(value || "").trim();
+    if (!v) return;
+    if (!dirs.includes(v)) dirs.push(v);
+  };
+
+  pushDir(process.env.BF_RPC_DIR);
+  pushDir(process.env.XDG_RUNTIME_DIR);
+  if (Number.isFinite(Number(process.getuid && process.getuid()))) {
+    pushDir(path.join("/run/user", String(process.getuid())));
+  }
+  pushDir("/tmp");
+  pushDir("/var/tmp");
+  pushDir("/dev/shm");
+
+  const expanded = [];
+  const suffixes = [
+    "",
+    "app/com.discordapp.Discord",
+    "app/com.discordapp.DiscordCanary",
+    "app/com.discordapp.DiscordPTB",
+    "app/com.vesktop.Vesktop",
+    "app/dev.vencord.Vesktop",
+    "snap.discord"
+  ];
+  for (const dir of dirs) {
+    for (const suffix of suffixes) {
+      const full = suffix ? path.join(dir, suffix) : dir;
+      if (!expanded.includes(full)) expanded.push(full);
+    }
+  }
+  return expanded;
+}
+
+function getDiscordRpcEndpoints() {
+  if (process.platform === "win32") {
+    return Array.from({ length: 10 }, (_, i) => `\\\\.\\pipe\\discord-ipc-${i}`);
+  }
+  if (process.platform === "linux") {
+    const out = [];
+    for (const base of getLinuxDiscordIpcDirs()) {
+      for (let i = 0; i < 10; i += 1) {
+        out.push(path.join(base, `discord-ipc-${i}`));
+      }
+    }
+    return out;
+  }
+  return [];
+}
+
+function ensureLinuxRpcSocketPath(socketPath) {
+  const dir = path.dirname(socketPath);
+  if (!fs.existsSync(dir)) {
+    return false;
   }
+  if (!fs.existsSync(socketPath)) return true;
+  try {
+    const stat = fs.lstatSync(socketPath);
+    if (stat.isSocket()) {
+      fs.unlinkSync(socketPath);
+      return true;
+    }
+  } catch (_) {}
+  return !fs.existsSync(socketPath);
 }
 
 function startDiscordRpcCapture(state) {
-  if (process.platform !== "win32") return;
+  if (process.platform !== "win32" && process.platform !== "linux") return;
   if (state.rpcServers && state.rpcServers.length) return;
   state.rpcServers = [];
   state.rpcBindErrors = [];
 
-  for (let i = 0; i < 10; i += 1) {
-    const pipeName = `\\\\.\\pipe\\discord-ipc-${i}`;
+  const endpoints = getDiscordRpcEndpoints();
+  for (const endpoint of endpoints) {
+    if (process.platform === "linux") {
+      try {
+        const ready = ensureLinuxRpcSocketPath(endpoint);
+        if (!ready) continue;
+      } catch (error) {
+        state.rpcBindErrors.push({ pipe: endpoint, error: String((error && error.message) || error || "unknown") });
+        continue;
+      }
+    }
+
     const server = net.createServer((socket) => {
       let buf = Buffer.alloc(0);
+      socket.on("error", (error) => {
+        if (!isIgnorableSocketWriteError(error)) {
+          console.warn("[BetterFluxer Bridge] RPC socket error:", String((error && error.message) || error || "unknown"));
+        }
+      });
       socket.on("data", (chunk) => {
         try {
           buf = Buffer.concat([buf, Buffer.from(chunk)]);
           buf = decodeRpcFrames(buf, (op, msg) => {
             if (op === 0) {
-              socket.write(
+              safeSocketWrite(
+                socket,
                 makeRpcFrame(1, {
                   cmd: "DISPATCH",
                   evt: "READY",
@@ -612,53 +781,24 @@ function startDiscordRpcCapture(state) {
     });
 
     server.on("error", (error) => {
-      state.rpcBindErrors.push({ pipe: pipeName, error: String((error && error.message) || error || "unknown") });
+      state.rpcBindErrors.push({ pipe: endpoint, error: String((error && error.message) || error || "unknown") });
     });
 
     try {
-      server.listen(pipeName, () => {
-        state.rpcServers.push({ pipe: pipeName, server });
+      server.listen(endpoint, () => {
+        if (process.platform === "linux") {
+          try {
+            fs.chmodSync(endpoint, 0o600);
+          } catch (_) {}
+        }
+        state.rpcServers.push({ pipe: endpoint, server });
       });
     } catch (error) {
-      state.rpcBindErrors.push({ pipe: pipeName, error: String((error && error.message) || error || "unknown") });
+      state.rpcBindErrors.push({ pipe: endpoint, error: String((error && error.message) || error || "unknown") });
     }
   }
 }
 
-async function queryLinuxMedia() {
-  if (process.platform !== "linux") {
-    return { ok: false, error: "Linux only", platform: process.platform };
-  }
-
-  const format = "{{title}}\t{{artist}}\t{{album}}\t{{playerName}}\t{{status}}";
-  try {
-    const result = await runCommand("playerctl", ["metadata", "--format", format], 4000);
-    if (result.code !== 0) {
-      const errText = (result.stderr || result.stdout || "").trim();
-      if (/No players found/i.test(errText)) {
-        return { ok: true, hasSession: false, source: "linux-mpris" };
-      }
-      return { ok: false, error: errText || `playerctl exited ${result.code}` };
-    }
-
-    const line = String(result.stdout || "").trim();
-    if (!line) return { ok: true, hasSession: false, source: "linux-mpris" };
-    const [title = "", artist = "", albumTitle = "", appId = "", playbackStatus = ""] = line.split("\t");
-    return {
-      ok: true,
-      hasSession: Boolean(String(title).trim() || String(artist).trim()),
-      source: "linux-mpris",
-      title: String(title || "").trim(),
-      artist: String(artist || "").trim(),
-      albumTitle: String(albumTitle || "").trim(),
-      appId: String(appId || "").trim(),
-      playbackStatus: String(playbackStatus || "").trim()
-    };
-  } catch (error) {
-    return { ok: false, error: String((error && error.message) || error || "playerctl failed") };
-  }
-}
-
 async function queryMacMedia() {
   if (process.platform !== "darwin") {
     return { ok: false, error: "macOS only", platform: process.platform };
@@ -727,6 +867,7 @@ async function queryMacMedia() {
 
 async function queryUniversalNowPlaying(state) {
   const now = Date.now();
+  let rpcFallback = null;
   if (state && state.lastRpcActivity && now - Number(state.lastRpcActivityAt || 0) < 180000) {
     const raw = state.lastRpcActivity.raw && typeof state.lastRpcActivity.raw === "object" ? state.lastRpcActivity.raw : {};
     const base = state.lastRpcActivity.normalized && typeof state.lastRpcActivity.normalized === "object"
@@ -741,31 +882,49 @@ async function queryUniversalNowPlaying(state) {
     if (startMs != null && endMs != null && endMs > startMs) {
       base.durationMs = endMs - startMs;
     }
-    return base;
+    if (base && base.ok && base.hasSession) {
+      return base;
+    }
+    rpcFallback = {
+      ok: true,
+      hasSession: false,
+      source: String(base.source || "discord-rpc-pipe"),
+      error: String(base.error || "No active RPC session")
+    };
   }
 
+  let fallback = null;
   if (process.platform === "win32") {
     const tuna = await queryTunaNowPlaying(state);
     if (tuna && tuna.ok && tuna.hasSession) return tuna;
-    return tuna && tuna.ok
+    fallback = tuna && tuna.ok
       ? tuna
       : { ok: false, hasSession: false, source: "windows-now-playing", error: "No active RPC or Tuna session" };
-  }
-  if (process.platform === "linux") {
-    const linux = await queryLinuxMedia();
-    if (linux.ok && linux.hasSession) return linux;
+  } else if (process.platform === "linux") {
     const tuna = await queryTunaNowPlaying(state);
     if (tuna && tuna.ok && tuna.hasSession) return tuna;
-    return linux.ok ? linux : tuna.ok ? tuna : linux;
-  }
-  if (process.platform === "darwin") {
+    fallback = tuna && tuna.ok
+      ? tuna
+      : { ok: false, hasSession: false, source: "linux-now-playing", error: "No active Tuna session" };
+  } else if (process.platform === "darwin") {
     const mac = await queryMacMedia();
     if (mac.ok && mac.hasSession) return mac;
     const tuna = await queryTunaNowPlaying(state);
     if (tuna && tuna.ok && tuna.hasSession) return tuna;
-    return mac.ok ? mac : tuna.ok ? tuna : mac;
+    fallback = mac.ok ? mac : tuna.ok ? tuna : mac;
+  } else {
+    fallback = { ok: false, error: `Unsupported platform: ${process.platform}` };
+  }
+
+  const preferredFallback =
+    fallback && fallback.hasSession
+      ? fallback
+      : rpcFallback || fallback;
+
+  if (loadBridgeSettings().nerdModeEnabled && (!preferredFallback || !preferredFallback.hasSession)) {
+    return buildNerdModeNowPlaying();
   }
-  return { ok: false, error: `Unsupported platform: ${process.platform}` };
+  return preferredFallback;
 }
 
 function formatNowPlayingForLog(np) {
@@ -906,6 +1065,7 @@ async function main() {
 
     const url = new URL(req.url || "/", `http://${HOST}:${PORT}`);
     if (url.pathname === "/health") {
+      const settings = loadBridgeSettings();
       return sendJson(
         res,
         200,
@@ -917,6 +1077,8 @@ async function main() {
           rpcPipesListening: (bridgeState.rpcServers || []).length,
           rpcPipeErrors: (bridgeState.rpcBindErrors || []).length,
           tunaPath: bridgeState.tunaPath,
+          nerdModeEnabled: Boolean(settings.nerdModeEnabled),
+          updateIntervalMs: settings.updateIntervalMs,
           allowlist,
           uptimeSec: Math.floor(process.uptime())
         },
diff --git a/do_not_edit/fluxer b/do_not_edit/fluxer
new file mode 160000
index 0000000..03813bb
--- /dev/null
+++ b/do_not_edit/fluxer
@@ -0,0 +1 @@
+Subproject commit 03813bbe17db008452f0f1be3090a7d2970a5447
diff --git a/nw/index.html b/nw/index.html
index 5bc695c..a2030bc 100644
--- a/nw/index.html
+++ b/nw/index.html
@@ -71,7 +71,7 @@
             Injecting
         
       
-      
+

Actions