Panel compacto estilo Mullvad usando Python + PyGObject (GTK3). Status cards con borde lateral de color, fuente Dune Rise, colores naranja/negro, log en tiempo real, registro .desktop automatico en el menu de aplicaciones. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
746 lines
27 KiB
Python
746 lines
27 KiB
Python
#!/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()
|