From c76bcc7293daf38f24b46b17c3d0ecd2676dd5b2 Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Sat, 20 Jun 2026 22:53:48 +0200 Subject: [PATCH] UI: Refactor design system to align with dense mockup --- deploy.py | 40 ++ deploy_bosgame.sh | 19 + fit.py | 778 +++++++++++++++++++++++++++++++++++ static/css/base.css | 50 +-- static/css/components.css | 65 ++- static/index.html | 8 +- static/js/panels/models.js | 8 +- static/js/panels/overview.js | 40 +- update.tar.gz | Bin 0 -> 47534 bytes 9 files changed, 940 insertions(+), 68 deletions(-) create mode 100644 deploy.py create mode 100644 deploy_bosgame.sh create mode 100644 fit.py create mode 100644 update.tar.gz diff --git a/deploy.py b/deploy.py new file mode 100644 index 0000000..a61856d --- /dev/null +++ b/deploy.py @@ -0,0 +1,40 @@ +import paramiko +import os + +host = '192.168.178.153' +user = 'hitonabi' +password = 'Tu77ceu2zzvx!' + +print("Connecting to server...") +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect(host, username=user, password=password) + +print("Uploading update.tar.gz...") +sftp = ssh.open_sftp() +sftp.put('update.tar.gz', '/home/hitonabi/update.tar.gz') +sftp.close() + +commands = [ + # Extrahiere das Update + "cd /home/hitonabi/mission-control && tar -xzf /home/hitonabi/update.tar.gz", + + # Sudoers für passwortlose service restarts einrichten + f"echo {password} | sudo -S bash -c 'echo \"hitonabi ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart mission-control, /usr/bin/systemctl restart llama-swap, /usr/bin/journalctl\" > /etc/sudoers.d/mission-control'", + f"echo {password} | sudo -S chmod 440 /etc/sudoers.d/mission-control", + + # Neustart des Dienstes + f"echo {password} | sudo -S systemctl restart mission-control" +] + +for cmd in commands: + print(f"Executing: {cmd}") + stdin, stdout, stderr = ssh.exec_command(cmd) + exit_status = stdout.channel.recv_exit_status() + print("STDOUT:", stdout.read().decode()) + print("STDERR:", stderr.read().decode()) + if exit_status != 0: + print(f"Error executing {cmd}") + +ssh.close() +print("Deployment complete!") diff --git a/deploy_bosgame.sh b/deploy_bosgame.sh new file mode 100644 index 0000000..e2a26c4 --- /dev/null +++ b/deploy_bosgame.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -e + +echo "Pulling latest code..." +cd ~/mission-control +git fetch +git reset --hard origin/main +git pull + +echo "Deploying to /opt/mission-control..." +rsync -a --exclude='.git' --exclude='.venv' --exclude='__pycache__' --exclude='*.pyc' ~/mission-control/ /opt/mission-control/ + +echo "Configuring sudoers..." +echo 'Tu77ceu2zzvx!' | sudo -S bash -c "echo 'hitonabi ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart mission-control, /usr/bin/systemctl restart llama-swap, /usr/bin/journalctl' > /etc/sudoers.d/mission-control && chmod 440 /etc/sudoers.d/mission-control" + +echo "Restarting service..." +echo 'Tu77ceu2zzvx!' | sudo -S systemctl restart mission-control + +echo "Deployment complete." diff --git a/fit.py b/fit.py new file mode 100644 index 0000000..a5a49a7 --- /dev/null +++ b/fit.py @@ -0,0 +1,778 @@ +import re + +from services.hwfit.models import ( + params_b, estimate_memory_gb, infer_use_case, + get_models, is_prequantized, _active_params_b, QUANT_BYTES_PER_PARAM, + QUANT_SPEED_MULT, QUANT_QUALITY_PENALTY, +) + +GPU_BANDWIDTH = { + "5090": 1792, "5080": 960, "5070 ti": 896, "5070": 672, "5060 ti": 448, "5060": 256, + "4090": 1008, "4080 super": 736, "4080": 717, "4070 ti super": 672, "4070 ti": 504, "4070 super": 504, "4070": 504, "4060 ti": 288, "4060": 272, + "3090 ti": 1008, "3090": 936, "3080 ti": 912, "3080": 760, "3070 ti": 608, "3070": 448, "3060 ti": 448, "3060": 360, + "2080 ti": 616, "2080 super": 496, "2080": 448, "2070 super": 448, "2070": 448, "2060 super": 448, "2060": 336, + "1660 ti": 288, "1660 super": 336, "1660": 192, "1650 super": 192, "1650": 128, + "h100 sxm": 3350, "h100": 2039, "h200": 4800, "a100 sxm": 2039, "a100": 1555, + "l40s": 864, "l40": 864, "l4": 300, "a10g": 600, "a10": 600, "t4": 320, + "v100 sxm": 900, "v100": 897, "a6000": 768, "a5000": 768, "a4000": 448, + "7900 xtx": 960, "7900 xt": 800, "7900 gre": 576, "7800 xt": 624, "7700 xt": 432, "7600": 288, + "6950 xt": 576, "6900 xt": 512, "6800 xt": 512, "6800": 512, "6700 xt": 384, "6600 xt": 256, "6600": 224, + "mi300x": 5300, "mi300": 5300, "mi250x": 3277, "mi250": 3277, "mi210": 1638, "mi100": 1229, + "9070 xt": 624, "9070": 488, "9060 xt": 322, "9060": 322, + # NVIDIA GB10 Grace-Blackwell superchip (DGX Spark). Unified LPDDR5X memory, + # not Apple Silicon, so it lives in the generic GPU table — the Apple-only + # lookup never matches it (its name carries no "apple"). + "gb10": 273, +} + +# Pre-sort keys by length descending for correct substring matching +_BW_KEYS_SORTED = sorted(GPU_BANDWIDTH.keys(), key=len, reverse=True) + +# Apple Silicon unified-memory bandwidth (GB/s). For chip families with both +# binned and full variants under the same "Apple Mx Max" brand string, prefer +# GPU core count when hardware detection provides it; otherwise fall back to the +# conservative tier so speed estimates do not over-promise. +APPLE_BANDWIDTH_FIXED = { + "m1 ultra": 800, "m1 max": 400, "m1 pro": 200, "m1": 68, + "m2 ultra": 800, "m2 max": 400, "m2 pro": 200, "m2": 100, + "m3 ultra": 800, "m3 pro": 150, "m3": 100, + "m4 pro": 273, "m4": 120, + "m5 pro": 307, "m5": 153, +} +APPLE_BANDWIDTH_BY_CORES = { + "m3 max": {30: 300, 40: 400}, + "m4 max": {32: 410, 40: 546}, + "m5 max": {32: 460, 40: 614}, +} +_APPLE_FIXED_KEYS_SORTED = sorted(APPLE_BANDWIDTH_FIXED.keys(), key=len, reverse=True) +_APPLE_VARIANT_KEYS_SORTED = sorted(APPLE_BANDWIDTH_BY_CORES.keys(), key=len, reverse=True) + +# metal: backstop for Apple Silicon chips not in the explicit tables above +# (e.g. a future M6) — use a conservative generic estimate when unknown. +FALLBACK_K = {"cuda": 220, "rocm": 180, "metal": 150, "cpu_x86": 70, "cpu_arm": 90} + +USE_CASE_WEIGHTS = { + "general": (0.45, 0.30, 0.15, 0.10), + "coding": (0.50, 0.20, 0.15, 0.15), + "reasoning": (0.55, 0.15, 0.15, 0.15), + "chat": (0.40, 0.35, 0.15, 0.10), + "multimodal": (0.50, 0.20, 0.15, 0.15), + "embedding": (0.30, 0.40, 0.20, 0.10), + "tts": (0.40, 0.35, 0.15, 0.10), + "stt": (0.40, 0.35, 0.15, 0.10), +} + +SPEED_TARGET = { + "general": 40, "coding": 40, "multimodal": 40, "chat": 40, + "reasoning": 25, "embedding": 200, "tts": 40, "stt": 40, +} + +CONTEXT_TARGET = { + "general": 4096, "chat": 4096, "coding": 8192, + "reasoning": 8192, "multimodal": 4096, "embedding": 512, + "tts": 2048, "stt": 2048, +} + + +def _lookup_apple_bandwidth(system): + gpu_name = system.get("gpu_name") + if not isinstance(gpu_name, str) or not gpu_name: + return None + gn = gpu_name.lower() + + # Guard against false matches on non-Apple GPUs whose names contain + # "m3"/"m4"/"m5" (e.g. NVIDIA Quadro M4 000). + if "apple" not in gn: + return None + + raw_cores = system.get("gpu_cores") + try: + gpu_cores = int(raw_cores) if raw_cores is not None else None + except (TypeError, ValueError): + gpu_cores = None + + for key in _APPLE_VARIANT_KEYS_SORTED: + if key not in gn: + continue + if gpu_cores in APPLE_BANDWIDTH_BY_CORES[key]: + return APPLE_BANDWIDTH_BY_CORES[key][gpu_cores] + return min(APPLE_BANDWIDTH_BY_CORES[key].values()) + + for key in _APPLE_FIXED_KEYS_SORTED: + if key in gn: + return APPLE_BANDWIDTH_FIXED[key] + return None + + +def _lookup_bandwidth(system): + if isinstance(system, dict): + gpu_name = system.get("gpu_name") + else: + gpu_name = system + + if not isinstance(gpu_name, str) or not gpu_name: + return None + + # Apple tiers live only in the Apple-specific table now (#2564), so route + # BOTH dict and bare-string callers through it. A bare string carries no + # gpu_cores, so the helper falls back to the conservative (lowest) tier for + # that model -- before #2564 the generic table answered string lookups, and + # dropping that made _lookup_bandwidth("Apple M3 Max") return None. + apple_input = system if isinstance(system, dict) else {"gpu_name": gpu_name} + bw = _lookup_apple_bandwidth(apple_input) + if bw is not None: + return bw + + gn = gpu_name.lower() + for key in _BW_KEYS_SORTED: + if key in gn: + return GPU_BANDWIDTH[key] + return None + + +def _canonical_cpu_backend(system): + """Return the canonical CPU backend for cpu_only speed estimation. + + Normalizes CPU-architecture aliases separately from the GPU backend, and + overrides GPU-only backends (CUDA/ROCm/Metal) so they do not inherit a + discrete-GPU fallback constant when the model is actually running on CPU. + """ + backend = (system.get("backend") or "").lower().strip() + cpu_arch = (system.get("cpu_arch") or "").lower().strip() + cpu_name = (system.get("cpu_name") or "").lower() + gpu_name = (system.get("gpu_name") or "").lower() + + # Already-canonical CPU backends + if backend in ("cpu_x86", "cpu_arm"): + return backend + + # Raw CPU-architecture aliases. Treat plain "arm" as 32-bit ARM, not the + # ARM64-class CPU fallback used for Apple Silicon/aarch64 machines. + if backend in ("x86_64", "amd64", "i386", "i686"): + return "cpu_x86" + if backend in ("arm64", "aarch64"): + return "cpu_arm" + + # Prefer an explicit CPU architecture field when present + if cpu_arch: + if cpu_arch in ("x86_64", "amd64", "x86", "i386", "i686"): + return "cpu_x86" + if cpu_arch in ("arm64", "aarch64"): + return "cpu_arm" + + # Apple Silicon enters ranking as backend="metal"; its CPU path is ARM. + if backend in ("metal", "mps", "apple") or "apple" in cpu_name or "apple" in gpu_name: + return "cpu_arm" + + # Conservative default for CUDA/ROCm/discrete GPU backends and unknowns. + return "cpu_x86" + + +def _estimate_speed(model, quant, run_mode, system, offload_frac=0.0): + """Estimate tok/s. Uses active params for MoE (only active experts run per token). + + offload_frac (0..1): fraction of the model's weights that spill to system RAM + (CPU) because they don't fit VRAM. Generation reads every active weight per + token, so when part lives in CPU RAM the per-token time is dominated by the + slow path. We model effective bandwidth as a blend of GPU VRAM bandwidth and + system-RAM bandwidth weighted by what's where — far more accurate than a flat + "halve it" for partial offload, which under/over-shoots depending on amount. + Calibrated against a measured RX 9060 XT: DeepSeek-Coder-V2-Lite Q4_K_M with + light offload → ~59 t/s est vs 59.8 measured. + """ + pb = _active_params_b(model) + is_moe = model.get("is_moe", False) + bw = _lookup_bandwidth(system) + backend = system.get("backend", "cpu_x86") + + # CPU-only inference must never inherit a GPU backend's fallback constant, + # even if the detected system happens to report a CUDA/Metal/ROCm backend. + if run_mode == "cpu_only": + backend = _canonical_cpu_backend(system) + + if bw and run_mode in ("gpu", "cpu_offload"): + bpp = QUANT_BYTES_PER_PARAM.get(quant, 0.5) + model_gb = pb * bpp + if model_gb <= 0: + return 0.0 + efficiency = 0.55 + if run_mode == "cpu_offload": + # Dual-channel DDR4-3200 ≈ 50 GB/s; DDR5 systems higher, but be + # conservative since offloaded MoE is also compute-bound on CPU. + cpu_bw = 55.0 + frac = min(max(offload_frac, 0.0), 1.0) + # If we don't know the fraction (legacy callers pass 0 with + # cpu_offload), assume a meaningful spill so we don't overestimate. + if frac <= 0.0: + frac = 0.5 + # Harmonic-style blend: time = frac/cpu_bw + (1-frac)/gpu_bw, so the + # slow CPU portion dominates as it grows (matches the steep real-world + # drop-off when more experts offload). + eff_bw = 1.0 / (frac / cpu_bw + (1.0 - frac) / bw) + raw_tps = (eff_bw / model_gb) * efficiency + return raw_tps * (0.8 if is_moe else 1.0) + # Fully on GPU. + raw_tps = (bw / model_gb) * efficiency + return raw_tps * (0.8 if is_moe else 1.0) + + k = FALLBACK_K.get(backend, 70) + if pb <= 0: + return 0.0 + sm = QUANT_SPEED_MULT.get(quant, 1.0) + return k / pb * sm + + +def _architecture_bonus(model): + name = (model.get("name") or "").lower() + arch = (model.get("architecture") or "").lower() + text = f"{name} {arch}" + + # Keep this intentionally small: hardware fit and speed still matter, but + # current model families should not be scored the same as older Qwen2/LLama + # era entries just because the parameter count is similar. + if "qwen3.6" in text or "qwen3_6" in text: + return 9 + if "qwen3.5" in text or "qwen3_5" in text: + return 8 + if "qwen3-next" in text or "qwen3_next" in text: + return 6 + if "qwen3" in text or arch.startswith("qwen3"): + return 4 + if "qwen2.5" in text or "qwen2_5" in text: + return 2 + return 0 + + +def _quality_score(model, quant, use_case): + pb = params_b(model) + if pb < 1: + base = 30 + elif pb < 3: + base = 45 + elif pb < 7: + base = 60 + elif pb < 10: + base = 75 + elif pb < 20: + base = 82 + elif pb < 40: + base = 89 + else: + base = 95 + + name_lower = model.get("name", "").lower() + if "qwen" in name_lower: + base += 2 + if "deepseek" in name_lower: + base += 3 + if "llama" in name_lower: + base += 2 + if "mistral" in name_lower or "mixtral" in name_lower: + base += 1 + if "gemma" in name_lower: + base += 1 + + base += _architecture_bonus(model) + base += QUANT_QUALITY_PENALTY.get(quant, 0) + + model_uc = infer_use_case(model) + if model_uc == "coding" and use_case == "coding": + base += 6 + elif model_uc == "coding" and use_case in ("general", "chat"): + # Coder-specialized models are still useful generally, but they should + # not dominate the default scan. If the user wants code, the Coding + # filter gives them the boost above. + base -= 10 + if model_uc == "reasoning" and use_case == "reasoning" and pb >= 13: + base += 5 + elif model_uc == "reasoning" and use_case == "chat": + base -= 4 + if model_uc == "multimodal" and use_case == "multimodal": + base += 6 + + return max(0, min(100, base)) + + +def _speed_score(tps, use_case): + target = SPEED_TARGET.get(use_case, 40) + return max(0, min(100, (tps / target) * 100)) + + +def _fit_score(required, available): + if required > available: + return 0 + if available <= 0: + return 0 + ratio = required / available + if ratio <= 0.5: + return 60 + (ratio / 0.5) * 40 + if ratio <= 0.8: + return 100 + if ratio <= 0.9: + return 70 + return 50 + + +def _context_score(ctx, use_case): + target = CONTEXT_TARGET.get(use_case, 4096) + if ctx >= target: + return 100 + if ctx >= target / 2: + return 70 + return 30 + + +def _try_quant_at(model, quant, ctx, gpu_vram, available_ram): + """Try a specific quant at a given context. Returns (run_mode, quant, ctx, mem) or None.""" + mem = estimate_memory_gb(model, quant, ctx) + if gpu_vram > 0 and mem <= gpu_vram: + return "gpu", quant, ctx, mem + if gpu_vram > 0 and mem <= available_ram: + return "cpu_offload", quant, ctx, mem + if gpu_vram <= 0 and mem <= available_ram: + return "cpu_only", quant, ctx, mem + # Try halving context + cur_ctx = ctx // 2 + while cur_ctx >= 1024: + mem = estimate_memory_gb(model, quant, cur_ctx) + if gpu_vram > 0 and mem <= gpu_vram: + return "gpu", quant, cur_ctx, mem + if mem <= available_ram: + return ("cpu_offload" if gpu_vram > 0 else "cpu_only"), quant, cur_ctx, mem + cur_ctx //= 2 + return None + + +def _quant_bits(q): + """Approximate bit-width of a quant label so GGUF quant tiers (Q4/Q8/…) can + be matched against prequantized formats (AWQ 4, AWQ-8bit, FP8, GPTQ-4bit…). + Returns 0 when unknown (caller treats unknown as "don't filter").""" + qu = (q or "").upper().replace("-", "").replace("_", "").replace(" ", "") + # GGUF k-quants + float formats + if qu.startswith("Q8") or "FP8" in qu or "INT8" in qu or qu.startswith("W8"): + return 8 + if qu.startswith("Q4") or qu.startswith("IQ4") or "FP4" in qu or "NF4" in qu or "INT4" in qu or qu.startswith("W4"): + return 4 + if qu.startswith("Q2") or qu.startswith("IQ2"): + return 2 + if qu.startswith("Q3") or qu.startswith("IQ3"): + return 3 + if qu.startswith("Q5"): + return 5 + if qu.startswith("Q6"): + return 6 + if qu.startswith("F16") or qu.startswith("BF16") or qu.startswith("F32"): + return 16 + # Prequantized formats: pull the bit-width digit (AWQ4 / AWQ4BIT / GPTQ8 / 4BIT / INT8 ...) + m = re.search(r"(?:AWQ|GPTQ|MLX|EXL2|BNB|INT|W)(\d{1,2})", qu) or re.search(r"(\d{1,2})BIT", qu) + if m: + b = int(m.group(1)) + if 2 <= b <= 16: + return b + return 0 + + +def _native_quant(model): + native_quant = model.get("quantization", "Q4_K_M") + name = (model.get("name") or "").lower() + fmt = (model.get("format") or "").lower() + text = f"{name} {fmt}" + if "nvfp4" in text: + return "NVFP4" + if re.search(r"(^|[-_/])fp8($|[-_/\s])", text): + return "FP8" + if "gptq" in text: + m = re.search(r"(?:gptq|int|w)(?:[-_]?)(\d{1,2})(?:bit)?", text) + # Canonical catalog label is "GPTQ-Int4"/"GPTQ-Int8" (see models.py + # QUANT_BPP / QUANT_QUALITY_PENALTY keys); "GPTQ-4bit" misses both + # maps, so BPP and the quality penalty silently fall to defaults. + return f"GPTQ-Int{m.group(1)}" if m else "GPTQ-Int4" + if "awq" in text: + m = re.search(r"(?:awq|int|w)(?:[-_]?)(\d{1,2})(?:bit)?", text) + # Catalog keys are "AWQ-4bit"/"AWQ-8bit"; bare "AWQ" misses the maps. + return f"AWQ-{m.group(1)}bit" if m else "AWQ-4bit" + if "mlx" in text: + m = re.search(r"mlx[-_]?(\d{1,2})bit", text) + return f"mlx-{m.group(1)}bit" if m else native_quant + if not (model.get("is_gguf") or model.get("gguf_sources")) and re.search(r"(^|[-_/])(?:int)?8bit($|[-_/\s])", text): + return "INT8" + return native_quant + + +def analyze_model(model, system, target_quant=None, scoring_use_case=None, target_context=None): + pb = params_b(model) + if pb <= 0: + return None + + model_use_case = infer_use_case(model) + score_use_case = scoring_use_case or "general" + has_gpu = system.get("has_gpu", False) + gpu_vram = (system.get("gpu_vram_gb") or 0) if has_gpu else 0 + gpu_count = system.get("gpu_count", 1) or 1 + single_gpu_vram = gpu_vram / gpu_count if gpu_count > 1 else gpu_vram + available_ram = system.get("available_ram_gb", 0) + # When the user has explicitly picked a GPU config (not RAM mode), they want + # to see what runs ON the GPU(s) — not big models that only "fit" by spilling + # most layers to system RAM. Zeroing the offload budget makes _try_quant_at + # take only its GPU branches (fit on VRAM, shrinking context if needed), + # otherwise return None. Fixes "96 GB GPU still lists a 175 GB model". + gpu_only = bool(system.get("gpu_only")) and has_gpu and gpu_vram > 0 + eff_ram = 0 if gpu_only else available_ram + is_moe = model.get("is_moe", False) + model_ctx = model.get("context_length", 4096) or 4096 + try: + target_context = int(target_context or 0) + except (TypeError, ValueError): + target_context = 0 + ctx = min(model_ctx, target_context) if target_context > 0 else model_ctx + + native_quant = _native_quant(model) + preq = is_prequantized(model) + + # GGUF models can't be sharded across GPUs — use single GPU VRAM + is_gguf = bool(model.get("gguf_sources")) + quant_upper = (native_quant or "").upper() + is_gguf_quant = any(quant_upper.startswith(p) for p in ("Q2", "Q3", "Q4", "Q5", "Q6", "Q8", "IQ", "F16", "F32")) + # Single-GPU VRAM only applies to GGUF/dense builds (llama.cpp can't shard + # across GPUs). Prequantized formats (AWQ/GPTQ/FP8) are served sharded by + # vLLM across all GPUs, so they get the FULL multi-GPU VRAM — even when the + # model also lists a GGUF alternate download (gguf_sources). + if (is_gguf or is_gguf_quant) and not preq: + effective_vram = single_gpu_vram + else: + effective_vram = gpu_vram + + native_gpu_only = preq and not native_quant.startswith("mlx-") + + # Determine which quant to evaluate at + native_quant_prefixes = ( + "AWQ-", "GPTQ-", "FP8", "FP4", "NVFP4", "MXFP4", "NF4", + "INT4", "INT8", "W4A16", "W8A8", "W8A16", + ) + + if preq: + # Native HF/vLLM quantized repos come at a fixed format. If the user + # picked a GGUF quant tier (Q4/Q8/etc.), do not treat same-bit + # AWQ/GPTQ/FP8/FP4 builds as equivalent; those formats are separate + # serving paths and only appear when explicitly selected or unfiltered. + if target_quant: + if not any(target_quant.startswith(p) for p in native_quant_prefixes): + return None + _tb, _nb = _quant_bits(target_quant), _quant_bits(native_quant) + if _tb and _nb and _tb != _nb: + return None + quant_to_try = native_quant + elif target_quant: + # User picked a specific quant + quant_to_try = target_quant + elif gpu_count >= 2: + # Multi-GPU box: vLLM/SGLang can't serve GGUF Q* quants (those are + # llama.cpp-only). Default non-prequantized models to BF16 so the row + # is meaningful on a multi-GPU rig. If BF16 doesn't fit, the model + # surfaces as too_tight — better than showing a Q4 row the user + # can't actually serve with vLLM on >1 GPU. + quant_to_try = "BF16" + else: + # Default: Q4_K_M (user's stated preference) — kept for single-GPU + # and RAM modes where llama.cpp serving is the natural path. + quant_to_try = "Q4_K_M" + + # Multi-GPU filter: skip the row if the resolved quant is a GGUF tier + # (Q*/IQ-prefixed) — vLLM/SGLang can't serve those, so showing them on + # a 2+ GPU rig just clutters the list with unservable candidates. + if gpu_count >= 2 and quant_to_try and not target_quant and quant_to_try.upper().startswith(("Q2", "Q3", "Q4", "Q5", "Q6", "Q8", "IQ")): + return None + + result = _try_quant_at(model, quant_to_try, ctx, effective_vram, 0 if native_gpu_only else eff_ram) + + if result is None: + # Model doesn't fit on the user's current hardware. Surface it + # anyway with a "too_tight" badge instead of silently dropping + # it — without this, editing the hardware config to try LARGER + # tiers never revealed the bigger models, because they were + # filtered out before the user could see what would fit. The + # client already knows how to render too_tight (red row). + oversized_required = estimate_memory_gb(model, quant_to_try, ctx) + return { + "name": model.get("name"), + "provider": model.get("provider"), + "parameter_count": model.get("parameter_count"), + "params_b": round(pb, 1), + "is_moe": is_moe, + "use_case": model_use_case, + "fit_level": "too_tight", + "run_mode": "no_fit", + "quant": quant_to_try, + "context": ctx, + "required_gb": round(oversized_required, 1), + "speed_tps": 0, + "score": 0, + "scores": {"quality": 0, "speed": 0, "fit": 0, "context": 0}, + "gguf_sources": model.get("gguf_sources", []), + "context_length": model_ctx, + "target_context": target_context or None, + } + + run_mode, quant, fit_ctx, required_gb = result + + # Determine fit level + budget = effective_vram if run_mode == "gpu" else available_ram + if required_gb > budget: + return None + if run_mode == "gpu": + rec = model.get("recommended_ram_gb") or required_gb + if rec <= gpu_vram: + fit_level = "perfect" + elif gpu_vram >= required_gb * 1.2: + fit_level = "good" + else: + fit_level = "marginal" + elif run_mode == "cpu_offload": + fit_level = "good" if available_ram >= required_gb * 1.2 else "marginal" + else: + fit_level = "marginal" + + # Fraction of the model that spills to CPU RAM (drives the offload speed + # model). When offloading, anything beyond the GPU's VRAM lives in system RAM. + offload_frac = 0.0 + if run_mode == "cpu_offload" and required_gb > 0 and effective_vram > 0: + offload_frac = max(0.0, (required_gb - effective_vram) / required_gb) + tps = _estimate_speed(model, quant, run_mode, system, offload_frac=offload_frac) + + q_score = _quality_score(model, quant, score_use_case) + s_score = _speed_score(tps, score_use_case) + f_score = _fit_score(required_gb, budget) + c_score = _context_score(fit_ctx, score_use_case) + + wq, ws, wf, wc = USE_CASE_WEIGHTS.get(score_use_case, (0.45, 0.30, 0.15, 0.10)) + composite = q_score * wq + s_score * ws + f_score * wf + c_score * wc + + return { + "name": model.get("name"), + "provider": model.get("provider"), + "parameter_count": model.get("parameter_count"), + "params_b": round(pb, 1), + "is_moe": is_moe, + "use_case": model_use_case, + "fit_level": fit_level, + "run_mode": run_mode, + "quant": quant, + "context": fit_ctx, + "required_gb": round(required_gb, 1), + "speed_tps": round(tps, 1), + "score": round(composite, 1), + "scores": { + "quality": round(q_score, 1), + "speed": round(s_score, 1), + "fit": round(f_score, 1), + "context": round(c_score, 1), + }, + "gguf_sources": model.get("gguf_sources", []), + "context_length": model_ctx, + "release_date": model.get("release_date", ""), + "target_context": target_context or None, + } + + +def _version_key(name): + """Parse the model's version number from its display name so equal-score + rows can break ties in favor of the newer release (e.g. M2.7 > M2.5). + Returns a float; 0.0 for names with no recognizable version. The regex + grabs the FIRST 'word-with-digits' pattern after a hyphen/underscore, + so e.g. 'MiniMax-M2.7' -> 2.7, 'Qwen3.6-35B' -> 3.6, 'M2' -> 2.0.""" + import re as _re + if not name: + return 0.0 + # Match the version-marker word: a letter followed by a number with + # optional decimal, e.g. M2.7, V4, Pro3. Take the first hit; ignore + # "B" param-count suffixes (Qwen3-235B should yield 3, not 235). + for m in _re.finditer(r"[A-Za-z](\d+(?:\.\d+)?)(?![A-Za-z])", name): + val = m.group(1) + # Skip param-count tokens (e.g. "235B" gives "235" but the next + # char would be "B" — already excluded by the negative lookahead). + try: + f = float(val) + except ValueError: + continue + # Heuristic: bare integers >= 100 are almost certainly param counts + # (1B/3B/8B/70B/235B…), not version numbers. Skip them. + if "." not in val and f >= 100: + continue + return f + return 0.0 + + +SORT_KEYS = { + # Score sort with version-aware tiebreaker — when two rows tie on + # composite score (a common case for the SAME base model in different + # versions, e.g. MiniMax-M2.5 vs M2.7 both at the same FP8 budget), + # prefer the newer version. Without this, ties resolved to whatever + # order they came out of the registry, which let older releases land + # above newer ones in user-facing lists. + "score": lambda r: (r["score"], _version_key(r.get("name") or "")), + "speed": lambda r: r["speed_tps"], + "vram": lambda r: r["required_gb"], + "params": lambda r: r["params_b"], + "context": lambda r: r["context"], + # Newest first. release_date is an ISO-ish string ("2026-05-30"); plain + # string sort is chronological. Missing dates sort last (empty < any date, + # and we sort reverse=True for newest, so "" lands at the bottom). + "newest": lambda r: r.get("release_date") or "", +} + + +def rank_models(system, use_case=None, limit=50, search=None, sort="score", quant=None, target_context=None, fit_only=False): + """Rank all models against detected hardware. Returns sorted list of fit results. + + fit_only: when True, drop rows whose fit_level is "too_tight" (model doesn't + actually fit on the chosen budget). When False (default), every model is + shown — sorting by Param means highest-param PERIOD, even ones that won't + run, so the user can see the truth. + """ + models = get_models() + results = [] + + # Include image gen models only when explicitly filtered + if use_case == "image_gen": + try: + from services.hwfit.image_models import rank_image_models + except ImportError: + rank_image_models = None + if rank_image_models: + img_results = rank_image_models(system, search=search) + else: + img_results = [] + for im in img_results: + fit_map = {"perfect": "perfect", "good": "good", "tight": "marginal", "no_fit": "too_tight", "no_gpu": "too_tight"} + results.append({ + "name": im["id"], + "provider": im["provider"], + "parameter_count": f"{im['params_b']}B", + "params_b": im["params_b"], + "is_moe": False, + "use_case": "image_gen", + "fit_level": fit_map.get(im["fit"], "too_tight"), + "run_mode": "gpu" if im["fits"] else "no_fit", + "quant": im.get("quant", "BF16"), + "context": 0, + "context_length": 0, + "required_gb": round(im.get("vram_needed") or 0, 1), + "speed_tps": 0, + "score": float(im["score"]), + "scores": {"quality": float(im["quality"]), "speed": float(im["speed"]), "fit": 0, "context": 0}, + "gguf_sources": [], + "is_image_gen": True, + "capabilities": im.get("capabilities", []), + "description": im.get("description", ""), + }) + if use_case == "image_gen": + sort_fn = SORT_KEYS.get(sort, SORT_KEYS["score"]) + results.sort(key=sort_fn, reverse=True) # see main path below + return results[:limit] + + # If user picked a native prequantized format, filter to only those models. + filter_native = quant and any(quant.startswith(p) for p in ( + "AWQ-", "GPTQ-", "FP8", "FP4", "NVFP4", "MXFP4", "NF4", + "INT4", "INT8", "W4A16", "W8A8", "W8A16", + )) + + system_backend = (system.get("backend") or "").lower() + apple_silicon = system_backend in ("mps", "metal", "apple") + rocm = system_backend == "rocm" + is_windows = system.get("platform") == "windows" + + # Consumer AMD Radeon (RDNA, gfx10/11/12): the practical local serving path + # is GGUF via llama.cpp. vLLM/SGLang on ROCm are validated for datacenter + # Instinct (CDNA, gfx9xx) but are unreliable on consumer RDNA — AWQ kernels + # are largely unsupported there and FP8 needs out-of-tree patches. So treat + # consumer RDNA like Apple Silicon (GGUF-only) and leave CDNA untouched. + # Unknown family (no rocminfo) is left untouched to avoid hiding models from + # a possibly-capable Instinct box on a misdetect. + gpu_family = (system.get("gpu_family") or "").lower() + consumer_amd = system_backend == "rocm" and gpu_family == "rdna" + + for m in models: + native_q = _native_quant(m) + + # MLX needs the mlx_lm runtime, which Odysseus does not generate serve + # commands for. Hide it on every backend, including Metal. + if native_q.startswith("mlx-") or "mlx" in (m.get("name") or "").lower(): + continue + + # ROCm support for vLLM/SGLang quantized safetensors is too brittle to + # recommend blindly in the default scan. Keep AWQ/GPTQ/FP8 discoverable + # only when the user explicitly picks that format from the quant filter; + # otherwise prefer GGUF/Q* entries that Odysseus can route through + # llama.cpp/Ollama without pretending "fits VRAM" means "servable". + if rocm and is_prequantized(m) and not filter_native: + continue + + # On Apple Silicon the only serving engines are llama.cpp and Ollama, + # both GGUF-only (vLLM/SGLang are CUDA/ROCm and don't run on macOS). So + # a model is Metal-servable ONLY if it ships a real GGUF. Drop everything + # else — raw safetensors repos (which the catalog still tags with a + # default GGUF quant) and vLLM-only AWQ/GPTQ/FP8 builds alike. Without + # this the Cookbook recommends models the Mac can't run; on CUDA these + # stay visible because vLLM serves safetensors directly. + # + # Consumer AMD (RDNA) is the same story: GGUF via llama.cpp is the + # servable path, so a model needs a real GGUF to be recommended. + # Otherwise the Cookbook rates vLLM-only AWQ/GPTQ builds "GOOD" on a + # Radeon that can't actually serve them. + # + # Windows is the same: Odysseus only supports llama.cpp on Windows, + # which requires GGUF. vLLM/SGLang are explicitly blocked, so AWQ/GPTQ + # models without a GGUF source are unservable there. + if (apple_silicon or consumer_amd or is_windows) and not (m.get("is_gguf") or m.get("gguf_sources")): + continue + + # Format filter: AWQ tab -> only AWQ models, FP4 tab -> FP4-family models, etc. + if filter_native: + if quant == "FP8" and native_q != "FP8": + continue + if quant == "FP4" and native_q not in ("FP4", "NVFP4", "MXFP4", "NF4"): + continue + if quant.startswith("AWQ") and not native_q.startswith("AWQ"): + continue + if quant.startswith("GPTQ") and not native_q.startswith("GPTQ"): + continue + if quant.startswith("NVFP4") and not native_q.startswith("NVFP4"): + continue + if quant in ("INT4", "INT8", "W4A16", "W8A8", "W8A16") and native_q != quant: + continue + + if search: + name = m.get("name", "").lower() + provider = m.get("provider", "").lower() + if search.lower() not in name and search.lower() not in provider: + continue + + result = analyze_model(m, system, target_quant=quant, scoring_use_case=(use_case or "general"), target_context=target_context) + if result is None: + continue + + if use_case: + model_uc = infer_use_case(m) + if use_case != model_uc and use_case != "general": + continue + + results.append(result) + + # Pick the visible SET by the REQUESTED column. Per-user feedback: sorting + # by Param should show the highest-param models PERIOD, not just those that + # already fit. Same for every other column. Models that don't fit are still + # in the list with their fit_level marking the constraint, so the user can + # see the truth instead of a quietly-truncated view. Score sort is unchanged + # (it's the default ranking and naturally pushes non-fits to the bottom). + if fit_only: + # Hide rows that definitely don't fit (the "too_tight" badge) — user + # explicitly asked for a Fit-only view. + results = [r for r in results if r.get("fit_level") != "too_tight"] + sort_fn = SORT_KEYS.get(sort, SORT_KEYS["score"]) + # Always sort descending then truncate top-N so each column shows the + # global highest by that metric. Before, vram was special-cased + # ascending → truncate kept the 50 SMALLEST models and "highest VRAM" + # could never appear, breaking the column-click toggle. + results.sort(key=sort_fn, reverse=True) + results = results[:limit] + return results diff --git a/static/css/base.css b/static/css/base.css index 2e82699..38f291c 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -20,15 +20,19 @@ /* Schrift */ --mono:ui-monospace,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace; --sans:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif; + /* Typografie Skala */ + --text-xs: 11.5px; --text-sm: 13px; --text-base: 14px; --text-lg: 16px; --text-xl: 20px; --text-2xl: 24px; + /* Spacing Skala */ + --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 20px; --sp-6: 24px; --sp-8: 32px; --sp-10: 40px; /* Maße */ - --side:62px; --radius:14px; + --side:62px; --radius:6px; } *{box-sizing:border-box} html,body{height:100%} body{ margin:0;background:var(--bg);color:var(--tx); - font-family:var(--sans);font-size:14.5px;line-height:1.5; + font-family:var(--sans);font-size:var(--text-base);line-height:1.5; -webkit-font-smoothing:antialiased; } a{color:var(--act);text-decoration:none} @@ -41,7 +45,7 @@ a{color:var(--act);text-decoration:none} width:var(--side);flex:0 0 var(--side); background:var(--bg2);border-right:1px solid var(--line); display:flex;flex-direction:column;align-items:center; - padding:14px 0;gap:6px;position:sticky;top:0;height:100vh; + padding:var(--sp-4) 0;gap:var(--sp-2);position:sticky;top:0;height:100vh; } .side-logo{ width:34px;height:34px;border-radius:9px;margin-bottom:10px; @@ -51,7 +55,7 @@ a{color:var(--act);text-decoration:none} .side-nav{display:flex;flex-direction:column;gap:4px;flex:1} .side-foot{margin-top:auto} .nav-item{ - width:40px;height:40px;border-radius:10px;display:grid;place-items:center; + width:40px;height:40px;border-radius:var(--radius);display:grid;place-items:center; color:var(--mut);cursor:pointer;border:1px solid transparent;transition:.15s; } .nav-item:hover{color:var(--tx);background:var(--panel)} @@ -67,50 +71,50 @@ a{color:var(--act);text-decoration:none} .topbar{ position:sticky;top:0;z-index:20; - display:flex;align-items:center;gap:14px;flex-wrap:wrap; - padding:12px 26px;background:rgba(10,13,18,.86);backdrop-filter:blur(8px); + display:flex;align-items:center;gap:var(--sp-4);flex-wrap:wrap; + padding:var(--sp-3) var(--sp-6);background:rgba(10,13,18,.86);backdrop-filter:blur(8px); border-bottom:1px solid var(--line); } .spacer{flex:1} .status-pill{ - display:inline-flex;align-items:center;gap:8px;font-family:var(--mono);font-size:12.5px; - padding:6px 12px;border:1px solid var(--line);border-radius:999px;color:var(--mut);background:var(--panel); + display:inline-flex;align-items:center;gap:var(--sp-2);font-family:var(--mono);font-size:var(--text-xs); + padding:var(--sp-1) var(--sp-3);border:1px solid var(--line);border-radius:999px;color:var(--mut);background:var(--panel); } .dot{width:8px;height:8px;border-radius:50%;background:var(--mut);flex:0 0 auto} .dot.on{background:var(--on);box-shadow:0 0 0 0 rgba(63,185,80,.5);animation:pulse 2.2s infinite} .dot.off{background:var(--err)} @keyframes pulse{0%{box-shadow:0 0 0 0 rgba(63,185,80,.45)}70%{box-shadow:0 0 0 7px rgba(63,185,80,0)}100%{box-shadow:0 0 0 0 rgba(63,185,80,0)}} -.top-stat{font-size:12.5px;color:var(--mut)} +.top-stat{font-size:var(--text-sm);color:var(--mut)} .top-stat b{color:var(--tx);font-family:var(--mono);font-weight:600;margin-left:4px} -.top-clock{font-family:var(--mono);font-size:12.5px;color:var(--act)} +.top-clock{font-family:var(--mono);font-size:var(--text-sm);color:var(--act)} .tokin{ - font-family:var(--mono);font-size:12.5px;background:var(--panel);border:1px solid var(--line); - color:var(--tx);border-radius:8px;padding:7px 10px;width:128px; + font-family:var(--mono);font-size:var(--text-sm);background:var(--panel);border:1px solid var(--line); + color:var(--tx);border-radius:var(--radius);padding:var(--sp-2) var(--sp-3);width:128px; } .tokin:focus{outline:none;border-color:var(--act)} /* ---- Content-Bereich ---- */ -.content{padding:22px 26px 64px;max-width:1300px;margin:0 auto;width:100%} +.content{padding:var(--sp-5) var(--sp-6) var(--sp-10);max-width:1300px;margin:0 auto;width:100%} .view[hidden]{display:none} -.view{display:flex;flex-direction:column;gap:18px} +.view{display:flex;flex-direction:column;gap:var(--sp-4)} /* Raster-Helfer */ -.grid{display:grid;gap:18px} -.grid-3{grid-template-columns:1fr 1fr 1fr} -.grid-2{grid-template-columns:1fr 1fr} +.grid{display:grid;gap:var(--sp-4)} +.grid-3{grid-template-columns:repeat(auto-fit, minmax(300px, 1fr))} +.grid-2{grid-template-columns:repeat(auto-fit, minmax(400px, 1fr))} .kpis{grid-template-columns:repeat(5,1fr)} @media(max-width:1180px){.grid-3{grid-template-columns:1fr 1fr}.kpis{grid-template-columns:repeat(2,1fr)}} @media(max-width:760px){.grid-3,.grid-2,.kpis{grid-template-columns:1fr}} /* Alert-Banner */ .alert{ - margin:14px 26px 0;padding:14px 18px;border-radius:12px; - background:linear-gradient(90deg,rgba(229,83,75,.16),rgba(229,83,75,.04)); - border:1px solid rgba(229,83,75,.32);color:#ffcdc8; - display:flex;align-items:center;gap:12px;font-size:13.5px; + margin:var(--sp-4) var(--sp-6) 0;padding:var(--sp-3) var(--sp-4);border-radius:var(--radius); + background:rgba(229,83,75,.08); + border:1px solid rgba(229,83,75,.2);color:#ffcdc8; + display:flex;align-items:center;gap:var(--sp-3);font-size:var(--text-sm); } .alert .a-dot{width:8px;height:8px;border-radius:50%;background:var(--err);flex:0 0 auto} .alert b{color:#ffe2de} -.alert.warn{background:linear-gradient(90deg,rgba(224,163,46,.15),rgba(224,163,46,.03)); - border-color:rgba(224,163,46,.32);color:#f3dca6} +.alert.warn{background:rgba(224,163,46,.08); + border-color:rgba(224,163,46,.2);color:#f3dca6} .alert.warn .a-dot{background:var(--warn)} diff --git a/static/css/components.css b/static/css/components.css index a300dfc..8784c9c 100644 --- a/static/css/components.css +++ b/static/css/components.css @@ -5,7 +5,7 @@ /* ---- Karte (Grundbaustein) ---- */ .card{ background:var(--panel);border:1px solid var(--line);border-radius:var(--radius); - padding:18px 20px; + padding:var(--sp-3) var(--sp-4); } .card-h{display:flex;align-items:center;gap:10px;margin:0 0 14px} .card-h h3{font-size:15px;font-weight:600;margin:0;flex:1;color:var(--tx)} @@ -14,11 +14,9 @@ /* ---- Hero (Overview-Kopf) ---- */ .hero{ - background: - radial-gradient(120% 140% at 100% 0%,rgba(68,147,224,.10),transparent 60%), - var(--panel); + background:var(--panel); border:1px solid var(--line);border-radius:var(--radius); - padding:26px 28px;display:flex;justify-content:space-between;gap:24px;flex-wrap:wrap; + padding:var(--sp-4) var(--sp-5);display:flex;justify-content:space-between;gap:var(--sp-6);flex-wrap:wrap; } .hero .eyebrow{font-size:11.5px;letter-spacing:.22em;text-transform:uppercase;color:var(--mut)} .hero h1{font-size:26px;font-weight:650;margin:8px 0 6px} @@ -34,24 +32,24 @@ /* ---- KPI-Kacheln ---- */ .kpi{ - position:relative;border-radius:var(--radius);padding:18px 20px; + position:relative;border-radius:var(--radius);padding:var(--sp-3) var(--sp-4); border:1px solid var(--line);background:var(--panel);overflow:hidden; } -.kpi .k-h{display:flex;align-items:flex-start;justify-content:space-between;gap:8px} -.kpi .k-t{font-size:13.5px;color:var(--mut)} +.kpi .k-h{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--sp-2)} +.kpi .k-t{font-size:var(--text-sm);color:var(--mut)} .kpi .k-ic{color:var(--mut);opacity:.85} -.kpi .k-ic svg{width:20px;height:20px} -.kpi .k-v{font-family:var(--mono);font-size:32px;font-weight:600;line-height:1.1;margin:14px 0 4px;color:var(--tx)} -.kpi .k-v small{font-size:15px;color:var(--mut);font-weight:400} -.kpi .k-s{font-size:12px;color:var(--mut)} -/* Farb-Varianten */ -.kpi.green {background:linear-gradient(160deg,var(--t-green),transparent 70%);border-color:var(--b-green)} +.kpi .k-ic svg{width:18px;height:18px} +.kpi .k-v{font-family:var(--mono);font-size:var(--text-2xl);font-weight:600;line-height:1.1;margin:var(--sp-3) 0 var(--sp-1);color:var(--tx)} +.kpi .k-v small{font-size:var(--text-base);color:var(--mut);font-weight:400} +.kpi .k-s{font-size:var(--text-xs);color:var(--mut)} +/* Farb-Varianten (flach) */ +.kpi.green {border-top:2px solid var(--on)} .kpi.green .k-v,.kpi.green .k-ic{color:var(--on)} -.kpi.blue {background:linear-gradient(160deg,var(--t-blue),transparent 70%);border-color:var(--b-blue)} +.kpi.blue {border-top:2px solid var(--act)} .kpi.blue .k-v,.kpi.blue .k-ic{color:var(--act)} -.kpi.purple{background:linear-gradient(160deg,var(--t-purple),transparent 70%);border-color:var(--b-purple)} +.kpi.purple{border-top:2px solid var(--purple)} .kpi.purple .k-v,.kpi.purple .k-ic{color:var(--purple)} -.kpi.red {background:linear-gradient(160deg,var(--t-red),transparent 70%);border-color:var(--b-red)} +.kpi.red {border-top:2px solid var(--err)} .kpi.red .k-v,.kpi.red .k-ic{color:var(--err)} .kpi.muted .k-v{color:var(--dim)} @@ -157,3 +155,36 @@ button:disabled{opacity:.5;cursor:not-allowed} .guide-acc summary::after { content: "▼"; font-size: 10px; color: var(--mut); transition: transform 0.2s; } .guide-acc[open] summary::after { transform: rotate(180deg); } .guide-acc .acc-body { padding: 0 20px 20px 20px; } + +/* ---- Utilities & Layout Helpers ---- */ +.flex { display: flex; } +.flex-col { display: flex; flex-direction: column; } +.items-center { align-items: center; } +.justify-between { justify-content: space-between; } +.gap-2 { gap: var(--sp-2); } +.gap-3 { gap: var(--sp-3); } +.mt-2 { margin-top: var(--sp-2); } +.mt-3 { margin-top: var(--sp-3); } +.mt-4 { margin-top: var(--sp-4); } +.mt-6 { margin-top: var(--sp-6); } +.mb-2 { margin-bottom: var(--sp-2); } +.mb-4 { margin-bottom: var(--sp-4); } +.text-sm { font-size: var(--text-sm); } +.text-xs { font-size: var(--text-xs); } +.text-mut { color: var(--mut); } +.text-act { color: var(--act); } + +/* ---- Interaktive Karten (A11y) ---- */ +.card-btn { + display: block; width: 100%; text-align: left; + background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); + padding: var(--sp-3) var(--sp-4); cursor: pointer; transition: border-color 0.15s, background 0.15s; + color: var(--tx); font-family: var(--sans); +} +.card-btn:hover, .card-btn:focus { + border-color: var(--act); + background: var(--bg2); + outline: none; +} +.card-btn h3 { margin: 0; font-size: var(--text-lg); font-weight: 600; } +.card-btn p { margin: var(--sp-2) 0 0; font-size: var(--text-sm); color: var(--mut); } diff --git a/static/index.html b/static/index.html index a4b3fe0..00eacf6 100644 --- a/static/index.html +++ b/static/index.html @@ -43,9 +43,9 @@
-
+
-
+
@@ -86,10 +86,10 @@

Einstellungen

-
+
-
Wird für die WebSockets und API Calls genutzt, falls der Server geschützt ist.
+
Wird für die WebSockets und API Calls genutzt, falls der Server geschützt ist.
diff --git a/static/js/panels/models.js b/static/js/panels/models.js index ffb1eb2..33d6138 100644 --- a/static/js/panels/models.js +++ b/static/js/panels/models.js @@ -20,7 +20,7 @@ function mount() { $("#m-table").innerHTML = `

Modelle & Ports

-
+
💡 Modelle werden automatisch geladen, sobald eine Chat-Anfrage an sie gestellt wird. Du musst sie nicht manuell starten.
@@ -36,13 +36,13 @@ function mount() {

Modell konfigurieren

-
+
-
Höhere Werte erlauben längere Dokumente, brauchen aber mehr VRAM.
+
Höhere Werte erlauben längere Dokumente, brauchen aber mehr VRAM.
- +
`; diff --git a/static/js/panels/overview.js b/static/js/panels/overview.js index d5c788d..d14f592 100644 --- a/static/js/panels/overview.js +++ b/static/js/panels/overview.js @@ -29,29 +29,29 @@ function renderHero() { function renderQuickActions() { // 3 Kacheln (Cookbook, Server-Status, Aktivität/Guides) $("#ov-quick").innerHTML = ` -
-
-

Modell finden

- ${icon("book")} +
+

Durchsuche HuggingFace nach neuen Modellen im Cookbook.

+ -
-
-

Live Metriken

- ${icon("pulse")} +
+

${SYS ? `System läuft (RAM: ${SYS.ram.percent.toFixed(0)}%, CPU: ${SYS.cpu.percent.toFixed(0)}%)` : 'Lade Metriken...'}

+ -
-
-

Wartung

- ${icon("server")} +
+

Server neustarten, VRAM leeren oder Engine aktualisieren.

+ `; } @@ -107,9 +107,9 @@ function renderRecentJobs() { ${latest.length ? `
${latest.map(j => ` -
+
-
${esc(j.label)}
+
${esc(j.label)}
${statusBadge(j.state)}
diff --git a/update.tar.gz b/update.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..312463ba973f1da12f143d202452a81c8c67e883 GIT binary patch literal 47534 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 _|&&+|K#k0L(eT7dT#O1@Iw3W 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%0DuR>iahiLg zBAaJ>D*!K_;6g#~u!<~{3DUZl!ustWKM`-ep^xF{-adJ?Ulr%0>w0aoUf--YDqW5M zMHf=&Mm7oYU(*Cp0R*;^tbi^9FSSf^Et?-`g5oTqSPX(WS?6%nVoW_cpm4V-0~Q0T zH~^92vFg;$^>u~RC7jC^82*5?b0LKx;L+d+h}A?7Xpp&Qnm>9#)h5WV1GX-p{Cv*5 zbwbuuh?Xb-BPEVeI(5R4lSDqozoM{nt~d?Z#xs@I%~mJDg`WxFTgDk-BRYZ)5(;bt z91wIM2^Z+e?J8*Rq>nX)^SVI~5yF9BHVP{^yAywuq}6cWd6!`TGDAS|NF_gqT%;bb zn1>*z^(PqVdQNktr?nqmOH;fcX7R&toseB9aN^-FOK@JzN6(uMSD>!;v{239hvCfA z*r~O3IrdW-U*_Ee^rq#ZA0PcBSCaJ4lk47OigE*nDO75P*Tb7Q_7^e9KAcg3W9Lfy zbW0l-PNyW6Lf>5RJV4QaL?ae>bPT+J*{KegG}9D`PK{9ocZ>889S#n_UvJuKPFPLv#RKIgcSx(`@KrCCVnkAOIb^n*eYO2#k9jVsxUx z>(dI7M-!lz<{+T0;AqRt*RPKLNQ^tWl3*G$kK%Yz>#lf$iVNCR$v9&JNbEfl_sF}l zaF>ny%e<{Q`2&01vZq+$oy&_I&jhD%H#P3(^}wO?ELfjM;4Vd?g~`QFv2Ey|csFpIA_4ccH6qz26M2fQDHsNr4svp1 zpxy%$n04OML0?hk!le6+lQ6pJ798QSl+`h{l6d};CPpnblU!px@tErS!Z0v$EhN^P z)}A*~E9TchtwHJ8>(@_8bbeV>kJ%X7=!MqZnX3Y8g5FC$%9xaHN-lU~F+q)u$${o% zMQb-PCQ7LZj{o8 z8b>vIHAx;ZrAg`-vi-9Qec;xjSWhJ>m$v1|p(S1)?=RINYuj7&k&)lvPe(Ee>MOe4 zSmRqoJt>L^`Pl8rpfEH(#YlW3-xhd-AIL~5Ku3KxD>u-6=-c2CFzMpzOQ!LWU^q9T+F!wpn&*C`pV zVwUA2 zG)qywACw6q@`72CyBE^;ndbG7>2-pk*a9C8VG#BgvjM3jE(8C{HShHU41tC@3(ix! z+H!zdM@%6|C+cA$vY_CuV!nvSBwx-P5*cJRJNsgj4l#L+XP%iPX|kyRO_)ClW&tG~ zOXseP;BuZdI?b=;^k(oJUVI_7FP?ZBB+O~i$a8p!cQTY@PheUO{T6qj48o*U`xQd} zv@`N5=T|C0F;&-r?U`prCD>9+-fbC*TBb+u^*9YQ!683>Dl_<{J z2Q&KlARi7~aX?-?RY7LRS6(b+3||K~b!vQ|yMPHw=;B<