#!/usr/bin/env python3 """ OASIS Control Panel v2 — Solar Net Hub Panel compacto estilo Mullvad, version mejorada. """ import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk, GLib, Gdk, GdkPixbuf import subprocess import os import sys import signal import threading import json import shutil from pathlib import Path # ── Rutas ────────────────────────────────────────────────────────────────── SCRIPT_DIR = Path(__file__).parent.resolve() INSTALLER_SH = SCRIPT_DIR / "installer.sh" OASIS_DIR = Path.home() / "oasis" ECOIN_DIR = Path.home() / "ecoin" MODEL_FILE = "oasis-42-1-chat.Q4_K_M.gguf" # ── CSS ──────────────────────────────────────────────────────────────────── CSS = """ /* === FUERZA NEGRO EN TODO — sin excepciones === */ *, widget, box, grid, stack, overlay, scrolledwindow, viewport, clamp, notebook, frame, paned, label, button, entry, textview, separator, headerbar, actionbar, menubar, menu, menuitem, treeview, iconview, flowbox, progressbar, levelbar, scale, spinbutton, combobox, popover, revealer, expander, listbox, row { background-color: #000000; background: #000000; color: #FF4E00; border-color: #1A1A1A; outline-color: transparent; box-shadow: none; text-shadow: none; font-family: "Dune Rise", "Cantarell", "Ubuntu", "DejaVu Sans", sans-serif; } /* El texto dentro de textview tambien */ textview text, textview text selection { background-color: #000000; color: #FF4E00; } /* === HEADER === */ box.header { background-color: #080808; border-bottom: 1px solid #1E1E1E; } label.header-title { font-size: 13pt; font-weight: bold; color: #FF4E00; letter-spacing: 4px; background-color: transparent; } label.header-sub { font-size: 7pt; color: #552200; letter-spacing: 2px; background-color: transparent; } /* === STATUS CARD === */ box.status-card { background-color: #080808; border-radius: 10px; border: 1px solid #1A1A1A; } box.card-border-running { background-color: #27D980; border-radius: 8px 0px 0px 8px; min-width: 5px; } box.card-border-stopped { background-color: #FF4E00; border-radius: 8px 0px 0px 8px; min-width: 5px; } box.card-border-unknown { background-color: #1A1A1A; border-radius: 8px 0px 0px 8px; min-width: 5px; } label.state-text { font-size: 17pt; font-weight: bold; letter-spacing: 2px; background-color: transparent; } label.state-running { color: #27D980; } label.state-stopped { color: #FF4E00; } label.state-unknown { color: #883300; } label.state-sub { font-size: 8pt; color: #552200; letter-spacing: 1px; background-color: transparent; } /* Dot */ label.header-dot { font-size: 8pt; background-color: transparent; } label.dot-running { color: #27D980; } label.dot-stopped { color: #FF4E00; } label.dot-unknown { color: #441100; } /* === TABS === */ notebook > header, notebook > header tabs, notebook > header tab, tab { background-color: #000000; background: #000000; border: none; box-shadow: none; } tab { color: #552200; border-bottom: 3px solid transparent; padding: 11px 24px; font-size: 8pt; letter-spacing: 2px; font-weight: bold; } tab:checked { color: #FF4E00; border-bottom: 3px solid #FF4E00; } tab:hover { color: #BB3300; } /* === BOTONES === */ button, button * { background-color: #000000; background: #000000; color: #FF4E00; box-shadow: none; text-shadow: none; } button.btn { border: 1px solid #FF4E00; border-radius: 20px; padding: 9px 18px; margin: 5px 5px; font-size: 7.5pt; font-weight: bold; letter-spacing: 1px; min-width: 128px; } button.btn:hover, button.btn:hover * { background-color: #27D980; background: #27D980; color: #000000; border-color: #27D980; } button.btn:active, button.btn:active * { background-color: #1fa865; background: #1fa865; color: #000000; } button.btn:disabled, button.btn:disabled * { color: #2A0E00; border-color: #1A0A00; } button.btn-primary { background-color: #FF4E00; background: #FF4E00; color: #000000; border: 1px solid #FF4E00; border-radius: 20px; padding: 9px 18px; margin: 5px 5px; font-size: 7.5pt; font-weight: bold; letter-spacing: 1px; min-width: 128px; } button.btn-primary * { background-color: transparent; color: #000000; } button.btn-primary:hover, button.btn-primary:hover * { background-color: #27D980; background: #27D980; color: #000000; border-color: #27D980; } button.btn-primary:disabled, button.btn-primary:disabled * { background-color: #1A0800; background: #1A0800; color: #2A1000; border-color: #1A0800; } /* === INFO === */ box.info-box { background-color: #050505; border-top: 1px solid #111111; } label.info-key { color: #662200; font-size: 8pt; letter-spacing: 1px; min-width: 80px; background-color: transparent; } label.info-val { color: #FF5500; font-size: 8pt; background-color: transparent; } /* === LOG === */ textview.log-view, textview.log-view text { background-color: #030303; color: #27D980; font-family: "DejaVu Sans Mono", "Monospace", monospace; font-size: 7.5pt; padding: 10px; } scrolledwindow.log-scroll { border: 1px solid #181818; border-radius: 8px; margin: 6px 14px; } separator { background-color: #0F0F0F; min-height: 1px; } label.section-lbl { color: #441100; font-size: 7pt; letter-spacing: 2px; background-color: transparent; } """ # ── Helpers de estado ────────────────────────────────────────────────────── def oasis_installed(): return ( (OASIS_DIR / "src" / "server" / "node_modules").is_dir() and (OASIS_DIR / "AI" / MODEL_FILE).is_file() ) def oasis_version(): pkg = OASIS_DIR / "src" / "server" / "package.json" if pkg.is_file(): try: return json.loads(pkg.read_text()).get("version", "—") except Exception: pass return "—" def oasis_running(): try: r = subprocess.run(["pgrep", "-f", "node.*server.js"], capture_output=True, timeout=2) return r.returncode == 0 except Exception: return False def ecoin_installed(): return ( (ECOIN_DIR / "ecoin" / "ecoin-qt").is_file() or (ECOIN_DIR / "ecoin" / "src" / "ecoind").is_file() ) def ecoin_wallet_exists(): return (Path.home() / ".ecoin" / "wallet.dat").is_file() def node_version(): try: r = subprocess.run(["node", "--version"], capture_output=True, text=True, timeout=3) return r.stdout.strip() except Exception: return "—" # ── Panel ────────────────────────────────────────────────────────────────── class OasisPanel(Gtk.ApplicationWindow): def __init__(self, app): super().__init__(application=app, title="SOLAR NET HUB") self.set_default_size(390, 620) self.set_resizable(False) self.set_position(Gtk.WindowPosition.CENTER) self._build_ui() GLib.timeout_add_seconds(3, self._trigger_poll) self._trigger_poll() # ── Layout ──────────────────────────────────────────────────────────── def _build_ui(self): root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) self.add(root) root.pack_start(self._make_header(), False, False, 0) nb = Gtk.Notebook() nb.set_show_border(False) root.pack_start(nb, True, True, 0) self.notebook = nb nb.append_page(self._make_oasis_tab(), self._tab_lbl("OASIS")) nb.append_page(self._make_ecoin_tab(), self._tab_lbl("ECOIN")) nb.append_page(self._make_sistema_tab(), self._tab_lbl("SISTEMA")) def _tab_lbl(self, t): return Gtk.Label(label=t) # ── Header ──────────────────────────────────────────────────────────── def _make_header(self): outer = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) outer.get_style_context().add_class("header") outer.set_margin_top(12) outer.set_margin_bottom(12) outer.set_margin_start(16) outer.set_margin_end(16) # Logo logo_path = SCRIPT_DIR / "oasis-logo.png" if logo_path.is_file(): try: pb = GdkPixbuf.Pixbuf.new_from_file_at_scale(str(logo_path), 38, 38, True) outer.pack_start(Gtk.Image.new_from_pixbuf(pb), False, False, 0) except Exception: pass # Titulo + subtitulo vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=1) vbox.set_margin_start(10) vbox.set_valign(Gtk.Align.CENTER) title = Gtk.Label(label="SOLAR NET HUB") title.get_style_context().add_class("header-title") title.set_halign(Gtk.Align.START) vbox.pack_start(title, False, False, 0) sub = Gtk.Label(label="OASIS + ECOIN CONTROL PANEL") sub.get_style_context().add_class("header-sub") sub.set_halign(Gtk.Align.START) vbox.pack_start(sub, False, False, 0) outer.pack_start(vbox, True, True, 0) # Dot de estado global self.header_dot = Gtk.Label(label="●") self.header_dot.get_style_context().add_class("header-dot") self.header_dot.get_style_context().add_class("dot-unknown") outer.pack_end(self.header_dot, False, False, 0) return outer # ── OASIS Tab ───────────────────────────────────────────────────────── def _make_oasis_tab(self): scroll = Gtk.ScrolledWindow() scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) scroll.add(box) # Status card con borde lateral de color card_wrap = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) card_wrap.get_style_context().add_class("status-card") card_wrap.set_margin_top(14) card_wrap.set_margin_bottom(6) card_wrap.set_margin_start(14) card_wrap.set_margin_end(14) # Borde lateral (color dinamico) self.oasis_border = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.oasis_border.get_style_context().add_class("card-border-unknown") card_wrap.pack_start(self.oasis_border, False, False, 0) # Contenido de la card card_inner = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) card_inner.set_margin_top(14) card_inner.set_margin_bottom(14) card_inner.set_margin_start(14) card_inner.set_margin_end(14) self.oasis_state_lbl = Gtk.Label(label="COMPROBANDO") self.oasis_state_lbl.get_style_context().add_class("state-text") self.oasis_state_lbl.get_style_context().add_class("state-unknown") self.oasis_state_lbl.set_halign(Gtk.Align.START) card_inner.pack_start(self.oasis_state_lbl, False, False, 0) self.oasis_sub_lbl = Gtk.Label(label="") self.oasis_sub_lbl.get_style_context().add_class("state-sub") self.oasis_sub_lbl.set_halign(Gtk.Align.START) card_inner.pack_start(self.oasis_sub_lbl, False, False, 0) card_wrap.pack_start(card_inner, True, True, 0) box.pack_start(card_wrap, False, False, 0) # Botones fila 1 r1 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) r1.set_halign(Gtk.Align.CENTER) r1.set_margin_top(8) self.btn_o_start = self._btn("▶ INICIAR", self._on_oasis_start, primary=True) self.btn_o_stop = self._btn("■ DETENER", self._on_oasis_stop) r1.pack_start(self.btn_o_start, False, False, 0) r1.pack_start(self.btn_o_stop, False, False, 0) box.pack_start(r1, False, False, 0) # Botones fila 2 r2 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) r2.set_halign(Gtk.Align.CENTER) r2.set_margin_bottom(8) self.btn_o_install = self._btn("⬇ INSTALAR", self._on_oasis_install) self.btn_o_browser = self._btn("◎ ABRIR WEB", self._on_oasis_browser) r2.pack_start(self.btn_o_install, False, False, 0) r2.pack_start(self.btn_o_browser, False, False, 0) box.pack_start(r2, False, False, 0) # Info info_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) info_box.get_style_context().add_class("info-box") info_box.set_margin_top(6) grid = Gtk.Grid() grid.set_column_spacing(12) grid.set_row_spacing(7) grid.set_margin_start(18) grid.set_margin_end(18) grid.set_margin_top(12) grid.set_margin_bottom(12) self.oasis_ver_val = self._info_row(grid, 0, "VERSION") self.oasis_node_val = self._info_row(grid, 1, "NODE.JS") self.oasis_dir_val = self._info_row(grid, 2, "RUTA") info_box.pack_start(grid, False, False, 0) box.pack_start(info_box, False, False, 0) return scroll # ── ECOIN Tab ───────────────────────────────────────────────────────── def _make_ecoin_tab(self): scroll = Gtk.ScrolledWindow() scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) scroll.add(box) # Status card card_wrap = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) card_wrap.get_style_context().add_class("status-card") for edge in ("top", "bottom", "start", "end"): getattr(card_wrap, f"set_margin_{edge}")(14) card_wrap.set_margin_bottom(6) self.ecoin_border = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.ecoin_border.get_style_context().add_class("card-border-unknown") card_wrap.pack_start(self.ecoin_border, False, False, 0) card_inner = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) card_inner.set_margin_top(14) card_inner.set_margin_bottom(14) card_inner.set_margin_start(14) card_inner.set_margin_end(14) self.ecoin_state_lbl = Gtk.Label(label="COMPROBANDO") self.ecoin_state_lbl.get_style_context().add_class("state-text") self.ecoin_state_lbl.get_style_context().add_class("state-unknown") self.ecoin_state_lbl.set_halign(Gtk.Align.START) card_inner.pack_start(self.ecoin_state_lbl, False, False, 0) self.ecoin_sub_lbl = Gtk.Label(label="") self.ecoin_sub_lbl.get_style_context().add_class("state-sub") self.ecoin_sub_lbl.set_halign(Gtk.Align.START) card_inner.pack_start(self.ecoin_sub_lbl, False, False, 0) card_wrap.pack_start(card_inner, True, True, 0) box.pack_start(card_wrap, False, False, 0) r1 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) r1.set_halign(Gtk.Align.CENTER) r1.set_margin_top(8) self.btn_e_install = self._btn("⬇ INSTALAR", self._on_ecoin_install, primary=True) self.btn_e_gui = self._btn("◈ ABRIR GUI", self._on_ecoin_gui) r1.pack_start(self.btn_e_install, False, False, 0) r1.pack_start(self.btn_e_gui, False, False, 0) box.pack_start(r1, False, False, 0) r2 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) r2.set_halign(Gtk.Align.CENTER) r2.set_margin_bottom(8) self.btn_e_wallet = self._btn("✦ CREAR WALLET", self._on_ecoin_wallet) self.btn_e_connect = self._btn("⟳ CONECTAR", self._on_ecoin_connect) r2.pack_start(self.btn_e_wallet, False, False, 0) r2.pack_start(self.btn_e_connect, False, False, 0) box.pack_start(r2, False, False, 0) info_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) info_box.get_style_context().add_class("info-box") info_box.set_margin_top(6) grid = Gtk.Grid() grid.set_column_spacing(12) grid.set_row_spacing(7) grid.set_margin_start(18) grid.set_margin_end(18) grid.set_margin_top(12) grid.set_margin_bottom(12) self.ecoin_wallet_val = self._info_row(grid, 0, "WALLET") self.ecoin_qt_val = self._info_row(grid, 1, "ECOIN-QT") self.ecoin_daemon_val = self._info_row(grid, 2, "ECOIND") info_box.pack_start(grid, False, False, 0) box.pack_start(info_box, False, False, 0) return scroll # ── SISTEMA Tab ─────────────────────────────────────────────────────── def _make_sistema_tab(self): box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) hdr = Gtk.Label(label="LOG DE ACTIVIDAD") hdr.get_style_context().add_class("section-lbl") hdr.set_halign(Gtk.Align.START) hdr.set_margin_start(18) hdr.set_margin_top(14) hdr.set_margin_bottom(6) box.pack_start(hdr, False, False, 0) scroll = Gtk.ScrolledWindow() scroll.get_style_context().add_class("log-scroll") scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) scroll.set_vexpand(True) self.log_view = Gtk.TextView() self.log_view.get_style_context().add_class("log-view") self.log_view.set_editable(False) self.log_view.set_cursor_visible(False) self.log_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) self.log_buf = self.log_view.get_buffer() self._end_mark = self.log_buf.create_mark("end", self.log_buf.get_end_iter(), False) scroll.add(self.log_view) box.pack_start(scroll, True, True, 0) btn = self._btn("LIMPIAR LOG", self._on_clear_log) btn.set_halign(Gtk.Align.CENTER) btn.set_margin_top(8) btn.set_margin_bottom(12) box.pack_start(btn, False, False, 0) return box # ── Widget helpers ──────────────────────────────────────────────────── def _btn(self, label, cb, primary=False): b = Gtk.Button(label=label) b.get_style_context().add_class("btn-primary" if primary else "btn") b.connect("clicked", cb) return b def _info_row(self, grid, row, key): k = Gtk.Label(label=key) k.get_style_context().add_class("info-key") k.set_halign(Gtk.Align.START) grid.attach(k, 0, row, 1, 1) v = Gtk.Label(label="—") v.get_style_context().add_class("info-val") v.set_halign(Gtk.Align.START) grid.attach(v, 1, row, 1, 1) return v # ── Card border helper ──────────────────────────────────────────────── def _set_border(self, widget, state): for c in ("card-border-running", "card-border-stopped", "card-border-unknown"): widget.get_style_context().remove_class(c) widget.get_style_context().add_class(f"card-border-{state}") def _set_dot(self, lbl, state): for c in ("dot-running", "dot-stopped", "dot-unknown"): lbl.get_style_context().remove_class(c) lbl.get_style_context().add_class(f"dot-{state}") def _set_state(self, lbl, text, state): for c in ("state-running", "state-stopped", "state-unknown"): lbl.get_style_context().remove_class(c) lbl.set_text(text) lbl.get_style_context().add_class(f"state-{state}") # ── Status polling ──────────────────────────────────────────────────── def _trigger_poll(self): threading.Thread(target=self._poll_thread, daemon=True).start() return True def _poll_thread(self): o_inst = oasis_installed() o_run = oasis_running() o_ver = oasis_version() o_node = node_version() e_inst = ecoin_installed() e_wall = ecoin_wallet_exists() e_qt = (ECOIN_DIR / "ecoin" / "ecoin-qt").is_file() e_dmn = (ECOIN_DIR / "ecoin" / "src" / "ecoind").is_file() GLib.idle_add(self._apply_status, o_inst, o_run, o_ver, o_node, e_inst, e_wall, e_qt, e_dmn) def _apply_status(self, o_inst, o_run, o_ver, o_node, e_inst, e_wall, e_qt, e_dmn): # Header dot h = "running" if o_run else ("stopped" if o_inst else "unknown") self._set_dot(self.header_dot, h) # OASIS card o_state = "running" if o_run else ("stopped" if o_inst else "unknown") self._set_border(self.oasis_border, o_state) if o_run: self._set_state(self.oasis_state_lbl, "ACTIVO", "running") self.oasis_sub_lbl.set_text("servidor corriendo en puerto 3000") self.btn_o_start.set_sensitive(False) self.btn_o_stop.set_sensitive(True) self.btn_o_browser.set_sensitive(True) elif o_inst: self._set_state(self.oasis_state_lbl, "INSTALADO", "stopped") self.oasis_sub_lbl.set_text("servidor detenido") self.btn_o_start.set_sensitive(True) self.btn_o_stop.set_sensitive(False) self.btn_o_browser.set_sensitive(False) else: self._set_state(self.oasis_state_lbl, "NO INSTALADO", "unknown") self.oasis_sub_lbl.set_text("instala OASIS para comenzar") self.btn_o_start.set_sensitive(False) self.btn_o_stop.set_sensitive(False) self.btn_o_browser.set_sensitive(False) self.oasis_ver_val.set_text(f"v{o_ver}" if o_ver != "—" else "—") self.oasis_node_val.set_text(o_node if o_node != "—" else "no instalado") self.oasis_dir_val.set_text(str(OASIS_DIR) if o_inst else "—") # ECOIN card e_state = "running" if e_qt else ("stopped" if e_inst else "unknown") self._set_border(self.ecoin_border, e_state) if e_inst: self._set_state(self.ecoin_state_lbl, "COMPILADO", "running") self.ecoin_sub_lbl.set_text("wallet ECOIN disponible") self.btn_e_gui.set_sensitive(True) self.btn_e_wallet.set_sensitive(True) self.btn_e_connect.set_sensitive(True) else: self._set_state(self.ecoin_state_lbl, "NO INSTALADO", "unknown") self.ecoin_sub_lbl.set_text("instala ECOIN para comenzar") self.btn_e_gui.set_sensitive(False) self.btn_e_wallet.set_sensitive(False) self.btn_e_connect.set_sensitive(False) self.ecoin_wallet_val.set_text("Si" if e_wall else "No") self.ecoin_qt_val.set_text("Si" if e_qt else "No") self.ecoin_daemon_val.set_text("Si" if e_dmn else "No") # ── Acciones ───────────────────────────────────────────────────────── def _run(self, *args): cmd = [str(INSTALLER_SH)] + list(args) self._log(f"$ {' '.join(cmd)}") threading.Thread(target=self._exec_log, args=(cmd,), daemon=True).start() GLib.idle_add(self.notebook.set_current_page, 2) def _exec_log(self, cmd): try: proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1 ) for line in iter(proc.stdout.readline, ""): GLib.idle_add(self._log, line.rstrip()) proc.wait() GLib.idle_add(self._log, f"-- proceso terminado (codigo {proc.returncode}) --") except Exception as e: GLib.idle_add(self._log, f"[Error]: {e}") GLib.idle_add(self._trigger_poll) def _log(self, text): self.log_buf.insert(self.log_buf.get_end_iter(), text + "\n") self.log_view.scroll_to_mark(self._end_mark, 0.0, True, 0.0, 1.0) return False def _on_oasis_start(self, _): self._run("--oasis-start") def _on_oasis_install(self, _): self._run("--oasis-install") def _on_oasis_stop(self, _): self._log("Deteniendo OASIS...") threading.Thread(target=self._kill_oasis, daemon=True).start() def _kill_oasis(self): try: subprocess.run(["pkill", "-f", "node.*server.js"], timeout=5) GLib.idle_add(self._log, "Servidor OASIS detenido.") except Exception as e: GLib.idle_add(self._log, f"[Error al detener]: {e}") GLib.idle_add(self._trigger_poll) def _on_oasis_browser(self, _): subprocess.Popen(["xdg-open", "http://localhost:3000"]) self._log("Abriendo http://localhost:3000 ...") def _on_ecoin_install(self, _): self._run("--ecoin-install") def _on_ecoin_gui(self, _): self._run("--ecoin-gui") def _on_ecoin_wallet(self, _): self._run("--wallet-create") def _on_ecoin_connect(self, _): self._run("--wallet-connect") def _on_clear_log(self, _): self.log_buf.set_text("") # ── App ──────────────────────────────────────────────────────────────────── class OasisApp(Gtk.Application): def __init__(self): super().__init__(application_id="net.solarnethub.panel.v2") def do_activate(self): win = OasisPanel(self) win.show_all() def main(): # Manejo limpio de Ctrl+C — sin traceback signal.signal(signal.SIGINT, signal.SIG_DFL) provider = Gtk.CssProvider() provider.load_from_data(CSS.encode("utf-8")) Gtk.StyleContext.add_provider_for_screen( Gdk.Screen.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, ) app = OasisApp() sys.exit(app.run(sys.argv)) if __name__ == "__main__": main()