Compare commits

..

24 Commits

Author SHA1 Message Date
44383fabec fix(docker): gcc + python3-dev für pycryptodome arm/v7 Kompilierung
All checks were successful
Nightly Build / build (push) Successful in 12m26s
2026-06-30 14:30:10 +02:00
48bec55611 feat(ci): linux/arm/v7 Platform zu Docker-Build hinzugefügt (Raspberry Pi 2/3 32-bit)
Some checks failed
Nightly Build / build (push) Failing after 4m53s
2026-06-30 12:21:20 +02:00
ab44e234be fix(ui): AMS-Spool-Dropdown bleibt offen während Poll-Tick (kein innerHTML-Reset bei fokussiertem Select)
Some checks failed
Nightly Build / build (push) Has been cancelled
2026-06-30 12:17:03 +02:00
74fc2ddab0 feat: color picker, unified UI styling, filament mismatch detection, Spoolman slot assignment
All checks were successful
Nightly Build / build (push) Successful in 4m27s
- Slot color editor: Pickr HSV color picker (offline, served from lib/),
  recent swatches (up to 16, localStorage), copy color from other slot
- Unified axes control panel: XY+Z merged, shared step size + custom mm input
- Language selector moved from header to Settings → Appearance
- Filament mismatch detection blocks Upload-and-Print on material mismatch,
  slot mapper opens automatically
- Spoolman spool-per-slot assignment in AMS status tab and Filaments settings
- Fix: Spoolman sync rate label — 0=end of print, not disabled (Issue #76)
- Fix: lib/ assets served by bridge static handler for offline use
- UI: global unified select + input styling, set-row labels match modal-field
2026-06-30 11:13:34 +02:00
771599be0c Merge pull request 'fix: isolate filament profiles per printer in multi-printer bridge' (#75) from walterioo/KX-Bridge-Release:fix/per-printer-filament-profiles into nightly
All checks were successful
Nightly Build / build (push) Successful in 4m21s
2026-06-30 10:21:56 +02:00
0e1d46ee7f fix: isolate filament profiles per printer in multi-printer bridge (#74)
Some checks are pending
PR Check / lint-and-test (pull_request) Blocked by required conditions
Per-printer [filament_profiles_<id>] sections so configuring one printer no
longer overwrites another (read-fallback to the legacy global section keeps
single-printer setups unchanged). Dropdown/switch links now navigate to each
printer's own bridge_url. Adds pytest coverage and a CHANGELOG entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 07:13:10 +02:00
15e28244af fix(ci): jq static binary installieren falls nicht vorhanden
All checks were successful
Nightly Build / build (push) Successful in 4m12s
2026-06-29 09:57:46 +02:00
c1a3b9238d fix(ci): jq statt python3 für JSON-Serialisierung (python3 not found im Runner)
Some checks failed
Nightly Build / build (push) Failing after 4m10s
2026-06-29 07:03:09 +02:00
aad3833301 fix(ci): SIGPIPE bei head -1 in Pipelines beheben — read+drain statt head
Some checks failed
Nightly Build / build (push) Failing after 4m5s
2026-06-29 06:58:13 +02:00
a2f658e701 fix(ci): Python-Skript ausgelagert — kein Inline-Code in YAML mehr
Some checks failed
Nightly Build / build (push) Failing after 16s
2026-06-29 06:57:00 +02:00
ab2cf6e4ed fix(ci): Heredoc aus YAML entfernen — python3 -c statt <<PYEOF 2026-06-29 06:55:31 +02:00
1b05362c2b fix(ci): JSON-Serialisierung via python3 statt awk-Pipeline (SIGPIPE/exit-141-Fix) 2026-06-28 23:24:49 +02:00
cfe70430d3 feat: unified axes panel, language selector cleanup, filament mismatch detection, Spoolman slot assignment
Some checks failed
Nightly Build / build (push) Failing after 5m11s
- Merged XY + Z axis cards into single Axes Control panel with shared step
  size row and free-text mm input; Home XY/Z placed below respective pads;
  Home All + Motors Off centred at bottom
- Language selector removed from header bar, now only in Settings → Appearance
- Upload-and-Print blocked on GCode/AMS material mismatch; slot mapper dialog
  opens automatically with mismatch warning highlighting affected slots
- Spoolman spool-per-slot assignment: dropdown in AMS status tab per kachel,
  dedicated card in Filaments settings tab with save button
- nightly.yml: NIGHTLY_CHANGELOG.md takes precedence over auto-generated
  commit log when present; CI uses it as release body verbatim
2026-06-28 23:17:58 +02:00
2c8a62f130 fix(ci): Changelog nur feat/fix-Commits seit letztem Stable-Tag
Some checks failed
Nightly Build / build (push) Failing after 5m3s
2026-06-28 21:59:38 +02:00
e4b0716330 fix(ci): --tags beim fetch, kein --depth=1 (Tags für Versionsberechnung nötig)
All checks were successful
Nightly Build / build (push) Successful in 4m40s
2026-06-28 19:16:31 +02:00
d9fcc15c53 fix(ci): kein VERSION-Commit im CI (verhindert Push-Loop + Konflikt)
Some checks failed
Nightly Build / build (push) Failing after 2s
2026-06-28 19:16:11 +02:00
31dcf4c8fd ci: nightly.yml in paths-Filter aufnehmen (Workflow-Änderungen triggern Build)
Some checks failed
Nightly Build / build (push) Failing after 2s
2026-06-28 19:14:34 +02:00
319a8d5ccb fix(ci): Nightly-Version automatisch aus letztem Stable-Tag berechnen
Keine manuelle VERSION-Pflege mehr für Nightlies. CI ermittelt:
- letzten Stable-Tag (z.B. v0.9.27)
- nächste Patch-Version (0.9.28)
- laufenden Nightly-Counter aus vorhandenen Tags (nightly-0.9.28-nightlyN)

VERSION-Datei im Repo bleibt auf dem letzten Stable, wird vom CI
für jeden Nightly-Build überschrieben und committet.
2026-06-28 19:09:08 +02:00
700459085b fix(ci): Nightly-Changelog dynamisch aus Git-Log generieren (Delta seit letztem Tag)
CHANGES.md war statisch und zeigte immer denselben alten Stand.
Jetzt: git log <prev-tag>..HEAD, chore-Nightly-Commits rausgefiltert,
englisch, nur echte feat/fix/docs-Commits.
2026-06-28 19:05:20 +02:00
f93c07a971 chore: nightly 0.9.27-nightly10 2026-06-28 18:55:22 +02:00
81906cfffc fix(ui): Spoolman-Section im Filaments-Tab nach asynchronem Status-Load anzeigen
All checks were successful
Nightly Build / build (push) Successful in 4m8s
_loadSpoolmanStatus() rief _buildSpoolmanSection() nicht auf — Section blieb
versteckt weil configured-Flag beim Tab-Öffnen noch false war.
2026-06-28 18:54:30 +02:00
3f915b058b chore: releases/ komplett aus Git entfernen (Binaries gehören nicht ins Repo) 2026-06-28 17:54:53 +02:00
8b66172ca1 chore: nightly auf v0.9.27-Stand bringen 2026-06-28 17:54:35 +02:00
cec7cb2a5a chore: nightly auf master-Stand bringen (Ein-Repo)
All checks were successful
Nightly Build / build (push) Successful in 3m58s
2026-06-28 16:51:40 +02:00
38 changed files with 707 additions and 495 deletions

View File

@@ -0,0 +1,12 @@
#!/usr/bin/env python3
import sys, json
tag, version, body_file = sys.argv[1], sys.argv[2], sys.argv[3]
body = open(body_file).read()
payload = json.dumps({
"tag_name": tag,
"name": "KX-Bridge " + version + " Nightly",
"body": body,
"draft": False,
"prerelease": True
})
open("/tmp/release_body.json", "w").write(payload)

View File

@@ -10,6 +10,7 @@ on:
- 'requirements.txt'
- 'web/**'
- 'data/**'
- '.gitea/workflows/nightly.yml'
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
@@ -21,11 +22,11 @@ jobs:
- name: Checkout
run: |
if [ -d .git ]; then
git fetch origin nightly
git fetch --tags origin nightly
git reset --hard origin/nightly
git clean -fd
else
git clone --depth=1 --branch nightly https://gitea.it-drui.de/viewit/KX-Bridge-Release.git .
git clone --branch nightly https://gitea.it-drui.de/viewit/KX-Bridge-Release.git .
fi
- name: Install Docker CLI
@@ -64,11 +65,36 @@ jobs:
echo "${{ secrets.REGISTRY_TOKEN }}" | \
docker login gitea.it-drui.de -u "${{ secrets.REGISTRY_USER }}" --password-stdin
- name: Compute nightly version
run: |
# Letzten Stable-Tag ermitteln (v0.9.27 → minor=27)
LAST_STABLE=$(git tag --list 'v*' --sort=-version:refname \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+' | { read -r line; echo "$line"; cat >/dev/null; } || true)
if [ -z "$LAST_STABLE" ]; then
echo "ERROR: kein Stable-Tag gefunden" >&2; exit 1
fi
# Nächste Minor-Version: v0.9.27 → 0.9.28
MAJOR=$(echo "$LAST_STABLE" | sed 's/^v//' | cut -d. -f1)
MINOR=$(echo "$LAST_STABLE" | sed 's/^v//' | cut -d. -f2)
PATCH=$(echo "$LAST_STABLE" | sed 's/^v//' | cut -d. -f3)
NEXT_PATCH=$((PATCH + 1))
BASE="${MAJOR}.${MINOR}.${NEXT_PATCH}"
# Laufende Nummer: Anzahl vorhandener nightly-<BASE>-nightlyX Tags + 1
COUNT=$(git tag --list "nightly-${BASE}-nightly*" | wc -l | tr -d ' ')
N=$((COUNT + 1))
VERSION="${BASE}-nightly${N}"
echo "VERSION=${VERSION}" > /tmp/nightly_version.env
echo "BASE=${BASE}" >> /tmp/nightly_version.env
echo "LAST_STABLE=${LAST_STABLE}" >> /tmp/nightly_version.env
echo "Computed nightly version: ${VERSION} (after ${LAST_STABLE})"
- name: Build & push (amd64 + arm64)
run: |
VERSION=$(cat VERSION)
. /tmp/nightly_version.env
# VERSION-Datei im Arbeitsverzeichnis für den Docker-Build setzen (kein Commit)
echo "$VERSION" > VERSION
docker buildx build \
--platform linux/amd64,linux/arm64 \
--platform linux/amd64,linux/arm64,linux/arm/v7 \
--push \
--provenance=false \
--no-cache \
@@ -80,50 +106,68 @@ jobs:
env:
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
VERSION=$(cat VERSION)
. /tmp/nightly_version.env
TAG="nightly-${VERSION}"
git config user.name "gitea-actions"
git config user.email "actions@it-drui.de"
# Changelog aus CHANGES.md lesen (wird von release.sh aus dem Dev-Repo generiert)
# Letzten Stable-Tag als Changelog-Basis (nur echte vX.Y.Z-Tags)
PREV_TAG=$(git tag --list 'v*' --sort=-version:refname \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
| { read -r line; echo "$line"; cat >/dev/null; } || true)
[ -z "$PREV_TAG" ] && PREV_TAG=$(git rev-list --max-parents=0 HEAD)
# Changelog: NIGHTLY_CHANGELOG.md hat Vorrang (manuell gepflegt),
# sonst auto-generiert aus feat/fix-Commits seit letztem Stable-Tag
BODY_FILE=$(mktemp)
if [ -f CHANGES.md ]; then
cat CHANGES.md > "$BODY_FILE"
printf '## KX-Bridge %s — Nightly Build\n\n' "$VERSION" > "$BODY_FILE"
printf '[experimental] Untested features, for testers only.\n\n' >> "$BODY_FILE"
if [ -s NIGHTLY_CHANGELOG.md ]; then
cat NIGHTLY_CHANGELOG.md >> "$BODY_FILE"
else
# Fallback falls CHANGES.md fehlt
printf '## KX-Bridge %s -- Nightly Build\n\n' "$VERSION" > "$BODY_FILE"
printf '[experimentell] Ungetestete Features, nur fuer Tester geeignet.\n\n' >> "$BODY_FILE"
printf '- Automatischer Nightly-Build\n\n---\n\n' >> "$BODY_FILE"
printf '### Docker-Image aktualisieren\n\n```bash\ndocker compose pull && docker compose up -d\n```\n\n' >> "$BODY_FILE"
printf 'Image-Tag: `gitea.it-drui.de/viewit/kx-bridge:nightly`\n' >> "$BODY_FILE"
fi
# Tag setzen
git tag -f "$TAG"
git push https://gitea-actions:${GITEA_TOKEN}@gitea.it-drui.de/viewit/KX-Bridge-Release.git "$TAG" --force
# curl installieren (BusyBox wget kann kein DELETE/POST mit Headers)
if ! command -v curl >/dev/null 2>&1; then
if ! apk add --no-cache curl 2>/dev/null; then
wget -qO /usr/local/bin/curl \
"https://github.com/moparisthebest/static-curl/releases/download/v8.6.0/curl-amd64"
chmod +x /usr/local/bin/curl
printf '### Changes since `%s`\n\n' "$PREV_TAG" >> "$BODY_FILE"
git log "${PREV_TAG}..HEAD" --pretty=format:'%s' --no-merges \
| grep -E '^(feat|fix)[:(]' \
| grep -Ev '^(feat|fix)\((ci|release|build|workflow)\)' \
| sed 's/^/- /' \
>> "$BODY_FILE" || true
if ! grep -q '^- ' "$BODY_FILE"; then
printf '- No user-facing changes in this build\n' >> "$BODY_FILE"
fi
fi
printf '\n\n---\n\n### Update Docker image\n\n```bash\ndocker compose pull && docker compose up -d\n```\n\n' >> "$BODY_FILE"
printf 'Image tag: `gitea.it-drui.de/viewit/kx-bridge:nightly`\n' >> "$BODY_FILE"
# Altes Release loeschen falls vorhanden
# Tag setzen
git tag "$TAG"
git push https://gitea-actions:${GITEA_TOKEN}@gitea.it-drui.de/viewit/KX-Bridge-Release.git "$TAG"
# curl + jq installieren falls nötig
if ! command -v curl >/dev/null 2>&1; then
apk add --no-cache curl 2>/dev/null || \
{ wget -qO /usr/local/bin/curl \
"https://github.com/moparisthebest/static-curl/releases/download/v8.6.0/curl-amd64" \
&& chmod +x /usr/local/bin/curl; }
fi
if ! command -v jq >/dev/null 2>&1; then
ARCH=$(uname -m)
JQ_ARCH="amd64"; [ "$ARCH" = "aarch64" ] && JQ_ARCH="arm64"
wget -qO /usr/local/bin/jq \
"https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-${JQ_ARCH}"
chmod +x /usr/local/bin/jq
fi
# Altes Release löschen falls vorhanden
curl -s -X DELETE \
-H "Authorization: token ${GITEA_TOKEN}" \
"https://gitea.it-drui.de/api/v1/repos/viewit/KX-Bridge-Release/releases/tags/${TAG}" \
2>/dev/null || true
# Release erstellen (JSON-Body via awk escapen)
BODY_JSON=$(awk '{
gsub(/\\/, "\\\\"); gsub(/"/, "\\\""); gsub(/\t/, "\\t");
printf "%s\\n", $0
}' "$BODY_FILE" | awk 'BEGIN{printf "\""} {printf "%s", $0} END{printf "\""}')
JSON_PAYLOAD="{\"tag_name\":\"${TAG}\",\"name\":\"KX-Bridge ${VERSION} Nightly\",\"body\":${BODY_JSON},\"draft\":false,\"prerelease\":true}"
printf '%s' "$JSON_PAYLOAD" > /tmp/release_body.json
# Release erstellen JSON sicher via jq bauen
jq -n \
--arg tag "$TAG" \
--arg name "KX-Bridge ${VERSION} Nightly" \
--rawfile body "$BODY_FILE" \
'{"tag_name":$tag,"name":$name,"body":$body,"draft":false,"prerelease":true}' \
> /tmp/release_body.json
curl -s -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \

View File

@@ -61,7 +61,7 @@ jobs:
run: |
VERSION="${GITHUB_REF#refs/tags/v}"
docker buildx build \
--platform linux/amd64,linux/arm64 \
--platform linux/amd64,linux/arm64,linux/arm/v7 \
--push \
--provenance=false \
--no-cache \

4
.gitignore vendored
View File

@@ -4,9 +4,7 @@ __pycache__/
build/
dist/
*.spec
releases/*/kx-bridge
releases/*/extract_credentials
releases/*/extract_credentials.exe
releases/
!kx-bridge.spec

View File

@@ -1,5 +1,21 @@
# Changelog
## [Unreleased]
### Fixed
- **Filament profiles not isolated between printers in a multi-printer bridge**
(issue #74). The slot→profile mapping and `visible_vendors` were stored in a
single global `[filament_profiles]` section, so configuring one printer
overwrote the other and after a restart both loaded the same mapping. Each
printer now persists to its own `[filament_profiles_<id>]` section, with a
read-fallback to the legacy global section (single-printer setups unchanged).
- **Printer dropdown showed the other printer's filament profiles** (issue #74).
The header dropdown and the printers-management "switch" link navigated within
the same port (`/printerN`), so viewing another printer pulled its profile
names cross-instance from the local origin. The links now point at each
printer's own `bridge_url`, so every printer is viewed same-origin on its own
port.
## [0.9.26] 2026-06-21
### New

View File

@@ -1,33 +0,0 @@
## KX-Bridge 0.9.27-nightly9 — Nightly Build
[experimentell] Ungetestete Features, nur für Tester geeignet.
### Änderungen seit `v0.9.26`
- fix(spoolman): Status-Dot beim Seitenload initialisieren
- chore: CHANGES.md mit echtem Changelog aus Dev-Repo ins Release-Repo schreiben
- fix(spoolman): SPOOLMAN_* env-Cache bei Restart leeren, Status-Dot nach Save aktualisieren
- chore: README.es.md + CONTRIBUTING.md in release.sh-Sync aufnehmen
- docs: Nightly-Sektion, Wartungshinweis + CONTRIBUTING.md, FR/IT-Sprachen, Downloads-Badge
- fix(update): Nightly-Vergleich auf Versions-String umstellen (statt Datum)
- feat(update): Nightly-Track vom Stable-Track trennen
- fix(release): nightly immer auf nightly-Branch pushen, kein master-Push
- fix(camera): exponentielles Backoff bei ffmpeg-Fehler + /api/camera/reset + ↺-Button
- fix(release): Nightly-Release vom nightly-Branch erlauben
- feat(i18n): fehlende UI-Übersetzungen ergänzt + Keys alphabetisch sortiert (PR #70 @fenopy)
- fix: config/config.ini.example beim Release-Sync mitübertragen (Issue #72)
- feat(ui): Integrationen-Tab in Settings (Spoolman + Obico-Hinweis)
- feat(stack): KobraX Full Stack Compose für Portainer (KX-Bridge + Obico + Spoolman)
- feat(release): Nightly/Stable Release-Workflow mit eigenem Docker-Tag
- feat(spoolman): optionale Spoolman-Filamentverbrauch-Integration (PR #65, @p2l)
- fix(release): Artifact-Download per HTTP statt lokalem Dateipfad
---
### Docker-Image aktualisieren
```bash
docker compose pull && docker compose up -d
```
Image-Tag: `gitea.it-drui.de/viewit/kx-bridge:nightly`

View File

@@ -2,10 +2,11 @@ FROM python:3.11-slim-bookworm
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg gcc python3-dev && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir -r requirements.txt && \
apt-get purge -y gcc python3-dev && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
COPY kobrax_moonraker_bridge.py .
COPY web/ ./web/

11
NIGHTLY_CHANGELOG.md Normal file
View File

@@ -0,0 +1,11 @@
## Changes in this build
- Unified axes control panel: XY and Z merged into one card, shared step size selector (0.1 / 1 / 5 / 10 mm) plus custom mm input field, Home XY/Z buttons placed directly below their respective pads
- Language selector moved from header bar to Settings → Appearance
- Filament mismatch detection: Upload-and-Print is intercepted when GCode material differs from the loaded AMS slot — slot mapper dialog opens automatically to correct the assignment before printing
- Spoolman: assign a spool per AMS slot directly in the AMS status tab (dropdown per slot tile) and in the Filaments settings tab (dedicated assignment card)
- Fix: filament profiles now isolated per printer in multi-printer setups — configuring one printer no longer overwrites the other (PR #75 by @walterioo)
- Fix: printer dropdown and switch link now navigate to each printer's own bridge URL (same-origin, no cross-instance profile bleed)
- Fix: Spoolman sync rate label corrected — 0 means sync at end of print, not disabled (Issue #76)
- Slot color editor: Pickr color picker (HSV wheel + hex input), recent color swatches (up to 16, saved in browser), and "Copy color from slot" dropdown for identical backup spool setup (Issue #73)
- UI: unified dropdown and input field styling across all settings panels

View File

@@ -7,6 +7,7 @@ import os
import sys
import pathlib
import configparser
from typing import Optional
_BASE = pathlib.Path(sys.executable).parent if getattr(sys, "frozen", False) else pathlib.Path(__file__).parent
@@ -182,9 +183,27 @@ def list_printers() -> list[dict]:
return printers
def list_filament_profiles() -> dict[int, dict]:
def _filament_section(printer_id: Optional[str] = None) -> str:
"""Section name holding a printer's filament-profile mapping.
Multi-printer (one bridge, N printers): each printer keeps its own
``[filament_profiles_<id>]`` section so the mappings cannot overwrite each
other. ``printer_id is None`` (single-printer / legacy callers) maps to the
original global ``[filament_profiles]`` section — full backward compatibility.
"""
pid = str(printer_id).strip() if printer_id is not None else ""
if pid and pid != "0":
return f"filament_profiles_{pid}"
return "filament_profiles"
def list_filament_profiles(printer_id: Optional[str] = None) -> dict[int, dict]:
"""Liest die [filament_profiles]-Sektion aus config.ini.
With ``printer_id`` set, reads the per-printer ``[filament_profiles_<id>]``
section and falls back to the legacy global ``[filament_profiles]`` while
that printer has no own section yet.
Format pro AMS-Slot — primärer Selector ist (vendor, name), die `id` wird
aus der orca_filaments.json beim Speichern nachgeschlagen und mitgeführt
(als Hint für OrcaSlicer; das Orca-Datenmodell hat ~136 Profile mit
@@ -208,10 +227,13 @@ def list_filament_profiles() -> dict[int, dict]:
return {}
cfg = configparser.ConfigParser()
cfg.read(path, encoding="utf-8")
if not cfg.has_section("filament_profiles"):
section = _filament_section(printer_id)
if not cfg.has_section(section):
section = "filament_profiles" # fallback: legacy global section
if not cfg.has_section(section):
return {}
result: dict[int, dict] = {}
for key, value in cfg.items("filament_profiles"):
for key, value in cfg.items(section):
# Erwartet: slot_<idx>_id oder slot_<idx>_vendor oder slot_<idx>_name
if not key.startswith("slot_"):
continue
@@ -231,74 +253,97 @@ def list_filament_profiles() -> dict[int, dict]:
return result
def save_filament_profiles(profiles: dict[int, dict]) -> bool:
def save_filament_profiles(profiles: dict[int, dict], printer_id: Optional[str] = None) -> bool:
"""Schreibt die übergebenen Slot-Profile in die [filament_profiles]-
Sektion der config.ini. Existierende Einträge werden komplett ersetzt.
profiles: {slot_index: {"id": "OGFL01", "vendor": "Polymaker", "name": "PolyTerra PLA"}}
Mindestens vendor+name müssen gesetzt sein; id ist optional (Hint).
With ``printer_id`` set, writes the per-printer ``[filament_profiles_<id>]``
section only — other printers and the legacy global section are untouched.
"""
path = _find_config_file()
if not path:
return False
cfg = configparser.ConfigParser()
cfg.read(path, encoding="utf-8")
section = _filament_section(printer_id)
# visible_vendors (Issue #41) ist kein Slot-Mapping — beim Ersetzen der
# Sektion erhalten, sonst geht der Vendor-Filter beim Slot-Save verloren.
# First save of a per-printer section inherits the legacy global filter.
preserved_vendors = None
if cfg.has_option("filament_profiles", "visible_vendors"):
if cfg.has_option(section, "visible_vendors"):
preserved_vendors = cfg.get(section, "visible_vendors")
elif cfg.has_option("filament_profiles", "visible_vendors"):
preserved_vendors = cfg.get("filament_profiles", "visible_vendors")
if cfg.has_section("filament_profiles"):
cfg.remove_section("filament_profiles")
if cfg.has_section(section):
cfg.remove_section(section)
if profiles or preserved_vendors:
cfg["filament_profiles"] = {}
cfg[section] = {}
if preserved_vendors:
cfg["filament_profiles"]["visible_vendors"] = preserved_vendors
cfg[section]["visible_vendors"] = preserved_vendors
for slot_idx in sorted(profiles.keys()):
entry = profiles[slot_idx] or {}
if entry.get("vendor"):
cfg["filament_profiles"][f"slot_{slot_idx}_vendor"] = entry["vendor"]
cfg[section][f"slot_{slot_idx}_vendor"] = entry["vendor"]
if entry.get("name"):
cfg["filament_profiles"][f"slot_{slot_idx}_name"] = entry["name"]
cfg[section][f"slot_{slot_idx}_name"] = entry["name"]
if entry.get("id"):
cfg["filament_profiles"][f"slot_{slot_idx}_id"] = entry["id"]
cfg[section][f"slot_{slot_idx}_id"] = entry["id"]
with open(path, "w", encoding="utf-8") as f:
cfg.write(f)
return True
def list_visible_vendors() -> list[str]:
def list_visible_vendors(printer_id: Optional[str] = None) -> list[str]:
"""Liest [filament_profiles] visible_vendors (komma-separiert) aus config.ini.
Vendor-Sichtbarkeitsfilter für das Slot-Profil-Dropdown (Issue #41 Option A).
Leere Liste = keine Einschränkung (rückwärtskompatibel: alle Vendoren).
With ``printer_id`` set, reads the per-printer section and falls back to the
legacy global ``[filament_profiles]`` filter.
"""
path = _find_config_file()
if not path:
return []
cfg = configparser.ConfigParser()
cfg.read(path, encoding="utf-8")
if not cfg.has_option("filament_profiles", "visible_vendors"):
section = _filament_section(printer_id)
if not cfg.has_option(section, "visible_vendors"):
section = "filament_profiles" # fallback: legacy global section
if not cfg.has_option(section, "visible_vendors"):
return []
raw = cfg.get("filament_profiles", "visible_vendors")
raw = cfg.get(section, "visible_vendors")
return [v.strip() for v in raw.split(",") if v.strip()]
def save_visible_vendors(vendors: list[str]) -> bool:
def save_visible_vendors(vendors: list[str], printer_id: Optional[str] = None) -> bool:
"""Schreibt visible_vendors in [filament_profiles], ohne die Slot-Mappings
(slot_N_*) zu verlieren. Leere Liste entfernt den Key wieder."""
(slot_N_*) zu verlieren. Leere Liste entfernt den Key wieder.
With ``printer_id`` set, writes the per-printer section. When that section is
created here for the first time, the slot mappings are seeded from the legacy
global section so they are not orphaned by the read-fallback in
``list_filament_profiles``."""
path = _find_config_file()
if not path:
return False
cfg = configparser.ConfigParser()
cfg.read(path, encoding="utf-8")
if not cfg.has_section("filament_profiles"):
cfg.add_section("filament_profiles")
section = _filament_section(printer_id)
if not cfg.has_section(section):
cfg.add_section(section)
if section != "filament_profiles" and cfg.has_section("filament_profiles"):
for key, value in cfg.items("filament_profiles"):
if key.startswith("slot_"):
cfg[section][key] = value
clean = [v.strip() for v in (vendors or []) if v and v.strip()]
if clean:
cfg["filament_profiles"]["visible_vendors"] = ", ".join(clean)
elif cfg.has_option("filament_profiles", "visible_vendors"):
cfg.remove_option("filament_profiles", "visible_vendors")
cfg[section]["visible_vendors"] = ", ".join(clean)
elif cfg.has_option(section, "visible_vendors"):
cfg.remove_option(section, "visible_vendors")
with open(path, "w", encoding="utf-8") as f:
cfg.write(f)
return True

View File

@@ -142,6 +142,11 @@ _KX_UI_ASSETS: dict[str, str] = {
"style.css": "text/css",
"app.js": "application/javascript",
}
# Dateien aus lib/ werden anhand der Extension ausgeliefert (kein Whitelist-Eintrag nötig)
_KX_UI_LIB_TYPES: dict[str, str] = {
".js": "application/javascript",
".css": "text/css",
}
_KX_UI_TRANSLATION_RE = re.compile(r"^translations/([a-z]{2}(?:-[a-z]{2})?)\.json$")
# Ring-Buffer für Browser-Log-Stream (letzte 200 Einträge)
@@ -828,14 +833,14 @@ class KobraXBridge:
# Marke ("PolyTerra PLA — Polymaker") statt nur "Generic PLA" anzeigt.
try:
import config_loader as _cl
self._filament_profiles: dict[int, dict] = _cl.list_filament_profiles()
self._filament_profiles: dict[int, dict] = _cl.list_filament_profiles(self._printer_id)
except Exception:
self._filament_profiles = {}
# Vendor-Sichtbarkeitsfilter fürs Slot-Profil-Dropdown (Issue #41 Option A).
# Leere Liste = alle Vendoren sichtbar (rückwärtskompatibel).
try:
import config_loader as _cl
self._visible_vendors: list[str] = _cl.list_visible_vendors()
self._visible_vendors: list[str] = _cl.list_visible_vendors(self._printer_id)
except Exception:
self._visible_vendors = []
self._last_state: dict = {}
@@ -870,6 +875,7 @@ class KobraXBridge:
"print_speed_mode": 2,
"connection_error": "",
"file_ready": "",
"filament_mismatch": None,
"print_start_dialog": getattr(args, "print_start_dialog", 1),
"filament_mode": "toolhead",
"supplies_usage": 0,
@@ -2600,7 +2606,7 @@ class KobraXBridge:
# Persistieren in config.ini
try:
import config_loader as _cl
_cl.save_filament_profiles(self._filament_profiles)
_cl.save_filament_profiles(self._filament_profiles, self._printer_id)
except Exception as e:
log.warning(f"save_filament_profiles failed: {e}")
return self._json_cors({"error": str(e)}, status=500)
@@ -2630,7 +2636,7 @@ class KobraXBridge:
self._visible_vendors = [str(v).strip() for v in vendors if str(v).strip()]
try:
import config_loader as _cl
_cl.save_visible_vendors(self._visible_vendors)
_cl.save_visible_vendors(self._visible_vendors, self._printer_id)
except Exception as e:
log.warning(f"save_visible_vendors failed: {e}")
return self._json_cors({"error": str(e)}, status=500)
@@ -3273,6 +3279,31 @@ class KobraXBridge:
self._state["last_upload_size"] = file_size
if auto_print:
mismatch = self._check_filament_mismatch(gcode_filaments)
if mismatch:
log.info(f"Upload+Print blockiert — Filament-Mismatch: {mismatch}")
self._state["file_ready"] = remote_filename
self._state["filament_mismatch"] = mismatch
return web.json_response({
"done": True,
"filament_mismatch": True,
"mismatch_details": mismatch,
"files": {
"local": {
"name": remote_filename,
"origin": "local",
"path": remote_filename,
"refs": {
"download": f"http://{request.host}/api/files/local/{remote_filename}",
"resource": f"http://{request.host}/api/files/local/{remote_filename}",
}
}
},
"result": {
"item": {"path": remote_filename, "root": "gcodes"},
"action": "create_file",
}
}, status=201)
log.info(f"Upload+Print (print=true): {remote_filename}")
self._state["file_ready"] = ""
loop = asyncio.get_event_loop()
@@ -3301,6 +3332,45 @@ class KobraXBridge:
}
}, status=201)
def _check_filament_mismatch(self, gcode_filaments: list | None) -> list[dict] | None:
"""Vergleicht GCode-Filamente (is_used=True) mit aktuell belegten AMS-Slots.
Gibt Liste von Mismatch-Einträgen zurück wenn mindestens ein genutzter
GCode-Slot kein passendes Material im AMS hat — sonst None.
Wird nur ausgelöst wenn AMS-Daten vorhanden sind (mindestens 1 belegter Slot)."""
if not gcode_filaments:
return None
slots = self._ams_slots or []
occupied = {s["global_index"]: s for s in slots if s.get("type") and s.get("status") == 5}
if not occupied:
return None
mismatches = []
for f in gcode_filaments:
if not f.get("is_used"):
continue
idx = int(f.get("slot_index", -1))
gcode_mat = (f.get("material") or "").upper().strip()
if not gcode_mat:
continue
slot = occupied.get(idx)
if slot is None:
mismatches.append({
"slot_index": idx,
"gcode_material": gcode_mat,
"ams_material": None,
"reason": "empty",
})
else:
ams_mat = (slot.get("type") or "").upper().strip()
if ams_mat and ams_mat != gcode_mat:
mismatches.append({
"slot_index": idx,
"gcode_material": gcode_mat,
"ams_material": ams_mat,
"reason": "mismatch",
})
return mismatches if mismatches else None
def _start_print(self, filename: str, url: str = "", md5: str = "", filesize: int = 0,
gcode_filaments: list | None = None):
self._state["file_ready"] = ""
@@ -3492,6 +3562,7 @@ class KobraXBridge:
async def handle_api_file_ready_clear(self, request):
self._state["file_ready"] = ""
self._state["filament_mismatch"] = None
self._thumbnail_b64 = ""
self._push_status_update()
return web.json_response({"result": "ok"})
@@ -3510,6 +3581,12 @@ class KobraXBridge:
if ctype is not None:
path = os.path.join(_WEB_BASE, "web", "themes", self._ui_theme, name)
elif name.startswith("lib/"):
ext = os.path.splitext(name)[1].lower()
ctype = _KX_UI_LIB_TYPES.get(ext)
if not ctype:
raise web.HTTPNotFound()
path = os.path.join(_WEB_BASE, "web", "themes", self._ui_theme, name)
else:
m = _KX_UI_TRANSLATION_RE.match(name)
if not m:

View File

@@ -1,3 +0,0 @@
fb4bf06b0cfb5bcac81e2faf99d8ace1c15771ea009837802a08a4dd5ba77a8f /home/coding/Source/kobrax/releases/0.9.0-beta1/extract_credentials
68f9bf800d1df0e71423edd35e90a8f5f7fb6e9e5220a8c12ed98cc6c4fb4833 /home/coding/Source/kobrax/releases/0.9.0-beta1/extract_credentials.exe
7c1a99953e21fc3881f60df444940d66a4689b009e9a17ec936396857a6b9dc0 /home/coding/Source/kobrax/releases/0.9.0-beta1/kx-bridge

View File

@@ -1,11 +0,0 @@
06873a3b6773e6e755deff33dba468dee3af09859f9b026b14a600501c8285cb anycubic-certs.zip
68e08c6b6badb2bf86f61e2164e96b902944c885f083a598ea4e3c854c432e91 extract_credentials
8d4d78bdc887e2512790805b711b941420536d5f0f30043b5f3bc74d4fa893d3 extract_credentials.exe
9b79a5f8edc734018fc547ac232ddd822ec5fee050e99e3305ccf8f38b177c9e fetch_credentials
b79615bc2f9df33bccd5471bfcd83c9bdc5291e857fe5b04927b171342bc1b48 fetch_credentials.exe
37ae8e3f4dc7bb6cf434de980db4aed9c1047d995063ce289bdc268f7764380e kx-bridge.exe
4f636f91fef7eb1ea1ad1ae8c7f7eaa64b725e4dc2e74db42a53ce5dc5808ba3 kx-bridge-linux-amd64
b0ff6fa222cfbf79e0ab609aac4c9d3b1acd80b5c4df10c7847e0c7d125913bc kx-bridge-linux-amd64.zip
0cd9448ceec03cc7f545f9c62a21b7075a866dba5ee396ec26b48816f927db1a kx-bridge-linux-arm64
99c4641bbdcc000b2cf2142ee01c16942b13da47ea837885fa1be68e984f6101 kx-bridge-linux-arm64.zip
31659f76913e1c785cfa9e0a9e87f893a483a6a55248006b046ac07b25b40870 kx-bridge-windows.zip

Binary file not shown.

View File

@@ -1,24 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIEDTCCAvWgAwIBAgICAZAwDQYJKoZIhvcNAQEFBQAwgZsxCzAJBgNVBAYTAkNO
MRIwEAYDVQQIDAlHdWFuZ2RvbmcxETAPBgNVBAcMCFNoZW56aGVuMREwDwYDVQQK
DAhBbnljdWJpYzERMA8GA1UECwwIQW55Y3ViaWMxEzARBgNVBAMMCkFDIFJvb3Qg
Q0ExKjAoBgkqhkiG9w0BCQEWG2FueWN1YmljX2Nsb3VkQGFueWN1YmljLmNvbTAg
Fw0yMzA3MjAwMzI3NTFaGA8yMTIzMDcyMTAzMjc1MVowgZ8xCzAJBgNVBAYTAkNO
MRIwEAYDVQQIDAlHdWFuZ2RvbmcxETAPBgNVBAcMCFNoZW56aGVuMREwDwYDVQQK
DAhBbnljdWJpYzERMA8GA1UECwwIQW55Y3ViaWMxFzAVBgNVBAMMDkFueWN1Ymlj
U2xpY2VyMSowKAYJKoZIhvcNAQkBFhthbnljdWJpY19jbG91ZEBhbnljdWJpYy5j
b20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDdoQ7g2F/yecfpdlqT
b8W/84r3vQ4ZEWx2PbSTBcGD55HmzJp2lwABHFHbn4CltT9YzoJWpOiVMHYnyPep
43tkNUIcGm7z0jrTD5djyYjVAzEitkNzJspKK/xcVmZe/V7Q3IAWXtzgWCd0YpVk
K3J0HqoqJvcTSnYe4VXxbIGwbpeYyji9W/DuG1M4Z+sFiPDWeR9xo5IXRU5ZwaTP
8OiCCLSBbeKgf0UFWTIZdJ1JXJ7efbbstZOjf5L9LhBIC0hLdL4jlMpF7r0ThecJ
cTx9Bnw/hhy+i32rJTRzZDIaLhKg/bka9ZrORZdxxQRiPoMjLjoxtr4+AUaeLWkI
ajSJAgMBAAGjUzBRMB0GA1UdDgQWBBRI4P3/uKdYYFPEcFIwYxdv1p9gETAfBgNV
HSMEGDAWgBQlkDqpFERfr3u1rR9gNbNKtgrHIjAPBgNVHRMBAf8EBTADAQH/MA0G
CSqGSIb3DQEBBQUAA4IBAQBP3ws80Y9eBR2lpjYP3rVvH8kA6+LnEXT4PpHj+fSw
jciaNskzpiwNvBy00m32ACR5YKlMUjevlQuyyw+LQbTUwAEOwyy9SDQpiXdjL6q3
SPQ4aB4A57nFXOGrthc/nb9yFcteWrZrKbwvVUu2vqU7U8n7lJKjhVuFRWSXS3SV
sPc9JZ21kpPYWKbGtfD6jUlW0Ip+PurLw9FrbVwnEcOMf/ezSlrH5c8mfJyo8pVk
aC/6PpReqijusOSRZ5oLyhPvtgddXseJFByun1Ud0CDlFA05nGGPmnVcXD+GMnHH
i6baCTeifwp5Jpdzv4imcCPvayKUNuX32vYNfNkWC/R5
-----END CERTIFICATE-----

View File

@@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDdoQ7g2F/yecfp
dlqTb8W/84r3vQ4ZEWx2PbSTBcGD55HmzJp2lwABHFHbn4CltT9YzoJWpOiVMHYn
yPep43tkNUIcGm7z0jrTD5djyYjVAzEitkNzJspKK/xcVmZe/V7Q3IAWXtzgWCd0
YpVkK3J0HqoqJvcTSnYe4VXxbIGwbpeYyji9W/DuG1M4Z+sFiPDWeR9xo5IXRU5Z
waTP8OiCCLSBbeKgf0UFWTIZdJ1JXJ7efbbstZOjf5L9LhBIC0hLdL4jlMpF7r0T
hecJcTx9Bnw/hhy+i32rJTRzZDIaLhKg/bka9ZrORZdxxQRiPoMjLjoxtr4+AUae
LWkIajSJAgMBAAECggEASwRkC9lRiLqN30kvWW5g6hsec8KrTfLm2pMCVy2AlgxB
B3VD51YvKzERyBwSKITT/1RPK9K/4xe3NrpAkmGsd3vLd8W+vorvXFePr7gct7VP
4Wb+J7D+keKXlg2sswRiHqI0PN45Nzq/iBaCaJiIMiPbB0+PHBl9J/Cv7XsD3tq+
9WKhvXf2g1g9GMrLaCCcWXWCqcu0LlbqJnw3yMnJLSltmyFTmlVLjDHM75bMVz97
4emQzOlnRN2yA5cWWCaM+mgjNM2aWwUsXBZzCgwSqSaj1QD4B/epCuDBORWHS9D6
jL15w8xjly9q8OS+4d6beR5h9GiPyMK4Ff2wXImCXQKBgQDwXxtrL+kVZrQ/qftj
24F3+QDN0j5Z3lUMTfZPn6ng/E/aBfn8KcWJHj2vYkKZdB5wOXJr56BYe3Hukzfp
QF0E2+g1WAGskF1mb/vVab54geox5Y6CA+ionRn2kcCwybVkktR/0JK2UV9Qjb/z
k1WU+RUhNrW/GDBqYulaadnR+wKBgQDsCf2/yKGPxj4pIvAtn5RFSlfscddgkSnc
ouBkDXEp5ta+5PGrlrdzS/F0vFhvBPbfbVJxVwRnM/Oqj8c0/bj7oc5RpPxirciO
AaovKVPTiORaviytnB2HgkflkJfy5vdXv4ZQahAV/UwtSmLwBshe+Ya68MAFrQRa
7M4z6k4QSwKBgQCm7OVVoofzXMeADsONrTpT3pA4XvD95/CYAuwyj2ah35Z0igH4
o+mSN3YO/eXSO1mIBdz4Inqv98o/K+2ABjqSzUSNBvjipb63DL2Oj0i+1zmUPR6i
G6TOs4r8OGvgWbOmjHEV8fpwskHG5ymONZsRQYjy79N3SY0V1GrJZwjlUQKBgD0x
AeWcP7YkMK09b4KEYk3sTgrwIGPafj3Cw+VsTrAMNhPbCoPvWLO9NmWLBmoRoWae
0sarRmry3vKSv5QPSsuBURl9aiiy4NFfwRzk2+R1Eq4rqy1+0XD152muKJZCJlFL
R6jFNlJdDkiXhjqvp3ZnvfPswfs2tXBU/8gZsA8tAoGBALXfc5m9I5R1l1zN7tpa
ncA0S3EKzqmuCc3KzlS6OS0e9Lz1MsmfEsvxvW3w4SrdfTbwQpEy9RNg89dlgPtc
rdId1QdN2eWPY5M4lz9n9EYdzi9ufoKAEYu2a0lP+qz690JwmL1Jx49bvQEn5Nu0
4swn72uwBRlhjAw46MF77SBQ
-----END PRIVATE KEY-----

View File

@@ -1,89 +0,0 @@
pyinstaller --onefile --name extract_credentials /src/extract_credentials.py && cp /src/dist/extract_credentials.exe /out/ && pyinstaller --onefile --name fetch_credentials /src/fetch_credentials.py && cp /src/dist/fetch_credentials.exe /out/
114 INFO: PyInstaller: 3.6
114 INFO: Python: 3.7.5
114 INFO: Platform: Windows-7-6.1.7601-SP1
115 INFO: wrote Z:\src\extract_credentials.spec
120 INFO: UPX is not available.
121 INFO: Extending PYTHONPATH with paths
['Z:\\src', 'Z:\\src']
121 INFO: checking Analysis
121 INFO: Building Analysis because Analysis-00.toc is non existent
121 INFO: Initializing module dependency graph...
124 INFO: Caching module graph hooks...
130 INFO: Analyzing base_library.zip ...
2142 INFO: Caching module dependency graph...
2226 INFO: running Analysis Analysis-00.toc
2234 INFO: Adding Microsoft.Windows.Common-Controls to dependent assemblies of final executable
required by c:\Python37\python.exe
2467 INFO: Analyzing \src\extract_credentials.py
2497 INFO: Processing module hooks...
2497 INFO: Loading module hook "hook-encodings.py"...
2649 INFO: Loading module hook "hook-pydoc.py"...
2650 INFO: Loading module hook "hook-xml.py"...
2788 INFO: Looking for ctypes DLLs
2791 INFO: Analyzing run-time hooks ...
2794 INFO: Looking for dynamic libraries
2909 INFO: Looking for eggs
2909 INFO: Using Python library c:\Python37\python37.dll
2909 INFO: Found binding redirects:
[]
2910 INFO: Warnings written to Z:\src\build\extract_credentials\warn-extract_credentials.txt
2927 INFO: Graph cross-reference written to Z:\src\build\extract_credentials\xref-extract_credentials.html
2939 INFO: checking PYZ
2939 INFO: Building PYZ because PYZ-00.toc is non existent
2939 INFO: Building PYZ (ZlibArchive) Z:\src\build\extract_credentials\PYZ-00.pyz
3199 INFO: Building PYZ (ZlibArchive) Z:\src\build\extract_credentials\PYZ-00.pyz completed successfully.
3203 INFO: checking PKG
3203 INFO: Building PKG because PKG-00.toc is non existent
3203 INFO: Building PKG (CArchive) PKG-00.pkg
4599 INFO: Building PKG (CArchive) PKG-00.pkg completed successfully.
4601 INFO: Bootloader c:\Python37\lib\site-packages\PyInstaller\bootloader\Windows-64bit\run.exe
4601 INFO: checking EXE
4601 INFO: Building EXE because EXE-00.toc is non existent
4601 INFO: Building EXE from EXE-00.toc
4601 INFO: Appending archive to EXE Z:\src\dist\extract_credentials.exe
4604 INFO: Building EXE from EXE-00.toc completed successfully.
77 INFO: PyInstaller: 3.6
77 INFO: Python: 3.7.5
77 INFO: Platform: Windows-7-6.1.7601-SP1
77 INFO: wrote Z:\src\fetch_credentials.spec
83 INFO: UPX is not available.
84 INFO: Extending PYTHONPATH with paths
['Z:\\src', 'Z:\\src']
84 INFO: checking Analysis
84 INFO: Building Analysis because Analysis-00.toc is non existent
84 INFO: Initializing module dependency graph...
88 INFO: Caching module graph hooks...
94 INFO: Analyzing base_library.zip ...
2079 INFO: Caching module dependency graph...
2168 INFO: running Analysis Analysis-00.toc
2170 INFO: Adding Microsoft.Windows.Common-Controls to dependent assemblies of final executable
required by c:\Python37\python.exe
2425 INFO: Analyzing \src\fetch_credentials.py
2501 INFO: Processing module hooks...
2501 INFO: Loading module hook "hook-encodings.py"...
2630 INFO: Loading module hook "hook-pydoc.py"...
2631 INFO: Loading module hook "hook-xml.py"...
2765 INFO: Looking for ctypes DLLs
2765 INFO: Analyzing run-time hooks ...
2768 INFO: Looking for dynamic libraries
2878 INFO: Looking for eggs
2878 INFO: Using Python library c:\Python37\python37.dll
2878 INFO: Found binding redirects:
[]
2880 INFO: Warnings written to Z:\src\build\fetch_credentials\warn-fetch_credentials.txt
2896 INFO: Graph cross-reference written to Z:\src\build\fetch_credentials\xref-fetch_credentials.html
2901 INFO: checking PYZ
2901 INFO: Building PYZ because PYZ-00.toc is non existent
2901 INFO: Building PYZ (ZlibArchive) Z:\src\build\fetch_credentials\PYZ-00.pyz
3159 INFO: Building PYZ (ZlibArchive) Z:\src\build\fetch_credentials\PYZ-00.pyz completed successfully.
3163 INFO: checking PKG
3163 INFO: Building PKG because PKG-00.toc is non existent
3163 INFO: Building PKG (CArchive) PKG-00.pkg
4428 INFO: Building PKG (CArchive) PKG-00.pkg completed successfully.
4430 INFO: Bootloader c:\Python37\lib\site-packages\PyInstaller\bootloader\Windows-64bit\run.exe
4430 INFO: checking EXE
4430 INFO: Building EXE because EXE-00.toc is non existent
4430 INFO: Building EXE from EXE-00.toc
4430 INFO: Appending archive to EXE Z:\src\dist\fetch_credentials.exe
4434 INFO: Building EXE from EXE-00.toc completed successfully.

View File

@@ -1,56 +0,0 @@
32 INFO: PyInstaller: 6.19.0, contrib hooks: 2026.4
32 INFO: Python: 3.12.3
33 INFO: Platform: Linux-6.17.0-35-generic-x86_64-with-glibc2.39
33 INFO: Python environment: /usr
33 INFO: wrote /home/coding/Source/KX-Bridge-Release/build/extract_credentials.spec
34 INFO: Module search paths (PYTHONPATH):
['/usr/lib/python312.zip',
'/usr/lib/python3.12',
'/usr/lib/python3.12/lib-dynload',
'/home/coding/.local/lib/python3.12/site-packages',
'/usr/local/lib/python3.12/dist-packages',
'/usr/lib/python3/dist-packages',
'/usr/lib/python3.12/dist-packages',
'/home/coding/Source/KX-Bridge-Release']
352 INFO: checking Analysis
352 INFO: Building Analysis because Analysis-00.toc is non existent
352 INFO: Looking for Python shared library...
364 WARNING: Unrecognised line of output 'Der Cache wurde generiert von: ldconfig (Ubuntu GLIBC 2.39-0ubuntu8.7) stable release version 2.39' from ldconfig
364 INFO: Using Python shared library: /lib/x86_64-linux-gnu/libpython3.12.so.1.0
364 INFO: Running Analysis Analysis-00.toc
364 INFO: Target bytecode optimization level: 0
364 INFO: Initializing module dependency graph...
366 INFO: Initializing module graph hook caches...
380 INFO: Analyzing modules for base_library.zip ...
1005 INFO: Processing standard module hook 'hook-encodings.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
1923 INFO: Processing standard module hook 'hook-pickle.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
2491 INFO: Processing standard module hook 'hook-heapq.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
2739 INFO: Caching module dependency graph...
2764 INFO: Analyzing /home/coding/Source/KX-Bridge-Release/extract_credentials.py
2790 INFO: Processing standard module hook 'hook-platform.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
2801 INFO: Processing standard module hook 'hook-_ctypes.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
2808 INFO: Processing module hooks (post-graph stage)...
2812 INFO: Performing binary vs. data reclassification (1 entries)
2818 INFO: Looking for ctypes DLLs
2825 INFO: Analyzing run-time hooks ...
2825 INFO: Including run-time hook 'pyi_rth_inspect.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks/rthooks'
2829 INFO: Creating base_library.zip...
2841 INFO: Looking for dynamic libraries
2987 INFO: Warnings written to /home/coding/Source/KX-Bridge-Release/build/pyinstaller/extract_credentials/warn-extract_credentials.txt
2993 INFO: Graph cross-reference written to /home/coding/Source/KX-Bridge-Release/build/pyinstaller/extract_credentials/xref-extract_credentials.html
3000 INFO: checking PYZ
3000 INFO: Building PYZ because PYZ-00.toc is non existent
3000 INFO: Building PYZ (ZlibArchive) /home/coding/Source/KX-Bridge-Release/build/pyinstaller/extract_credentials/PYZ-00.pyz
3099 INFO: Building PYZ (ZlibArchive) /home/coding/Source/KX-Bridge-Release/build/pyinstaller/extract_credentials/PYZ-00.pyz completed successfully.
3105 INFO: checking PKG
3105 INFO: Building PKG because PKG-00.toc is non existent
3105 INFO: Building PKG (CArchive) extract_credentials.pkg
5376 INFO: Building PKG (CArchive) extract_credentials.pkg completed successfully.
5376 INFO: Bootloader /home/coding/.local/lib/python3.12/site-packages/PyInstaller/bootloader/Linux-64bit-intel/run
5376 INFO: checking EXE
5376 INFO: Building EXE because EXE-00.toc is non existent
5376 INFO: Building EXE from EXE-00.toc
5376 INFO: Copying bootloader EXE to /home/coding/Source/KX-Bridge-Release/releases/0.9.27/extract_credentials
5376 INFO: Appending PKG archive to custom ELF section in EXE
5386 INFO: Building EXE from EXE-00.toc completed successfully.
5386 INFO: Build complete! The results are available in: /home/coding/Source/KX-Bridge-Release/releases/0.9.27

View File

@@ -1,91 +0,0 @@
35 INFO: PyInstaller: 6.19.0, contrib hooks: 2026.4
35 INFO: Python: 3.12.3
36 INFO: Platform: Linux-6.17.0-35-generic-x86_64-with-glibc2.39
36 INFO: Python environment: /usr
36 INFO: wrote /home/coding/Source/KX-Bridge-Release/build/fetch_credentials.spec
37 WARNING: collect_data_files - skipping data collection for module 'pycryptodome' as it is not a package.
37 WARNING: collect_dynamic_libs - skipping library collection for module 'pycryptodome' as it is not a package.
117 INFO: Module search paths (PYTHONPATH):
['/usr/lib/python312.zip',
'/usr/lib/python3.12',
'/usr/lib/python3.12/lib-dynload',
'/home/coding/.local/lib/python3.12/site-packages',
'/usr/local/lib/python3.12/dist-packages',
'/usr/lib/python3/dist-packages',
'/usr/lib/python3.12/dist-packages',
'/home/coding/Source/KX-Bridge-Release']
458 INFO: checking Analysis
458 INFO: Building Analysis because Analysis-00.toc is non existent
458 INFO: Looking for Python shared library...
475 WARNING: Unrecognised line of output 'Der Cache wurde generiert von: ldconfig (Ubuntu GLIBC 2.39-0ubuntu8.7) stable release version 2.39' from ldconfig
475 INFO: Using Python shared library: /lib/x86_64-linux-gnu/libpython3.12.so.1.0
475 INFO: Running Analysis Analysis-00.toc
475 INFO: Target bytecode optimization level: 0
476 INFO: Initializing module dependency graph...
476 INFO: Initializing module graph hook caches...
491 INFO: Analyzing modules for base_library.zip ...
1017 INFO: Processing standard module hook 'hook-encodings.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
1871 INFO: Processing standard module hook 'hook-pickle.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
2469 INFO: Processing standard module hook 'hook-heapq.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
2692 INFO: Caching module dependency graph...
2717 INFO: Analyzing /home/coding/Source/KX-Bridge-Release/fetch_credentials.py
2750 INFO: Processing standard module hook 'hook-urllib3.py' from '/home/coding/.local/lib/python3.12/site-packages/_pyinstaller_hooks_contrib/stdhooks'
2817 INFO: Processing pre-safe-import-module hook 'hook-typing_extensions.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks/pre_safe_import_module'
2818 INFO: SetuptoolsInfo: initializing cached setuptools info...
3154 INFO: Processing standard module hook 'hook-multiprocessing.util.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
3216 INFO: Processing standard module hook 'hook-xml.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
3402 INFO: Processing standard module hook 'hook-_ctypes.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
4175 INFO: Processing standard module hook 'hook-chardet.py' from '/home/coding/.local/lib/python3.12/site-packages/_pyinstaller_hooks_contrib/stdhooks'
4759 INFO: Processing standard module hook 'hook-cryptography.py' from '/home/coding/.local/lib/python3.12/site-packages/_pyinstaller_hooks_contrib/stdhooks'
5142 INFO: hook-cryptography: cryptography uses dynamically-linked OpenSSL: '/lib/x86_64-linux-gnu/libssl.so.3'
5351 INFO: Processing standard module hook 'hook-bcrypt.py' from '/home/coding/.local/lib/python3.12/site-packages/_pyinstaller_hooks_contrib/stdhooks'
5426 INFO: Processing standard module hook 'hook-certifi.py' from '/home/coding/.local/lib/python3.12/site-packages/_pyinstaller_hooks_contrib/stdhooks'
5502 INFO: Processing standard module hook 'hook-Crypto.py' from '/home/coding/.local/lib/python3.12/site-packages/_pyinstaller_hooks_contrib/stdhooks'
5706 INFO: Processing standard module hook 'hook-pycparser.py' from '/home/coding/.local/lib/python3.12/site-packages/_pyinstaller_hooks_contrib/stdhooks'
5802 INFO: Processing standard module hook 'hook-setuptools.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
5806 INFO: Processing pre-safe-import-module hook 'hook-distutils.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks/pre_safe_import_module'
5861 INFO: Processing standard module hook 'hook-sysconfig.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
5896 INFO: Processing standard module hook 'hook-platform.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
5906 INFO: Processing standard module hook 'hook-_osx_support.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
6028 INFO: Processing pre-safe-import-module hook 'hook-importlib_metadata.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks/pre_safe_import_module'
6141 INFO: Processing pre-safe-import-module hook 'hook-packaging.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks/pre_safe_import_module'
6502 INFO: Processing standard module hook 'hook-pkg_resources.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
7089 INFO: Processing standard module hook 'hook-xml.dom.domreg.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
7323 INFO: Processing standard module hook 'hook-sqlite3.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
7648 INFO: Processing standard module hook 'hook-difflib.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
7995 INFO: Processing module hooks (post-graph stage)...
7997 WARNING: Hidden import "pycparser.lextab" not found!
7997 WARNING: Hidden import "pycparser.yacctab" not found!
8231 INFO: Processing pre-safe-import-module hook 'hook-platformdirs.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks/pre_safe_import_module'
8237 INFO: Processing standard module hook 'hook-platformdirs.py' from '/home/coding/.local/lib/python3.12/site-packages/_pyinstaller_hooks_contrib/stdhooks'
8304 INFO: Processing standard module hook 'hook-setuptools._vendor.importlib_metadata.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
8391 INFO: Processing standard module hook 'hook-setuptools._vendor.jaraco.text.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks'
8490 INFO: Performing binary vs. data reclassification (51 entries)
8605 INFO: Looking for ctypes DLLs
8618 INFO: Analyzing run-time hooks ...
8620 INFO: Including run-time hook 'pyi_rth_inspect.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks/rthooks'
8622 INFO: Including run-time hook 'pyi_rth_pkgutil.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks/rthooks'
8623 INFO: Including run-time hook 'pyi_rth_multiprocessing.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks/rthooks'
8625 INFO: Including run-time hook 'pyi_rth_setuptools.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks/rthooks'
8626 INFO: Including run-time hook 'pyi_rth_pkgres.py' from '/home/coding/.local/lib/python3.12/site-packages/PyInstaller/hooks/rthooks'
8628 INFO: Including run-time hook 'pyi_rth_cryptography_openssl.py' from '/home/coding/.local/lib/python3.12/site-packages/_pyinstaller_hooks_contrib/rthooks'
8635 INFO: Creating base_library.zip...
8664 INFO: Looking for dynamic libraries
9293 INFO: Warnings written to /home/coding/Source/KX-Bridge-Release/build/pyinstaller/fetch_credentials/warn-fetch_credentials.txt
9315 INFO: Graph cross-reference written to /home/coding/Source/KX-Bridge-Release/build/pyinstaller/fetch_credentials/xref-fetch_credentials.html
9327 INFO: checking PYZ
9327 INFO: Building PYZ because PYZ-00.toc is non existent
9327 INFO: Building PYZ (ZlibArchive) /home/coding/Source/KX-Bridge-Release/build/pyinstaller/fetch_credentials/PYZ-00.pyz
9755 INFO: Building PYZ (ZlibArchive) /home/coding/Source/KX-Bridge-Release/build/pyinstaller/fetch_credentials/PYZ-00.pyz completed successfully.
9766 INFO: checking PKG
9766 INFO: Building PKG because PKG-00.toc is non existent
9766 INFO: Building PKG (CArchive) fetch_credentials.pkg
14335 INFO: Building PKG (CArchive) fetch_credentials.pkg completed successfully.
14336 INFO: Bootloader /home/coding/.local/lib/python3.12/site-packages/PyInstaller/bootloader/Linux-64bit-intel/run
14336 INFO: checking EXE
14336 INFO: Building EXE because EXE-00.toc is non existent
14336 INFO: Building EXE from EXE-00.toc
14336 INFO: Copying bootloader EXE to /home/coding/Source/KX-Bridge-Release/releases/0.9.27/fetch_credentials
14336 INFO: Appending PKG archive to custom ELF section in EXE
14360 INFO: Building EXE from EXE-00.toc completed successfully.
14361 INFO: Build complete! The results are available in: /home/coding/Source/KX-Bridge-Release/releases/0.9.27

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,67 @@
"""Per-printer filament-profile isolation (config_loader).
Regression test for the multi-printer bug (issue #74): the slot->profile mapping
and ``visible_vendors`` lived in a single global ``[filament_profiles]`` section,
so configuring one printer overwrote the other and after a restart both loaded
the same map. Each printer now uses its own ``[filament_profiles_<id>]`` section,
with a read-fallback to the legacy global section for backward compatibility.
"""
import sys
import pathlib
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent)) # repo root
import config_loader # noqa: E402
BASE_INI = (
"[printer_1]\nname = K1\n\n"
"[printer_2]\nname = K2\n\n"
"[filament_profiles]\n"
"visible_vendors = Anycubic, SUNLU\n"
"slot_0_vendor = Anycubic\nslot_0_name = Anycubic PLA+\nslot_0_id = GFPLA+\n"
)
def _use_ini(monkeypatch, tmp_path, text=BASE_INI):
path = tmp_path / "config.ini"
path.write_text(text, encoding="utf-8")
monkeypatch.setattr(config_loader, "_find_config_file", lambda: path)
return path
def test_legacy_global_still_works(tmp_path, monkeypatch):
"""No printer_id -> original global section (single-printer back-compat)."""
_use_ini(monkeypatch, tmp_path)
assert config_loader.list_filament_profiles()[0]["name"] == "Anycubic PLA+"
assert config_loader.list_visible_vendors() == ["Anycubic", "SUNLU"]
def test_read_falls_back_to_global_until_first_save(tmp_path, monkeypatch):
"""Before any per-printer save, both printers see the global mapping."""
_use_ini(monkeypatch, tmp_path)
assert config_loader.list_filament_profiles("1")[0]["name"] == "Anycubic PLA+"
assert config_loader.list_filament_profiles("2")[0]["name"] == "Anycubic PLA+"
def test_saving_one_printer_does_not_touch_the_other(tmp_path, monkeypatch):
"""Core regression: configuring printer 1 must not change printer 2."""
_use_ini(monkeypatch, tmp_path)
config_loader.save_filament_profiles(
{0: {"vendor": "KINGROON", "name": "KINGROON PLA Basic", "id": "Pc0b8a01"}}, "1")
assert config_loader.list_filament_profiles("1")[0]["name"] == "KINGROON PLA Basic"
assert config_loader.list_filament_profiles("2")[0]["name"] == "Anycubic PLA+"
# legacy global section preserved untouched
assert config_loader.list_filament_profiles()[0]["name"] == "Anycubic PLA+"
def test_visible_vendors_isolated_per_printer(tmp_path, monkeypatch):
_use_ini(monkeypatch, tmp_path)
config_loader.save_visible_vendors(["KINGROON"], "1")
assert config_loader.list_visible_vendors("1") == ["KINGROON"]
assert config_loader.list_visible_vendors("2") == ["Anycubic", "SUNLU"]
def test_save_visible_vendors_keeps_slot_fallback(tmp_path, monkeypatch):
"""Creating a per-printer section only for vendors must not orphan slots."""
_use_ini(monkeypatch, tmp_path)
config_loader.save_visible_vendors(["KINGROON"], "1")
assert config_loader.list_filament_profiles("1")[0]["name"] == "Anycubic PLA+"

View File

@@ -54,6 +54,13 @@ function _loadSpoolmanStatus(){
_spoolmanStatus=d;
_slotSpoolMap=d.slot_spools||{};
_updateSpoolmanStatusDot();
_buildSpoolmanSection();
renderSpoolmanSlotCard();
if(d.configured){
fetch(_apiUrl('/kx/spoolman/spools')).then(function(r){return r.json();}).then(function(sd){
_spoolmanSpools=sd.spools||[];
});
}
}).catch(function(){});
}
function _updateSpoolmanStatusDot(){
@@ -78,12 +85,8 @@ function _buildSpoolmanSection(){
if(loading)loading.style.display='';
var usedSlots={};
document.querySelectorAll('#fd-slots select').forEach(function(sel){
var idx=parseInt(sel.value);
if(idx>=0){
var slot=(_amsSlots||[]).find(function(s){return s.slot_index===idx;});
if(slot&&!usedSlots[idx])usedSlots[idx]=slot;
}
(_amsSlots||[]).forEach(function(slot){
usedSlots[slot.slot_index]=slot;
});
fetch(_apiUrl('/kx/spoolman/spools')).then(function(r){return r.json();}).then(function(d){
@@ -289,7 +292,7 @@ function renderPrinterDropdown(){
menu.innerHTML=_printers.map(function(p){
var active=_activePrinter&&String(p.id)===String(_activePrinter.id);
var num=p.id;
return '<a href="/printer'+num+'" style="display:block;padding:10px 14px;color:'+(active?'var(--accent)':'var(--txt)')+';text-decoration:none;font-size:13px;border-bottom:1px solid var(--border)" '+(active?'style="font-weight:600"':'')+'>'+
return '<a href="'+((p.bridge_url||'').replace(/\/+$/,''))+'/printer'+num+'" style="display:block;padding:10px 14px;color:'+(active?'var(--accent)':'var(--txt)')+';text-decoration:none;font-size:13px;border-bottom:1px solid var(--border)" '+(active?'style="font-weight:600"':'')+'>'+
(active?'▶ ':'')+p.name+'</a>';
}).join('');
}
@@ -376,12 +379,10 @@ function applyLang(){
setText('d-chart-label',T.panel_temps_chart);
// Axis labels
setText('ptitle-motion-xy',T.panel_motion_xy);
setText('ptitle-motion-z',T.panel_motion_z);
document.querySelectorAll('.lbl-home-z').forEach(e=>e.textContent=T.btn_home_z);
document.querySelectorAll('.lbl-home-xy').forEach(e=>e.textContent=T.btn_home_xy);
document.querySelectorAll('.lbl-home-all').forEach(e=>e.textContent=T.btn_home_all);
document.querySelectorAll('.lbl-disable-motors').forEach(e=>e.textContent=T.btn_disable_motors);
document.querySelectorAll('.lbl-step').forEach(e=>e.textContent=T.label_step);
document.querySelectorAll('.temp-input').forEach(e=>e.setAttribute('placeholder',T.label_target_c.replace(':','')));
// Console
setText('ptitle-console',T.panel_console_title);
@@ -749,7 +750,7 @@ function applyState(){
frb.style.display='none';
if(!_fdDialogOpen&&!_fdUserCancelled&&_fdAutoOpenedFile!==s.file_ready){
_fdAutoOpenedFile=s.file_ready;
startReadyFileWithSlots(s.file_ready,true);
startReadyFileWithSlots(s.file_ready,true,s.filament_mismatch||null);
}
} else {
frb.style.display='flex';
@@ -962,6 +963,22 @@ function applyState(){
var tt=(profile.name||'')+(profile.id?' ('+profile.id+')':'');
vendorBadge='<div class="slot-label" style="font-size:9px;color:var(--accent);font-weight:600;margin-top:1px" title="'+tt+'">'+profile.vendor+'</div>';
}
var spoolSel='';
if(_spoolmanStatus.configured&&!empty&&_spoolmanSpools.length){
var curSpool=_slotSpoolMap[String(globalIdx)]||'';
var spoolOpts='<option value=""></option>'+_spoolmanSpools.map(function(sp){
var vendor=sp.filament&&sp.filament.vendor?sp.filament.vendor.name+' ':'';
var name=sp.filament?sp.filament.name:'#'+sp.id;
var rem=sp.remaining_weight!=null?' '+sp.remaining_weight.toFixed(0)+'g':'';
return '<option value="'+sp.id+'"'+(String(sp.id)==String(curSpool)?' selected':'')+'>'+
escHtml(vendor+name+rem)+'</option>';
}).join('');
spoolSel='<div onclick="event.stopPropagation()" style="margin-top:6px;border-top:1px solid rgba(255,255,255,.07);padding-top:5px">'
+'<div style="font-size:8px;font-weight:700;color:var(--accent);text-transform:uppercase;letter-spacing:.06em;margin-bottom:3px">🧵 Spoolman</div>'
+'<select data-spool-slot="'+globalIdx+'" onchange="onAmsSpoolChange(this)" '
+'style="width:100%;padding:3px 5px;font-size:10px;border-radius:5px;border:1px solid var(--border);background:var(--card);color:var(--txt);cursor:pointer">'+spoolOpts+'</select>'
+'</div>';
}
html+='<div class="ams-slot'+(active?' active':'')+(loaded?' loaded':'')+(activity?' '+activity:'')+(empty?' empty':'')
+'" style="--slot-color:'+col+';opacity:'+(empty?0.4:1)+';cursor:pointer" onclick="openSlotEdit('+i+')">'
+'<div class="slot-circle" style="background:'+col+'"></div>'
@@ -969,6 +986,7 @@ function applyState(){
+vendorBadge
+'<div class="slot-label">'+slotLabel+'</div>'
+'<div class="slot-label" style="font-size:10px;color:var(--txt2)">'+pct+'</div>'
+spoolSel
+'<div style="font-size:9px;color:var(--txt2);margin-top:2px">✏</div>'
+'</div>';
});
@@ -979,7 +997,10 @@ function applyState(){
}
html+='</div></div>';
});
document.getElementById('ams-slots').innerHTML=html;
// Nicht rendern wenn ein Spool-Dropdown gerade offen ist (verhindert Schließen beim Poll)
var activeEl=document.activeElement;
var spoolOpen=activeEl&&activeEl.tagName==='SELECT'&&activeEl.dataset.spoolSlot!=null;
if(!spoolOpen) document.getElementById('ams-slots').innerHTML=html;
}
// camera overlay
@@ -1075,6 +1096,7 @@ function openSettings(){
pi.value=sec;
}
renderFilamentMapping(d.filament_profiles||{});
renderSpoolmanSlotCard();
// Spoolman
var su=document.getElementById('s-spoolman-url');if(su)su.value=d.spoolman_server||'';
var sr=document.getElementById('s-spoolman-sync-rate');if(sr)sr.value=(d.spoolman_sync_rate!==undefined?d.spoolman_sync_rate:30);
@@ -1222,6 +1244,64 @@ function _vendorCheck(cb){
var v=cb.getAttribute('data-vendor');
if(cb.checked)_vendorChecklistSel[v]=true; else delete _vendorChecklistSel[v];
}
function renderSpoolmanSlotCard(){
var card=document.getElementById('spoolman-slot-card');
var rows=document.getElementById('spoolman-slot-rows');
if(!card||!rows)return;
if(!_spoolmanStatus.configured){card.style.display='none';return;}
card.style.display='';
Promise.all([
fetch(_apiUrl('/kx/spoolman/spools')).then(function(r){return r.json();}),
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json();})
]).then(function(res){
var spools=res[0].spools||[];
var slots=(res[1].result||[]).sort(function(a,b){return a.slot_index-b.slot_index;});
if(!slots.length){rows.innerHTML='<span style="font-size:11px;color:var(--txt2)">Keine AMS-Slots bekannt.</span>';return;}
rows.innerHTML=slots.map(function(slot){
var idx=parseInt(slot.slot_index);
var col=slot.color_hex||'#888';
var mat=slot.material||'';
var current=_slotSpoolMap[String(idx)]||'';
var opts='<option value=""></option>'+spools.map(function(sp){
var rem=sp.remaining_weight!=null?' ('+sp.remaining_weight.toFixed(0)+'g)':'';
var vendor=sp.filament&&sp.filament.vendor?sp.filament.vendor.name+' ':'';
var name=sp.filament?sp.filament.name:'Spool #'+sp.id;
var mat2=sp.filament&&sp.filament.material?' · '+sp.filament.material:'';
return '<option value="'+sp.id+'"'+(String(sp.id)==String(current)?' selected':'')+'>'+
escHtml('#'+sp.id+' '+vendor+name+mat2+rem)+'</option>';
}).join('');
return '<div style="display:flex;align-items:center;gap:8px;font-size:12px">'+
'<span style="display:inline-block;width:14px;height:14px;border-radius:50%;background:'+col+';border:1px solid var(--border);flex-shrink:0"></span>'+
'<span style="color:var(--txt2);min-width:60px">Slot '+(idx+1)+' <span style="color:var(--txt2);font-size:10px">'+escHtml(mat)+'</span></span>'+
'<select data-spool-slot="'+idx+'" style="flex:1;padding:3px 6px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt);font-size:12px">'+opts+'</select></div>';
}).join('');
}).catch(function(){rows.innerHTML='<span style="font-size:11px;color:var(--err)">Spoolman nicht erreichbar</span>';});
}
function onAmsSpoolChange(sel){
var idx=sel.getAttribute('data-spool-slot');
var val=sel.value;
if(val) _slotSpoolMap[String(idx)]=parseInt(val);
else delete _slotSpoolMap[String(idx)];
var mapping={};
Object.keys(_slotSpoolMap).forEach(function(k){mapping[k]=_slotSpoolMap[k];});
fetch(_apiUrl('/kx/spoolman/active-spool'),{method:'POST',
headers:{'Content-Type':'application/json'},body:JSON.stringify({slot_spools:mapping})});
}
function saveSpoolmanSlots(){
var mapping={};
document.querySelectorAll('#spoolman-slot-rows select[data-spool-slot]').forEach(function(sel){
var idx=sel.getAttribute('data-spool-slot');
var val=sel.value;
if(val)mapping[idx]=parseInt(val);
});
fetch(_apiUrl('/kx/spoolman/active-spool'),{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({slot_spools:mapping})}).then(function(r){return r.json();}).then(function(d){
_slotSpoolMap=d.slot_spools||{};
});
}
function saveVisibleVendors(){
var vendors=Object.keys(_vendorChecklistSel);
fetch(_apiUrl('/kx/filament/visible_vendors'),{method:'POST',headers:{'Content-Type':'application/json'},
@@ -1419,6 +1499,110 @@ function _fillSlotProfileDropdown(material, currentVendor, currentName){
});
});
}
// ── Pickr color picker ──────────────────────────────────────────────────────
var _pickr=null;
function _initPickr(hex){
// destroy previous instance if exists
if(_pickr){ try{ _pickr.destroyAndRemove(); }catch(e){} _pickr=null; }
var anchor=document.getElementById('slot-pickr-anchor');
if(!anchor||typeof Pickr==='undefined') return;
// fresh button element so Pickr can mount
anchor.innerHTML='<div id="slot-pickr-btn"></div>';
_pickr=Pickr.create({
el:'#slot-pickr-btn',
theme:'nano',
default: hex||'#808080',
inline: true,
showAlways: true,
components:{
preview:true, opacity:false, hue:true,
interaction:{ hex:true, rgba:false, input:true, save:false, clear:false }
}
});
_pickr.on('change',function(color){
var h=color.toHEXA().toString().slice(0,7);
document.getElementById('slot-edit-color').value=h;
document.getElementById('slot-edit-preview').style.background=h;
});
// Theme anpassen: Pickr benutzt eigene CSS-Variablen, wir überschreiben via style
requestAnimationFrame(function(){
var el=anchor.querySelector('.pickr');
if(el) el.style.cssText='width:100%';
var app=anchor.querySelector('.pcr-app');
if(app){
app.style.cssText='position:relative;width:100%;box-shadow:none;background:transparent';
var btn=app.querySelector('.pcr-result');
if(btn) btn.style.cssText='background:var(--raised);border:1px solid var(--border);color:var(--txt);border-radius:6px;font-size:12px';
}
});
}
// ── Color swatches (localStorage, max 16) ──────────────────────────────────
var _SWATCH_KEY='kxb_color_swatches';
var _SWATCH_MAX=16;
function _loadSwatches(){
try{ return JSON.parse(localStorage.getItem(_SWATCH_KEY)||'[]'); }catch(e){ return []; }
}
function _saveSwatches(arr){ try{ localStorage.setItem(_SWATCH_KEY, JSON.stringify(arr)); }catch(e){} }
function _addSwatch(hex){
var arr=_loadSwatches().filter(function(c){ return c.toLowerCase()!==hex.toLowerCase(); });
arr.unshift(hex);
if(arr.length>_SWATCH_MAX) arr=arr.slice(0,_SWATCH_MAX);
_saveSwatches(arr);
}
function _renderSwatches(){
var el=document.getElementById('slot-color-swatches');
if(!el) return;
var arr=_loadSwatches();
if(!arr.length){ el.style.display='none'; return; }
el.style.display='flex';
el.innerHTML=arr.map(function(c){
return '<div title="'+c+'" onclick="slotPickSwatch(\''+c+'\')" style="width:22px;height:22px;border-radius:4px;background:'+c+
';border:2px solid rgba(255,255,255,.2);cursor:pointer;flex-shrink:0"></div>';
}).join('');
}
function slotPickSwatch(hex){
if(_pickr){ _pickr.setColor(hex); }
var ci=document.getElementById('slot-edit-color');
if(ci) ci.value=hex;
document.getElementById('slot-edit-preview').style.background=hex;
}
// ── Copy color from other slot ──────────────────────────────────────────────
function _renderCopyFromSlot(currentGlobalIdx){
var slots=(window._amsSlots||[]).filter(function(s){
return s.global_index!==currentGlobalIdx && s.status==5 && Array.isArray(s.color);
});
var row=document.getElementById('slot-copy-row');
var sel=document.getElementById('slot-copy-select');
if(!row||!sel) return;
if(!slots.length){ row.style.display='none'; return; }
row.style.display='';
var ph=document.getElementById('lbl-slot-copy-from');
var phTxt=ph?ph.textContent:(T.slot_copy_from||'Copy color from slot…');
sel.innerHTML='<option value="">'+phTxt+'</option>'+slots.map(function(s){
var rgb=s.color;
var hex='#'+rgb.map(function(v){return('0'+Math.min(255,v).toString(16)).slice(-2)}).join('');
return '<option value="'+hex+'">Slot '+(s.global_index+1)+' — '+(s.type||'?')+' '+hex+'</option>';
}).join('');
}
function slotCopyColor(sel){
if(!sel.value) return;
var ci=document.getElementById('slot-edit-color');
if(!ci) return;
ci.value=sel.value;
document.getElementById('slot-edit-preview').style.background=sel.value;
sel.selectedIndex=0;
}
// ───────────────────────────────────────────────────────────────────────────
function openSlotEdit(i){
var slot=(window._amsSlots||[])[i]||{};
var globalIdx=slot.global_index!=null?slot.global_index:(slot.index!=null?slot.index:i);
@@ -1430,6 +1614,9 @@ function openSlotEdit(i){
var ci=document.getElementById('slot-edit-color');
ci.value=hex;
document.getElementById('slot-edit-preview').style.background=hex;
_initPickr(hex);
_renderSwatches();
_renderCopyFromSlot(globalIdx);
var mat=(slot.type||'PLA').toUpperCase();
document.getElementById('slot-edit-mat').value=mat;
var btns=document.getElementById('slot-mat-btns');
@@ -1554,6 +1741,7 @@ function hexToRgb(hex){
}
function saveSlotEdit(){
var hex=document.getElementById('slot-edit-color').value;
_addSwatch(hex);
var mat=document.getElementById('slot-edit-mat').value.trim().toUpperCase()||'PLA';
var color=hexToRgb(hex);
var slotIdx=_slotEditIndex;
@@ -1791,7 +1979,15 @@ function setStep(btn,v){
currentStep=v;
document.querySelectorAll('.step-btn').forEach(b=>b.classList.remove('active'));
btn.classList.add('active');
document.getElementById('step-display').textContent=v;
var ci=document.getElementById('step-custom');
if(ci)ci.value='';
}
function setStepCustom(inp){
var v=parseFloat(inp.value);
if(!isNaN(v)&&v>0){
currentStep=v;
document.querySelectorAll('.step-btn').forEach(b=>b.classList.remove('active'));
}
}
function move(axis,dir,dist){
// axis: 0=X,1=Y,2=Z → printer axis codes: 1=X,2=Y,3=Z
@@ -2486,7 +2682,20 @@ function confirmStoreWebVerify(){
});
}
function startReadyFileWithSlots(filename,_autoOpen){
function _showMismatchWarn(mismatches){
var el=document.getElementById('fd-mismatch-warn');
if(!el)return;
if(!mismatches||!mismatches.length){el.style.display='none';el.innerHTML='';return;}
var lines=mismatches.map(function(m){
var slot='Slot '+(m.slot_index+1);
if(m.reason==='empty')
return '⚠ '+slot+': GCode needs '+m.gcode_material+' — slot is empty';
return '⚠ '+slot+': GCode needs '+m.gcode_material+', loaded: '+(m.ams_material||'?');
});
el.innerHTML='<strong>Filament mismatch detected</strong><br>'+lines.join('<br>');
el.style.display='';
}
function startReadyFileWithSlots(filename,_autoOpen,_mismatch){
if(!_autoOpen) _fdAutoOpenedFile=null; // manueller Aufruf → Auto-Open-Sperre aufheben
var fn=filename||S.file_ready;
var currentFile=(storeFiles||[]).find(function(f){return f.filename===fn;});
@@ -2506,9 +2715,11 @@ function startReadyFileWithSlots(filename,_autoOpen){
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(d){
if(_autoOpenFile && _fdUserCancelled){_fdDialogOpen=false;return;}
openFilamentDialog(d.result||[]);
_showMismatchWarn(_mismatch||null);
}).catch(function(){
if(_autoOpenFile && _fdUserCancelled){_fdDialogOpen=false;return;}
openFilamentDialog([]);
_showMismatchWarn(_mismatch||null);
});
}
@@ -2744,6 +2955,7 @@ function openFilamentDialog(slots){
function closeFilamentDialog(){
var dlg=document.getElementById('filament-dialog');
if(dlg)dlg.classList.remove('open');
_showMismatchWarn(null);
_fdDialogOpen=false;
if(_fdAutoOpenedFile){
_fdUserCancelled=true;
@@ -3142,7 +3354,7 @@ function loadPrinterTab(){
'<div style="font-size:12px;color:var(--txt2);display:flex;gap:12px">'+
'<span>🌡 '+nt+'°C</span><span>🛏 '+bt+'°C</span>'+
'</div>'+
(!isActive?'<a href="/printer'+printerNum+'" style="display:block;text-align:center;padding:7px;background:var(--accent);color:#fff;border-radius:7px;font-size:13px;font-weight:600;text-decoration:none;margin-top:4px">'+T.printers_switch+'</a>':'<div style="text-align:center;padding:7px;font-size:12px;color:var(--txt2)">'+T.printers_current+'</div>')+
(!isActive?'<a href="'+url+'/printer'+printerNum+'" style="display:block;text-align:center;padding:7px;background:var(--accent);color:#fff;border-radius:7px;font-size:13px;font-weight:600;text-decoration:none;margin-top:4px">'+T.printers_switch+'</a>':'<div style="text-align:center;padding:7px;font-size:12px;color:var(--txt2)">'+T.printers_current+'</div>')+
'</div>';
}).join('');
});

View File

@@ -5,6 +5,8 @@
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>KX-Bridge</title>
<link rel="stylesheet" href="/kx/ui/style.css">
<link rel="stylesheet" href="/kx/ui/lib/pickr-nano.min.css">
<script src="/kx/ui/lib/pickr.min.js"></script>
<body>
<div id="conn-error-banner" style="display:none;background:#c0392b;color:#fff;padding:10px 18px;font-size:14px;text-align:center;position:sticky;top:0;z-index:999;"></div>
@@ -32,17 +34,6 @@
<span id="h-version" style="font-size:11px;opacity:.5;margin-left:6px"></span>
<div class="hbadge" id="h-badge"><span class="dot"></span><span id="h-state">Standby</span></div>
<button class="theme-btn" onclick="toggleTheme()">☀ / ☾</button>
<div style="display:flex;align-items:center;gap:6px">
<span aria-hidden="true" style="font-size:15px;line-height:1;opacity:.85">🌐</span>
<select class="theme-btn" id="lang-select" onchange="setLanguageFromSelect()" style="padding:6px 10px">
<option value="de">Deutsch</option>
<option value="en">English</option>
<option value="es">Espanol</option>
<option value="fr">Français</option>
<option value="it">Italiano</option>
<option value="zh-cn">中文(简体)</option>
</select>
</div>
<button class="theme-btn" onclick="showPanel('settings')" id="settings-btn" title="Einstellungen"></button>
<button class="conn-btn disconnected" id="conn-btn" onclick="toggleConnection()">⚡ Verbinden</button>
</header>
@@ -57,15 +48,26 @@
<span class="modal-title" id="slot-edit-title"></span>
<button onclick="closeSlotEdit()" style="background:none;border:none;color:var(--txt2);font-size:20px;cursor:pointer;line-height:1"></button>
</div>
<div style="display:flex;align-items:center;gap:16px;margin-bottom:20px">
<div id="slot-edit-preview" style="width:56px;height:56px;border-radius:50%;border:3px solid rgba(255,255,255,.2);flex-shrink:0"></div>
<div style="flex:1">
<div style="font-size:11px;color:var(--txt2);margin-bottom:4px" id="lbl-slot-color"></div>
<input type="color" id="slot-edit-color"
oninput="document.getElementById('slot-edit-preview').style.background=this.value"
style="width:100%;height:36px;border:1px solid var(--border);border-radius:6px;background:var(--raised);cursor:pointer;padding:2px">
<div style="display:flex;align-items:flex-start;gap:16px;margin-bottom:12px">
<div id="slot-edit-preview" style="width:56px;height:56px;border-radius:50%;border:3px solid rgba(255,255,255,.2);flex-shrink:0;margin-top:4px"></div>
<div style="flex:1;min-width:0">
<div style="font-size:11px;color:var(--txt2);margin-bottom:6px" id="lbl-slot-color"></div>
<!-- Pickr anchor — JS mounts the picker here -->
<div id="slot-pickr-anchor"></div>
<!-- hidden input keeps the hex value for saveSlotEdit() -->
<input type="hidden" id="slot-edit-color">
</div>
</div>
<!-- Recent color swatches (max 16, localStorage) -->
<div id="slot-color-swatches" style="display:flex;gap:5px;flex-wrap:wrap;margin-bottom:8px"></div>
<!-- Copy from slot -->
<div id="slot-copy-row" style="display:none;margin-bottom:16px">
<select id="slot-copy-select"
style="width:100%;padding:5px 8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:12px;box-sizing:border-box"
onchange="slotCopyColor(this)">
<option value="" id="lbl-slot-copy-from">Copy color from slot…</option>
</select>
</div>
<div style="margin-bottom:20px">
<div style="font-size:11px;color:var(--txt2);margin-bottom:6px" id="lbl-slot-material"></div>
<div style="display:flex;flex-wrap:wrap;gap:6px" id="slot-mat-btns">
@@ -252,39 +254,51 @@
<!-- Achsensteuerung -->
<div class="card">
<div class="card-title"><span></span> <span id="ptitle-motion-xy">XY-Achsen</span></div>
<div class="joypad">
<div></div>
<button class="joy" onclick="move(1,1,getStep())" title="Y+"></button>
<div></div>
<button class="joy" onclick="move(0,-1,getStep())" title="X"></button>
<button class="joy home" onclick="homeAll()" title="Home All"></button>
<button class="joy" onclick="move(0,1,getStep())" title="X+"></button>
<div></div>
<button class="joy" onclick="move(1,-1,getStep())" title="Y"></button>
<div></div>
<div class="card-title"><span></span> <span id="ptitle-motion-xy">Achsensteuerung</span></div>
<div style="display:flex;gap:16px;align-items:flex-start;flex-wrap:wrap">
<!-- XY -->
<div style="display:flex;flex-direction:column;align-items:center;gap:6px">
<div class="joypad">
<div></div>
<button class="joy" onclick="move(1,1,getStep())" title="Y+"></button>
<div></div>
<button class="joy" onclick="move(0,-1,getStep())" title="X"></button>
<div></div>
<button class="joy" onclick="move(0,1,getStep())" title="X+"></button>
<div></div>
<button class="joy" onclick="move(1,-1,getStep())" title="Y"></button>
<div></div>
</div>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt);width:100%" onclick="homeXY()"><span class="lbl-home-xy">Home XY</span></button>
</div>
<!-- Z -->
<div style="display:flex;flex-direction:column;align-items:center;gap:6px">
<div class="joypad" style="grid-template-columns:52px;grid-template-rows:repeat(2,52px)">
<button class="joy" onclick="move(2,1,getStep())" title="Z+"></button>
<button class="joy" onclick="move(2,-1,getStep())" title="Z"></button>
</div>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt);width:100%" onclick="homeZ()"><span class="lbl-home-z">Home Z</span></button>
</div>
</div>
<div class="step-btns">
<button class="step-btn" onclick="setStep(this,0.1)">0.1</button>
<button class="step-btn active" onclick="setStep(this,1)">1</button>
<button class="step-btn" onclick="setStep(this,5)">5</button>
<button class="step-btn" onclick="setStep(this,10)">10 mm</button>
<!-- Einheitliche Step-Size -->
<div style="display:flex;align-items:center;gap:6px;margin-top:10px;flex-wrap:wrap">
<div class="step-btns" style="margin-top:0">
<button class="step-btn" onclick="setStep(this,0.1)">0.1</button>
<button class="step-btn active" onclick="setStep(this,1)">1</button>
<button class="step-btn" onclick="setStep(this,5)">5</button>
<button class="step-btn" onclick="setStep(this,10)">10</button>
</div>
<input id="step-custom" type="number" min="0.1" max="260" step="0.1"
placeholder="mm"
style="width:60px;padding:4px 6px;font-size:12px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt);text-align:center"
oninput="setStepCustom(this)">
</div>
<div class="home-btns">
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="homeZ()"><span class="lbl-home-z">Home Z</span></button>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="homeXY()"><span class="lbl-home-xy">Home XY</span></button>
<!-- Globale Befehle zentriert -->
<div style="display:flex;justify-content:center;gap:8px;margin-top:10px">
<button class="btn btn-sm btn-accent" onclick="homeAll()"><span class="lbl-home-all">Home All</span></button>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="disableMotors()"><span class="lbl-disable-motors">Motors Off</span></button>
</div>
</div>
<div class="card">
<div class="card-title"><span></span> <span id="ptitle-motion-z">Z-Achse</span></div>
<div class="joypad" style="grid-template-columns:52px;grid-template-rows:repeat(2,52px)">
<button class="joy" onclick="move(2,1,getStep())" title="Z+"></button>
<button class="joy" onclick="move(2,-1,getStep())" title="Z"></button>
</div>
<div style="text-align:center;margin-top:8px;font-size:12px;color:var(--txt2)"><span class="lbl-step">Schrittweite:</span> <span id="step-display">1</span> mm</div>
</div>
<!-- Print Speed -->
<div class="card">
@@ -570,6 +584,12 @@
<div id="visible-vendors-list" style="max-height:260px;overflow-y:auto;border:1px solid var(--border);border-radius:6px;padding:8px"></div>
<button class="btn btn-sm" style="background:var(--accent);color:#fff;margin-top:8px" onclick="saveVisibleVendors()"><span id="lbl-visible-vendors-save">Auswahl speichern</span></button>
</div>
<div class="card" id="spoolman-slot-card" style="display:none">
<div class="card-title"><span>🧵</span> <span id="lbl-spoolman-slot-assign">Spoolman — Slot-Zuordnung</span></div>
<div style="font-size:11px;color:var(--txt2);margin-bottom:10px" id="lbl-spoolman-slot-hint">Spoolman-Spool pro AMS-Slot zuweisen. Der Verbrauch wird beim Druck automatisch gemeldet.</div>
<div id="spoolman-slot-rows" style="display:flex;flex-direction:column;gap:8px"></div>
<button class="btn btn-sm" style="background:var(--accent);color:#fff;margin-top:10px" onclick="saveSpoolmanSlots()"><span id="lbl-spoolman-slot-save">Zuordnung speichern</span></button>
</div>
</div>
<!-- Integrationen -->
@@ -582,7 +602,7 @@
<input type="text" id="s-spoolman-url" placeholder="http://spoolman:7912" style="width:200px">
</div>
<div class="set-row">
<label id="lbl-spoolman-sync-rate">Sync-Rate (s, 0=aus)</label>
<label id="lbl-spoolman-sync-rate">Sync-Rate (s, 0=Druckende)</label>
<input type="number" id="s-spoolman-sync-rate" min="0" max="3600" value="30" style="width:80px">
</div>
<div id="spoolman-status-row" style="margin-top:6px;font-size:12px;color:var(--txt2)">
@@ -655,6 +675,7 @@
<button onclick="closeFilamentDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)"></button>
</div>
<p id="fd-slots-hint" style="font-size:12px;color:var(--txt2);margin-bottom:10px">GCode-Kanal → AMS-Slot zuweisen:</p>
<div id="fd-mismatch-warn" style="display:none;margin-bottom:10px;padding:8px 10px;background:rgba(255,160,0,.12);border:1px solid rgba(255,160,0,.4);border-radius:8px;font-size:11px;color:#ffa000"></div>
<div id="fd-slots" style="display:flex;flex-direction:column;gap:8px;margin-bottom:16px"></div>
<div id="fd-objects-section" style="display:none;margin-bottom:16px">
<button type="button" id="fd-objects-toggle" onclick="toggleFdObjects()"

File diff suppressed because one or more lines are too long

3
web/themes/default/lib/pickr.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -15,10 +15,46 @@
body{background:var(--bg);color:var(--txt);font-family:var(--font);font-size:14px;min-height:100vh;display:flex;flex-direction:column}
a{color:var(--accent);text-decoration:none}
/* select/option-Farben explizit setzen — OrcaSlicers Device-Tab-Webview erbt
sie sonst nicht und rendert weiße Schrift auf weißem Grund (Issue #29). */
select{background:var(--raised)!important;color:var(--txt)!important}
sie sonst nicht und rendert weiße Schrift auf weißem Grund (Issue #29).
Einheitliches Styling für alle Dropdowns im gesamten UI. */
select{
background:var(--raised)!important;
color:var(--txt)!important;
border:1px solid var(--border)!important;
border-radius:8px!important;
padding:6px 10px!important;
font-size:13px!important;
appearance:none!important;
-webkit-appearance:none!important;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%23888' d='M6 8L0 0h12z'/%3E%3C/svg%3E")!important;
background-repeat:no-repeat!important;
background-position:right 10px center!important;
padding-right:28px!important;
cursor:pointer!important;
outline:none!important;
box-sizing:border-box!important;
}
select:focus{border-color:var(--accent)!important;box-shadow:0 0 0 2px rgba(0,200,255,0.18)!important}
select option{background:var(--card)!important;color:var(--txt)!important}
/* Einheitliches Styling für Text/Number-Inputs */
input[type=text],input[type=number],input[type=url],input[type=password],input[type=email],input[type=search]{
background:var(--raised);
color:var(--txt);
border:1px solid var(--border);
border-radius:8px;
padding:6px 10px;
font-size:13px;
outline:none;
box-sizing:border-box;
}
input[type=text]:focus,input[type=number]:focus,input[type=url]:focus,
input[type=password]:focus,input[type=email]:focus,input[type=search]:focus{
border-color:var(--accent);
box-shadow:0 0 0 2px rgba(0,200,255,0.18);
}
input::placeholder{color:var(--txt2);opacity:1}
/* ── HEADER ── */
header{background:var(--card);border-bottom:1px solid var(--border);
display:flex;align-items:center;gap:12px;padding:0 20px;height:52px;
@@ -265,6 +301,8 @@ canvas.tchart{width:100%;height:60px;display:block;border-radius:6px;background:
.modal-field input{background:var(--raised);border:1px solid var(--border);
border-radius:7px;color:var(--txt);padding:7px 10px;font-size:13px;width:100%}
.modal-field input:focus{outline:none;border-color:var(--accent)}
.set-row{display:flex;flex-direction:column;gap:4px;margin-bottom:10px}
.set-row label{font-size:12px;color:var(--txt2)}
.poll-btns{display:flex;gap:8px}
.poll-btn{flex:1;padding:7px;background:var(--raised);border:1px solid var(--border);
border-radius:7px;color:var(--txt2);cursor:pointer;font-size:13px;transition:all .15s}

View File

@@ -123,7 +123,7 @@
"lbl_light": "💡 Licht",
"lbl_remaining": "Restzeit:",
"lbl_slicer_time": "Slicer-Schätzung:",
"lbl_spoolman_sync_rate": "Sync-Rate (s, 0=aus)",
"lbl_spoolman_sync_rate": "Sync-Rate (s, 0=Druckende)",
"lbl_spoolman_url": "Server-URL",
"lbl_unload": "Ausziehen",
"lbl_zpos": "Z (mm)",
@@ -189,8 +189,7 @@
"panel_extras_camera": "Kamera",
"panel_extras_fan": "Lüfter",
"panel_extras_light": "Licht",
"panel_motion_xy": "XY-Achsen",
"panel_motion_z": "Z-Achse",
"panel_motion_xy": "Achsensteuerung",
"panel_print_btn_cancel": "✕ Abbrechen",
"panel_print_btn_pause": "⏸ Pause",
"panel_print_btn_resume": "▶ Fortsetzen",
@@ -325,5 +324,6 @@
"update_docker_copied": "Kopiert! Ausführen: docker compose pull && docker compose up -d",
"update_error": "Fehler",
"update_none": "Bereits aktuell",
"update_restarting": "Starte neu..."
}
"update_restarting": "Starte neu...",
"slot_copy_from": "Farbe von Slot kopieren…"
}

View File

@@ -123,7 +123,7 @@
"lbl_light": "💡 Light",
"lbl_remaining": "Remaining:",
"lbl_slicer_time": "Slicer estimate:",
"lbl_spoolman_sync_rate": "Sync rate (s, 0=off)",
"lbl_spoolman_sync_rate": "Sync rate (s, 0=end of print)",
"lbl_spoolman_url": "Server URL",
"lbl_unload": "Unload",
"lbl_zpos": "Z (mm)",
@@ -189,8 +189,7 @@
"panel_extras_camera": "Camera",
"panel_extras_fan": "Fan",
"panel_extras_light": "Light",
"panel_motion_xy": "XY Axes",
"panel_motion_z": "Z Axis",
"panel_motion_xy": "Axes Control",
"panel_print_btn_cancel": "✕ Cancel",
"panel_print_btn_pause": "⏸ Pause",
"panel_print_btn_resume": "▶ Resume",
@@ -325,5 +324,6 @@
"update_docker_copied": "Copied! Run: docker compose pull && docker compose up -d",
"update_error": "Error",
"update_none": "Already up to date",
"update_restarting": "Restarting..."
}
"update_restarting": "Restarting...",
"slot_copy_from": "Copy color from slot…"
}

View File

@@ -123,7 +123,7 @@
"lbl_light": "💡 Luz",
"lbl_remaining": "Restante:",
"lbl_slicer_time": "Estimación del slicer:",
"lbl_spoolman_sync_rate": "Tasa de sincronización (s, 0=desact.)",
"lbl_spoolman_sync_rate": "Tasa de sincronización (s, 0=fin impresión)",
"lbl_spoolman_url": "URL del servidor",
"lbl_unload": "Descargar",
"lbl_zpos": "Z (mm)",
@@ -189,8 +189,7 @@
"panel_extras_camera": "Cámara",
"panel_extras_fan": "Ventilador",
"panel_extras_light": "Luz",
"panel_motion_xy": "Ejes XY",
"panel_motion_z": "Eje Z",
"panel_motion_xy": "Control de Ejes",
"panel_print_btn_cancel": "✕ Cancelar",
"panel_print_btn_pause": "⏸ Pausa",
"panel_print_btn_resume": "▶ Reanudar",
@@ -325,5 +324,6 @@
"update_docker_copied": "Copiado. Ejecutar: docker compose pull && docker compose up -d",
"update_error": "Error",
"update_none": "Ya actualizado",
"update_restarting": "Reiniciando..."
}
"update_restarting": "Reiniciando...",
"slot_copy_from": "Copiar color del slot…"
}

View File

@@ -123,7 +123,7 @@
"lbl_light": "💡 Lumière",
"lbl_remaining": "Restant :",
"lbl_slicer_time": "Estimation slicer :",
"lbl_spoolman_sync_rate": "Taux de sync. (s, 0=désact.)",
"lbl_spoolman_sync_rate": "Taux de sync. (s, 0=fin impression)",
"lbl_spoolman_url": "URL du serveur",
"lbl_unload": "Décharger",
"lbl_zpos": "Z (mm)",
@@ -189,8 +189,7 @@
"panel_extras_camera": "Caméra",
"panel_extras_fan": "Ventilateur",
"panel_extras_light": "Lumière",
"panel_motion_xy": "Axes XY",
"panel_motion_z": "Axe Z",
"panel_motion_xy": "Contrôle des Axes",
"panel_print_btn_cancel": "✕ Annuler",
"panel_print_btn_pause": "⏸ Pause",
"panel_print_btn_resume": "▶ Reprendre",
@@ -325,5 +324,6 @@
"update_docker_copied": "Copié ! Exécuter : docker compose pull && docker compose up -d",
"update_error": "Erreur",
"update_none": "Déjà à jour",
"update_restarting": "Redémarrage…"
}
"update_restarting": "Redémarrage…",
"slot_copy_from": "Copier la couleur du slot…"
}

View File

@@ -123,7 +123,7 @@
"lbl_light": "💡 Luce",
"lbl_remaining": "Rimanente:",
"lbl_slicer_time": "Stima slicer:",
"lbl_spoolman_sync_rate": "Frequenza sync (s, 0=disatt.)",
"lbl_spoolman_sync_rate": "Frequenza sync (s, 0=fine stampa)",
"lbl_spoolman_url": "URL server",
"lbl_unload": "Rimuovi",
"lbl_zpos": "Z (mm)",
@@ -189,8 +189,7 @@
"panel_extras_camera": "Camera",
"panel_extras_fan": "Ventola",
"panel_extras_light": "Luce",
"panel_motion_xy": "Assi XY",
"panel_motion_z": "Asse Z",
"panel_motion_xy": "Controllo Assi",
"panel_print_btn_cancel": "✕ Annulla",
"panel_print_btn_pause": "⏸ Pausa",
"panel_print_btn_resume": "▶ Riprendi",
@@ -325,5 +324,6 @@
"update_docker_copied": "Copiato! Eseguire: docker compose pull && docker compose up -d",
"update_error": "Errore",
"update_none": "Già aggiornato",
"update_restarting": "Riavvio in corso..."
}
"update_restarting": "Riavvio in corso...",
"slot_copy_from": "Copia colore dallo slot…"
}

View File

@@ -123,7 +123,7 @@
"lbl_light": "💡 灯光",
"lbl_remaining": "剩余时间:",
"lbl_slicer_time": "切片预估:",
"lbl_spoolman_sync_rate": "同步频率0=关闭",
"lbl_spoolman_sync_rate": "同步频率0=打印结束",
"lbl_spoolman_url": "服务器地址",
"lbl_unload": "退料",
"lbl_zpos": "Z (mm)",
@@ -189,8 +189,7 @@
"panel_extras_camera": "相机",
"panel_extras_fan": "风扇",
"panel_extras_light": "灯光",
"panel_motion_xy": "XY 轴",
"panel_motion_z": "Z 轴",
"panel_motion_xy": "轴控制",
"panel_print_btn_cancel": "✕ 取消",
"panel_print_btn_pause": "⏸ 暂停",
"panel_print_btn_resume": "▶ 继续",
@@ -325,5 +324,6 @@
"update_docker_copied": "已复制执行docker compose pull && docker compose up -d",
"update_error": "错误",
"update_none": "已是最新版本",
"update_restarting": "重启中..."
}
"update_restarting": "重启中...",
"slot_copy_from": "从插槽复制颜色…"
}