HEX
Server: LiteSpeed
System: Linux houston.panomity.com 6.8.0-100-generic #100-Ubuntu SMP PREEMPT_DYNAMIC Tue Jan 13 16:40:06 UTC 2026 x86_64
User: nudepix (1011)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: //usr/local/CyberCP/tools/gen_dark.py
#!/usr/bin/env python3
"""Generate a dark-mode override stylesheet for CyberPanel internal pages.

Scans every template's embedded <style> blocks, finds class/id selectors whose
declarations hardcode NEUTRAL light values (white/grey backgrounds, light
borders, dark/grey text) instead of design tokens, and emits override rules
scoped under [data-theme="dark"] #main-content that route them to the dark
tokens. Light mode is never touched. Brand/semantic colors are left alone.
"""
import os, re, glob, colorsys

ROOT = "/Users/cyberpersons/cyberpanel"

# ---- value buckets (normalized: lowercase, spaces stripped) ----
BG_SECONDARY = {"#fff", "#ffffff", "white", "#fefefe", "#fcfcfc", "#fdfdfd"}
BG_MUTED = {"#f8f9ff","#f0f1ff","#fafbff","#f6f7f9","#f6f7fc","#f9fafb","#f9fafc",
            "#f3f4f6","#f1f5f9","#f8fafc","#f5f6fa","#eef0f4","#eceefb","#f4f5fa",
            "#fafafa","#f5f5f5","#f7f8fa","#eef","#f0f2f5","#f8f9fa","#fbfbfc",
            "#f2f3f5","#fafbfc","#eff1f5","#f0f4ff","#edf2f7","#f0f0ff","#eef0ff",
            "#eeeeff","#f5f5ff","#fbfcff","#fcfdff","#eef1f6","#f7f9fc","#fcfcff"}
BORDER_LIGHT = {"#e8e9ff","#e2e8f0","#e5e7eb","#e7e9ee","#eee","#eeeeee","#ddd",
                "#dddddd","#e0e0e0","#e9ebf4","#e9ecef","#dee2e6","#e1e4e8",
                "#ebedf0","#eaecef","#e3e6ea","#edf0f3","#d1d5db","#e6e8eb",
                "#e4e7ec","#f0f0f0","#ececec"}
TEXT_HEADING = {"#1e293b","#0f172a","#111827","#1a202c","#111","#000","#000000",
                "#1f2937","#2d3748","#212529","#1c1e21","#222","#212121"}
TEXT_PRIMARY = {"#2f3640","#333","#333333","#374151","#334155","#3b4252","#444",
                "#2d3436","#343a40","#393e46","#2c3e50","#4b5563"}
TEXT_SECONDARY = {"#64748b","#6b7280","#6c757d","#94a3b8","#9ca3af","#718096",
                  "#7b8794","#8492a6","#888","#999","#777","#667085","#52525b",
                  "#6e7891","#5a6270","#73798c","#848d9c"}

def norm(v):
    v = v.strip().lower().replace("!important", "").strip()
    v = v.rstrip(";").strip()
    return v

def rgb_channels(val):
    m = re.search(r"rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)", val)
    if not m: return None
    return int(m.group(1)), int(m.group(2)), int(m.group(3))

def gradient_is_neutral_light(val):
    """True if a linear/radial-gradient is composed only of neutral light stops."""
    if "gradient" not in val:
        return False
    hexes = re.findall(r"#[0-9a-fA-F]{3,6}", val)
    names = re.findall(r"\b(white|whitesmoke|ghostwhite|snow)\b", val.lower())
    if not hexes and not names:
        return False
    for h in hexes:
        if h.lower() not in (BG_SECONDARY | BG_MUTED | BORDER_LIGHT):
            return False
    return True

def _rgb(v):
    v = v.strip().lower()
    if v == "white": return (255, 255, 255)
    if v == "black": return (0, 0, 0)
    m = re.match(r"^#([0-9a-f]{3}|[0-9a-f]{6})$", v)
    if m:
        h = m.group(1)
        if len(h) == 3: h = "".join(c * 2 for c in h)
        return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
    return rgb_channels(v)
def _lum(c): return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]
def _sat(c):
    mx, mn = max(c), min(c)
    return 0 if mx == 0 else (mx - mn) / mx
def _hue(c):
    h, _, _ = colorsys.rgb_to_hls(c[0] / 255, c[1] / 255, c[2] / 255)
    d = h * 360
    if d < 18 or d >= 345: return "red"
    if d < 70:  return "amber"
    if d < 170: return "green"
    if d < 250: return "blue"
    if d < 290: return "indigo"
    return "red"
SOFT_BG = {"red": "var(--danger-soft-bg)", "green": "var(--success-soft-bg)",
           "amber": "var(--warn-soft-bg)", "blue": "var(--info-soft-bg)",
           "indigo": "var(--accent-soft-bg)"}
SOFT_TEXT = {"red": "var(--danger-soft-text)", "green": "var(--success-soft-text)",
             "amber": "var(--warn-soft-text)", "blue": "var(--info-soft-text)",
             "indigo": "var(--text-heading)"}

def bg_token(val):
    n = norm(val)
    if n in BG_SECONDARY: return "var(--bg-secondary)"
    if n in BG_MUTED: return "var(--bg-muted)"
    if gradient_is_neutral_light(n): return "var(--bg-muted)"
    if "gradient" in n: return None
    c = _rgb(n)
    if not c: return None
    L, S = _lum(c), _sat(c)
    if S < 0.10:                                  # neutral
        if L >= 246: return "var(--bg-secondary)"
        if L >= 205: return "var(--bg-muted)"
        return None                               # mid/dark neutral: intentional, leave
    if L >= 200:                                  # light semantic tint -> soft dark tint
        return SOFT_BG[_hue(c)]
    return None                                   # saturated mid/dark bg (brand) -> leave

def bg_text_misuse(val):
    """Pages set `background: var(--text-primary, #1e293b)` to get a DARK panel
    (code editors / terminals). In dark mode the text token flips light and the
    panel breaks. Restore a dark surface: use the (dark) fallback, else a dark token."""
    n = norm(val)
    m = re.match(r"^var\(\s*--text-[a-z]+\s*(?:,\s*([^)]+))?\)$", n)
    if not m:
        return None
    fb = (m.group(1) or "").strip()
    c = _rgb(fb) if fb else None
    if c is not None:
        if _lum(c) <= 175:            # intended a dark/mid surface -> keep that exact color
            return fb
        return None                   # light fallback: not a dark-panel misuse, leave
    return "var(--bg-hover)"          # no fallback: give it a dark surface token

def border_token(val):
    # value may be "1px solid #e8e9ff" — extract color
    n = norm(val)
    m = re.findall(r"#[0-9a-fA-F]{3,6}|\b(?:white|whitesmoke)\b", n)
    if not m: return None
    col = m[-1] if isinstance(m[-1], str) else None
    col = re.findall(r"#[0-9a-fA-F]{3,6}|white|whitesmoke", n)
    col = col[-1] if col else None
    if not col: return None
    if col in BORDER_LIGHT: return "var(--border-color)"
    c = _rgb(col)
    if c and _lum(c) >= 198:                       # any light border (incl. semantic tints) -> token
        return "var(--border-color)"
    return None

def text_token(val):
    n = norm(val)
    if "gradient" in n: return None
    if n in TEXT_HEADING: return "var(--text-heading)"
    if n in TEXT_PRIMARY: return "var(--text-primary)"
    if n in TEXT_SECONDARY: return "var(--text-secondary)"
    c = _rgb(n)
    if not c: return None
    L, S = _lum(c), _sat(c)
    if S < 0.16:                                  # neutral dark/grey ink -> light text token
        if L <= 90:  return "var(--text-heading)"
        if L <= 130: return "var(--text-primary)"
        if L <= 165: return "var(--text-secondary)"
        return None                               # already-light text -> leave
    if L <= 95:                                   # vivid dark semantic ink -> soft light ink
        return SOFT_TEXT[_hue(c)]
    return None                                   # vivid mid/bright brand text -> leave (reads on dark)

# selectors we refuse to emit (too broad / would over-reach in dark mode)
BAD_SEL_RE = re.compile(r"[%@]|:root|^html|^body|::?before|::?after|keyframes|^\d|^from$|^to$")
def selector_ok(sel):
    sel = sel.strip()
    if not sel: return False
    if BAD_SEL_RE.search(sel): return False
    if "[data-theme" in sel: return False
    # must be class/id targeted (not a bare element/universal selector)
    if "." not in sel and "#" not in sel and "[" not in sel: return False
    if "*" in sel: return False
    return True

def split_selectors(selblob):
    return [s.strip() for s in selblob.split(",") if s.strip()]

# accumulate: selector -> dict(props)
rules = {}  # sel -> {prop: tokenvalue}

def add(sel, prop, token):
    if not selector_ok(sel): return
    sel = re.sub(r"\s+", " ", sel.strip())
    rules.setdefault(sel, {})
    # don't clobber a more specific earlier mapping for same prop with different token
    rules[sel].setdefault(prop, token)

# innermost-block matcher
BLOCK_RE = re.compile(r"([^{}]+)\{([^{}]*)\}", re.S)
STYLE_RE = re.compile(r"<style[^>]*>(.*?)</style>", re.S | re.I)
DECL_RE = re.compile(r"([a-zA-Z-]+)\s*:\s*([^;{}]+)\s*;?")

EXCLUDE = re.compile(r"/mode/|/static/filemanager/|_backup\.html|_fixed\.html|"
                     r"indexJavaFixed\.html|loginSystem/templates/loginSystem/test\.html")

templates = []
for f in glob.glob(os.path.join(ROOT, "**", "*.html"), recursive=True):
    if EXCLUDE.search(f): continue
    templates.append(f)

scanned = 0
for f in templates:
    try:
        txt = open(f, encoding="utf-8", errors="ignore").read()
    except Exception:
        continue
    if "<style" not in txt: continue
    styleblobs = STYLE_RE.findall(txt)
    if not styleblobs: continue
    scanned += 1
    css = "\n".join(styleblobs)
    css = re.sub(r"/\*.*?\*/", "", css, flags=re.S)  # strip comments (else they glue onto selectors)
    for m in BLOCK_RE.finditer(css):
        selblob, body = m.group(1), m.group(2)
        # skip at-rule preludes captured as selector
        decls = dict()
        for d in DECL_RE.finditer(body):
            prop = d.group(1).strip().lower()
            val = d.group(2).strip()
            decls[prop] = val
        if not decls: continue
        # backgrounds
        for prop in ("background", "background-color"):
            if prop in decls:
                t = bg_token(decls[prop])
                if not t:
                    t = bg_text_misuse(decls[prop])   # background: var(--text-*, dark) -> dark surface
                if t:
                    for sel in split_selectors(selblob):
                        add(sel, "background", t)
        # borders -> border-color
        for prop in ("border", "border-color", "border-top", "border-bottom",
                     "border-left", "border-right"):
            if prop in decls:
                t = border_token(decls[prop])
                if t:
                    for sel in split_selectors(selblob):
                        add(sel, "border-color", t)
        # text color
        if "color" in decls:
            t = text_token(decls["color"])
            if t:
                for sel in split_selectors(selblob):
                    add(sel, "color", t)

# ---- emit ----
out = []
out.append("/* ====================================================================")
out.append("   CyberPanel Dark-Mode Overrides  (AUTO-GENERATED — issue #1804)")
out.append("   --------------------------------------------------------------------")
out.append("   Internal pages hardcode the old light palette (white / #f8f9ff / dark")
out.append("   text) in their embedded <style> instead of using design tokens, so in")
out.append("   dark mode they render as blinding white panels. This sheet re-maps ONLY")
out.append("   the neutral light values those pages declare to the dark tokens, scoped")
out.append("   to [data-theme=\"dark\"] #main-content so LIGHT MODE IS NEVER AFFECTED and")
out.append("   brand / semantic colors (accents, success, danger…) are left untouched.")
out.append("   Regenerate with tools/gen_dark.py — do not edit by hand.")
out.append("   ==================================================================== */")
out.append("")

PROP_ORDER = ["background", "border-color", "color"]
for sel in sorted(rules):
    props = rules[sel]
    decls = []
    for p in PROP_ORDER:
        if p in props:
            decls.append(f"{p}: {props[p]} !important;")
    if not decls: continue
    scoped = f'[data-theme="dark"] #main-content {sel}'
    out.append(scoped + " { " + " ".join(decls) + " }")

dest = os.path.join(ROOT, "baseTemplate/static/baseTemplate/css/cyberpanel-dark.css")
open(dest, "w").write("\n".join(out) + "\n")
print(f"scanned {scanned} templates with <style>")
print(f"emitted {len(rules)} selector rules -> {dest}")