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 @@ + + 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" +