$(function() { function TailscaleFunnelViewModel(parameters) { var self = this; self.settings = parameters[0]; // Status observables self.funnelStatus = ko.observable("Checking..."); self.funnelEnabled = ko.observable(false); self.publicUrl = ko.observable("Not available"); self.tailscaleInstalled = ko.observable(true); self.tailscaleRunning = ko.observable(true); // Button states self.refreshInProgress = ko.observable(false); self.toggleInProgress = ko.observable(false); // Initialize self.onStartup = function() { self.refreshStatus(); }; // Refresh the status self.refreshStatus = function() { if (self.refreshInProgress()) { return; } self.refreshInProgress(true); self.funnelStatus("Checking..."); $.ajax({ url: PLUGIN_BASEURL + "tailscale_funnel/status", type: "GET", dataType: "json", success: function(response) { if (response.status === "success") { self.tailscaleInstalled(response.data.tailscale_installed); self.tailscaleRunning(response.data.tailscale_running); if (response.data.tailscale_installed && response.data.tailscale_running) { self.funnelEnabled(response.data.funnel_enabled); self.funnelStatus(response.data.funnel_enabled ? "Enabled" : "Disabled"); self.publicUrl(response.data.public_url || "Not available"); } else if (!response.data.tailscale_installed) { self.funnelStatus("Tailscale not installed"); } else if (!response.data.tailscale_running) { self.funnelStatus("Tailscale not running"); } } else { self.funnelStatus("Error: " + response.message); } }, error: function(xhr, status, error) { self.funnelStatus("Error: " + error); }, complete: function() { self.refreshInProgress(false); } }); }; // Toggle funnel on/off self.toggleFunnel = function() { if (self.toggleInProgress()) { return; } var enable = !self.funnelEnabled(); // Show confirmation if required if (self.settings.settings.plugins.tailscale_funnel.confirm_enable() && enable) { if (!confirm("Enabling Funnel will make your OctoPrint instance accessible from the public internet. Do you want to continue?")) { return; } } self.toggleInProgress(true); self.funnelStatus(enable ? "Enabling..." : "Disabling..."); var action = enable ? "enable" : "disable"; $.ajax({ url: PLUGIN_BASEURL + "tailscale_funnel/" + action, type: "POST", dataType: "json", success: function(response) { if (response.status === "success") { self.funnelEnabled(enable); self.funnelStatus(enable ? "Enabled" : "Disabled"); if (enable && response.data && response.data.public_url) { self.publicUrl(response.data.public_url); } else if (!enable) { self.publicUrl("Not available"); } // Show success message new PNotify({ title: "Tailscale Funnel", text: response.message, type: "success" }); } else { self.funnelStatus("Error"); // Show error message new PNotify({ title: "Tailscale Funnel Error", text: response.message, type: "error" }); } }, error: function(xhr, status, error) { self.funnelStatus("Error"); // Show error message new PNotify({ title: "Tailscale Funnel Error", text: "Failed to " + action + " Funnel: " + error, type: "error" }); }, complete: function() { self.toggleInProgress(false); } }); }; // Copy URL to clipboard self.copyUrlToClipboard = function() { if (self.publicUrl() && self.publicUrl() !== "Not available") { navigator.clipboard.writeText(self.publicUrl()).then(function() { new PNotify({ title: "Copied to Clipboard", text: "Public URL copied to clipboard", type: "success" }); }, function() { // Fallback for older browsers var textArea = document.createElement("textarea"); textArea.value = self.publicUrl(); document.body.appendChild(textArea); textArea.select(); try { document.execCommand("copy"); new PNotify({ title: "Copied to Clipboard", text: "Public URL copied to clipboard", type: "success" }); } catch (err) { new PNotify({ title: "Copy Failed", text: "Failed to copy URL to clipboard", type: "error" }); } document.body.removeChild(textArea); }); } }; // Handle messages from the backend self.onDataUpdaterPluginMessage = function(plugin, data) { if (plugin !== "tailscale_funnel") { return; } if (data.type === "funnel_status_change") { self.funnelEnabled(data.enabled); self.funnelStatus(data.enabled ? "Enabled" : "Disabled"); if (data.enabled) { // Refresh to get the public URL self.refreshStatus(); } else { self.publicUrl("Not available"); } } }; } // Register the ViewModel OCTOPRINT_VIEWMODELS.push({ construct: TailscaleFunnelViewModel, dependencies: ["settingsViewModel"], elements: [] }); });