diff --git a/octoprint_tailscale_funnel/MANIFEST.in b/octoprint_tailscale_funnel/MANIFEST.in
index 5122ed1..e699ade 100644
--- a/octoprint_tailscale_funnel/MANIFEST.in
+++ b/octoprint_tailscale_funnel/MANIFEST.in
@@ -1,7 +1,6 @@
include README.md
include LICENSE
-include requirements.txt
include setup.py
-recursive-include static *
-recursive-include templates *
+recursive-include octoprint_tailscale_funnel/static *
+recursive-include octoprint_tailscale_funnel/templates *
recursive-include tests *
\ No newline at end of file
diff --git a/octoprint_tailscale_funnel/octoprint_tailscale_funnel/__init__.py b/octoprint_tailscale_funnel/octoprint_tailscale_funnel/__init__.py
index 69ff9f6..7901ee3 100644
--- a/octoprint_tailscale_funnel/octoprint_tailscale_funnel/__init__.py
+++ b/octoprint_tailscale_funnel/octoprint_tailscale_funnel/__init__.py
@@ -91,7 +91,7 @@ class TailscaleFunnelPlugin(octoprint.plugin.StartupPlugin,
def get_assets(self):
return dict(
- js=["js/tailscale_funnel.js"],
+ js=["js/tailscale_funnel.js", "js/tailscale_funnel_navbar.js"],
css=["css/tailscale_funnel.css"],
less=["less/tailscale_funnel.less"]
)
@@ -100,7 +100,8 @@ class TailscaleFunnelPlugin(octoprint.plugin.StartupPlugin,
def get_template_configs(self):
return [
- dict(type="settings", custom_bindings=True, template="tailscale_funnel_settings.jinja2")
+ dict(type="settings", custom_bindings=True, template="tailscale_funnel_settings.jinja2"),
+ dict(type="navbar", name="Funnel", custom_bindings=False, template="tailscale_funnel_navbar.jinja2")
]
##~~ BlueprintPlugin mixin
diff --git a/octoprint_tailscale_funnel/octoprint_tailscale_funnel/static/css/tailscale_funnel.css b/octoprint_tailscale_funnel/octoprint_tailscale_funnel/static/css/tailscale_funnel.css
index 0a42fe9..dc05e1e 100644
--- a/octoprint_tailscale_funnel/octoprint_tailscale_funnel/static/css/tailscale_funnel.css
+++ b/octoprint_tailscale_funnel/octoprint_tailscale_funnel/static/css/tailscale_funnel.css
@@ -38,4 +38,15 @@
background-color: #fcf8e3;
border-color: #faebcc;
color: #8a6d3b;
+}
+
+/* Navbar Button Farbkennung */
+#navbar_plugin_tailscale_funnel > a.tsf-enabled {
+ background-color: #5cb85c;
+ color: #fff;
+}
+
+#navbar_plugin_tailscale_funnel > a.tsf-disabled {
+ background-color: #777;
+ color: #fff;
}
\ No newline at end of file
diff --git a/octoprint_tailscale_funnel/octoprint_tailscale_funnel/static/js/tailscale_funnel_navbar.js b/octoprint_tailscale_funnel/octoprint_tailscale_funnel/static/js/tailscale_funnel_navbar.js
new file mode 100644
index 0000000..0d72706
--- /dev/null
+++ b/octoprint_tailscale_funnel/octoprint_tailscale_funnel/static/js/tailscale_funnel_navbar.js
@@ -0,0 +1,105 @@
+$(function() {
+ var lastEnabled = null;
+
+ function refreshNavbarStatus() {
+ var $status = $("#tsf_nav_status");
+ var $openLi = $("#tsf_nav_open_li");
+ var $open = $("#tsf_nav_open");
+ var $toggleText = $("#tsf_nav_toggle_text");
+ var $btn = $("#navbar_plugin_tailscale_funnel > a");
+ if ($status.length === 0) return;
+ $status.text("Checking...");
+ $.ajax({
+ url: PLUGIN_BASEURL + "tailscale_funnel/status",
+ type: "GET",
+ dataType: "json",
+ success: function(resp) {
+ if (resp && resp.status === "success") {
+ var enabled = !!(resp.data && resp.data.funnel_enabled);
+ var url = resp.data && resp.data.public_url ? resp.data.public_url : "";
+ $status.text(enabled ? "Enabled" : "Disabled");
+ $toggleText.text(enabled ? "Disable" : "Enable");
+ $btn.toggleClass('tsf-enabled', enabled).toggleClass('tsf-disabled', !enabled);
+ lastEnabled = enabled;
+ if (enabled && url) {
+ $open.attr("href", url);
+ $openLi.removeClass("hidden");
+ } else {
+ $open.attr("href", "#");
+ $openLi.addClass("hidden");
+ }
+ } else {
+ $status.text("Error");
+ $toggleText.text("Enable");
+ $btn.removeClass('tsf-enabled tsf-disabled');
+ $openLi.addClass("hidden");
+ }
+ },
+ error: function() {
+ $status.text("Error");
+ $toggleText.text("Enable");
+ $btn.removeClass('tsf-enabled tsf-disabled');
+ $openLi.addClass("hidden");
+ }
+ });
+ }
+
+ // Refresh when dropdown opens, and on click of Refresh
+ $(document).on('show.bs.dropdown', '#navbar_plugin_tailscale_funnel', refreshNavbarStatus);
+ $(document).on('click', '#tsf_nav_refresh', function(e) {
+ e.preventDefault();
+ refreshNavbarStatus();
+ });
+
+ $(document).on('click', '#tsf_nav_toggle', function(e) {
+ e.preventDefault();
+ var enable = !(lastEnabled === true);
+ // Optional simpler confirm: nur beim Aktivieren
+ if (enable) {
+ var c = window.confirm("Enabling Funnel will make your OctoPrint instance accessible from the public internet. Continue?");
+ if (!c) return;
+ }
+ var action = enable ? 'enable' : 'disable';
+ var $status = $("#tsf_nav_status");
+ var $toggleText = $("#tsf_nav_toggle_text");
+ var $btn = $("#navbar_plugin_tailscale_funnel > a");
+ $status.text(enable ? 'Enabling...' : 'Disabling...');
+ $.ajax({
+ url: PLUGIN_BASEURL + 'tailscale_funnel/' + action,
+ type: 'POST',
+ dataType: 'json',
+ success: function(resp) {
+ if (resp && resp.status === 'success') {
+ if (enable) {
+ $status.text('Enabled');
+ $toggleText.text('Disable');
+ $btn.addClass('tsf-enabled').removeClass('tsf-disabled');
+ var url = resp.data && resp.data.public_url ? resp.data.public_url : '';
+ if (url) {
+ $("#tsf_nav_open").attr('href', url);
+ $("#tsf_nav_open_li").removeClass('hidden');
+ }
+ lastEnabled = true;
+ } else {
+ $status.text('Disabled');
+ $toggleText.text('Enable');
+ $btn.addClass('tsf-disabled').removeClass('tsf-enabled');
+ $("#tsf_nav_open").attr('href', '#');
+ $("#tsf_nav_open_li").addClass('hidden');
+ lastEnabled = false;
+ }
+ // Finaler Abgleich mit Backendstatus
+ setTimeout(refreshNavbarStatus, 250);
+ } else {
+ $status.text('Error');
+ $btn.removeClass('tsf-enabled tsf-disabled');
+ }
+ },
+ error: function() {
+ $status.text('Error');
+ $btn.removeClass('tsf-enabled tsf-disabled');
+ }
+ });
+ });
+});
+
diff --git a/octoprint_tailscale_funnel/octoprint_tailscale_funnel/templates/tailscale_funnel_navbar.jinja2 b/octoprint_tailscale_funnel/octoprint_tailscale_funnel/templates/tailscale_funnel_navbar.jinja2
new file mode 100644
index 0000000..2ddbcca
--- /dev/null
+++ b/octoprint_tailscale_funnel/octoprint_tailscale_funnel/templates/tailscale_funnel_navbar.jinja2
@@ -0,0 +1,14 @@
+
+
+
+ Funnel
+
+
+
+
diff --git a/octoprint_tailscale_funnel/requirements.txt b/octoprint_tailscale_funnel/requirements.txt
index e90264b..eaf6289 100644
--- a/octoprint_tailscale_funnel/requirements.txt
+++ b/octoprint_tailscale_funnel/requirements.txt
@@ -1 +1,2 @@
+# Runtime dependency for installation via OctoPrint's Plugin Manager
OctoPrint>=1.3.0
\ No newline at end of file
diff --git a/octoprint_tailscale_funnel/setup.py b/octoprint_tailscale_funnel/setup.py
index 20f7801..81506c7 100644
--- a/octoprint_tailscale_funnel/setup.py
+++ b/octoprint_tailscale_funnel/setup.py
@@ -14,7 +14,7 @@ plugin_package = "octoprint_tailscale_funnel"
plugin_name = "OctoPrint-Tailscale-Funnel"
# The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module
-plugin_version = "0.1.6.2"
+plugin_version = "0.1.6.3"
# The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin
# module
@@ -88,7 +88,18 @@ setup_parameters = octoprint_setuptools.create_plugin_setup_parameters(
)
if len(additional_setup_parameters):
- from octoprint.util import dict_merge
+ try:
+ from octoprint.util import dict_merge
+ except Exception:
+ # Fallback to allow building without the octoprint package installed
+ def dict_merge(a, b):
+ result = dict(a)
+ for k, v in b.items():
+ if k in result and isinstance(result[k], dict) and isinstance(v, dict):
+ result[k] = dict_merge(result[k], v)
+ else:
+ result[k] = v
+ return result
setup_parameters = dict_merge(setup_parameters, additional_setup_parameters)
setup(**setup_parameters)
\ No newline at end of file
diff --git a/octoprint_tailscale_funnel/update.json b/octoprint_tailscale_funnel/update.json
index 64643f2..5cfab55 100644
--- a/octoprint_tailscale_funnel/update.json
+++ b/octoprint_tailscale_funnel/update.json
@@ -1,4 +1,4 @@
{
- "version": "0.1.6.2"
+ "version": "0.1.6.3"
}
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..fd6253c
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,7 @@
+setuptools>=68
+wheel>=0.41
+build>=1.0
+packaging>=23
+# Kompatible verfügbare Version laut Index
+octoprint_setuptools==1.0.3
+
diff --git a/scripts/build_plugin.sh b/scripts/build_plugin.sh
new file mode 100755
index 0000000..7d1e90f
--- /dev/null
+++ b/scripts/build_plugin.sh
@@ -0,0 +1,147 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Usage: scripts/build_plugin.sh [VERSION]
+# - Läuft im Projekt-.venv
+# - Optional: VERSION (z.B. 0.1.6.4). Wenn gesetzt, wird setup.py gepatcht.
+# - Baut wheel + sdist und erstellt zusätzlich ein "normales" ZIP der sdist-Struktur.
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
+PLUGIN_DIR="$ROOT_DIR/octoprint_tailscale_funnel"
+DIST_DIR="$PLUGIN_DIR/dist"
+VENV_DIR="$ROOT_DIR/.venv"
+
+VERSION_INPUT="${1:-}"
+
+echo "Project root: $ROOT_DIR"
+echo "Plugin dir: $PLUGIN_DIR"
+echo "Venv dir: $VENV_DIR"
+
+# Ensure venv
+if [[ ! -x "$VENV_DIR/bin/python" ]]; then
+ echo "Creating venv at $VENV_DIR"
+ python3 -m venv "$VENV_DIR"
+fi
+
+if [[ -f "$ROOT_DIR/requirements.txt" ]]; then
+ "$VENV_DIR/bin/pip" install -q --upgrade pip
+ "$VENV_DIR/bin/pip" install -q -r "$ROOT_DIR/requirements.txt"
+else
+ "$VENV_DIR/bin/pip" install -q --upgrade pip setuptools wheel build packaging octoprint_setuptools
+fi
+
+# Optional: bump version
+if [[ -n "$VERSION_INPUT" ]]; then
+ echo "Setting version to $VERSION_INPUT"
+ "$VENV_DIR/bin/python" - "$VERSION_INPUT" "$PLUGIN_DIR/setup.py" <<'PY'
+import sys, re, pathlib
+ver = sys.argv[1]
+setup_path = pathlib.Path(sys.argv[2])
+text = setup_path.read_text()
+new_text = re.sub(r'^(plugin_version\s*=\s*")([^"]*)(")', lambda m: m.group(1) + ver + m.group(3), text, flags=re.M)
+setup_path.write_text(new_text)
+print("Updated version to", ver)
+PY
+fi
+
+# Clean dist
+mkdir -p "$DIST_DIR"
+rm -f "$DIST_DIR"/*
+
+# Build wheel + sdist
+echo "Building (wheel + sdist)..."
+"$VENV_DIR/bin/python" -m build --no-isolation "$PLUGIN_DIR"
+
+# Create additional plain ZIP from sdist content (flat source zip)
+SDIST_TGZ=$(ls -1 "$DIST_DIR"/*.tar.gz | tail -n1 || true)
+if [[ -n "$SDIST_TGZ" ]]; then
+ echo "Creating plain ZIP from sdist: $SDIST_TGZ"
+ TMP_DIR="$(mktemp -d)"
+ tar -xzf "$SDIST_TGZ" -C "$TMP_DIR"
+ SRC_DIR="$(find "$TMP_DIR" -maxdepth 1 -type d -name 'octoprint_tailscale_funnel-*' | head -n1)"
+ if [[ -n "$SRC_DIR" ]]; then
+ ZIP_NAME="$(basename "$SRC_DIR").zip"
+ (cd "$SRC_DIR" && zip -rq "$DIST_DIR/$ZIP_NAME" .)
+ echo "Created: $DIST_DIR/$ZIP_NAME"
+ else
+ echo "WARN: Could not find extracted sdist directory to zip"
+ fi
+ rm -rf "$TMP_DIR"
+else
+ echo "WARN: No sdist .tar.gz found, skipping plain ZIP creation"
+fi
+
+echo "Artifacts in $DIST_DIR:"
+ls -lah "$DIST_DIR"
+
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Usage: scripts/build_plugin.sh [VERSION]
+# - Läuft im Projekt-.venv
+# - Optional: VERSION (z.B. 0.1.6.4). Wenn gesetzt, wird setup.py gepatcht.
+# - Baut wheel + sdist und erstellt zusätzlich ein "normales" ZIP der sdist-Struktur.
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
+PLUGIN_DIR="$ROOT_DIR/octoprint_tailscale_funnel"
+DIST_DIR="$PLUGIN_DIR/dist"
+VENV_DIR="$ROOT_DIR/.venv"
+
+VERSION_INPUT="${1:-}"
+
+echo "Project root: $ROOT_DIR"
+echo "Plugin dir: $PLUGIN_DIR"
+echo "Venv dir: $VENV_DIR"
+
+# Ensure venv
+if [[ ! -x "$VENV_DIR/bin/python" ]]; then
+ echo "Creating venv at $VENV_DIR"
+ python3 -m venv "$VENV_DIR"
+fi
+
+"$VENV_DIR/bin/pip" install -q --upgrade pip setuptools wheel build packaging octoprint_setuptools
+
+# Optional: bump version
+if [[ -n "$VERSION_INPUT" ]]; then
+ echo "Setting version to $VERSION_INPUT"
+ "$VENV_DIR/bin/python" - "$VERSION_INPUT" "$PLUGIN_DIR/setup.py" <<'PY'
+import sys, re, pathlib
+ver = sys.argv[1]
+setup_path = pathlib.Path(sys.argv[2])
+text = setup_path.read_text()
+new_text = re.sub(r'^(plugin_version\s*=\s*")([^"]*)(")', lambda m: m.group(1) + ver + m.group(3), text, flags=re.M)
+setup_path.write_text(new_text)
+print("Updated version to", ver)
+PY
+fi
+
+# Clean dist
+mkdir -p "$DIST_DIR"
+rm -f "$DIST_DIR"/*
+
+# Build wheel + sdist
+echo "Building (wheel + sdist)..."
+"$VENV_DIR/bin/python" -m build --no-isolation "$PLUGIN_DIR"
+
+# Create additional plain ZIP from sdist content (flat source zip)
+SDIST_TGZ=$(ls -1 "$DIST_DIR"/*.tar.gz | tail -n1 || true)
+if [[ -n "$SDIST_TGZ" ]]; then
+ echo "Creating plain ZIP from sdist: $SDIST_TGZ"
+ TMP_DIR="$(mktemp -d)"
+ tar -xzf "$SDIST_TGZ" -C "$TMP_DIR"
+ SRC_DIR="$(find "$TMP_DIR" -maxdepth 1 -type d -name 'octoprint_tailscale_funnel-*' | head -n1)"
+ if [[ -n "$SRC_DIR" ]]; then
+ ZIP_NAME="$(basename "$SRC_DIR").zip"
+ (cd "$SRC_DIR" && zip -rq "$DIST_DIR/$ZIP_NAME" .)
+ echo "Created: $DIST_DIR/$ZIP_NAME"
+ else
+ echo "WARN: Could not find extracted sdist directory to zip"
+ fi
+ rm -rf "$TMP_DIR"
+else
+ echo "WARN: No sdist .tar.gz found, skipping plain ZIP creation"
+fi
+
+echo "Artifacts in $DIST_DIR:"
+ls -lah "$DIST_DIR"
+