chore: eliminar INSTALLER_V2 y archivos obsoletos del directorio local

V2 queda accesible en el historial de git. Solo se mantiene V3 activo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
SITO 2026-03-30 12:30:46 +02:00
parent 67acbf1add
commit 56f4190d41
10 changed files with 0 additions and 2032 deletions

View file

@ -1,695 +0,0 @@
#!/usr/bin/env python3
"""
OASIS Control Panel Solar Net Hub
Panel de control compacto estilo Mullvad para gestionar OASIS y ECOIN en Linux.
"""
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib, Gdk, GdkPixbuf
import subprocess
import os
import sys
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 = """
* {
font-family: "Dune Rise", "Cantarell", "Ubuntu", "DejaVu Sans", sans-serif;
color: #E6E6E6;
}
window {
background-color: #000000;
}
/* Header */
box.panel-header {
background-color: #080808;
border-bottom: 1px solid #1C1C1C;
}
label.panel-title {
font-size: 11pt;
font-weight: bold;
color: #FF4E00;
letter-spacing: 3px;
}
/* Status card */
box.status-card {
background-color: #0D0D0D;
border: 1px solid #1C1C1C;
border-radius: 12px;
}
label.status-main {
font-size: 14pt;
font-weight: bold;
}
label.state-running { color: #27D980; }
label.state-stopped { color: #FF4E00; }
label.state-unknown { color: #555555; }
label.status-sub {
font-size: 8pt;
color: #666666;
}
/* Dots */
label.dot { font-size: 9pt; }
label.dot-running { color: #27D980; }
label.dot-stopped { color: #FF4E00; }
label.dot-unknown { color: #333333; }
/* Botones */
button.btn {
background-color: #000000;
color: #FF4E00;
border: 1px solid #FF4E00;
border-radius: 18px;
padding: 8px 16px;
margin: 5px 6px;
font-size: 8pt;
font-weight: bold;
letter-spacing: 1px;
min-width: 130px;
}
button.btn:hover {
background-color: #27D980;
color: #000000;
border-color: #27D980;
}
button.btn:active {
background-color: #1fa865;
color: #000000;
border-color: #1fa865;
}
button.btn:disabled {
background-color: #080808;
color: #2A2A2A;
border-color: #181818;
}
button.btn-primary {
background-color: #FF4E00;
color: #000000;
border: 1px solid #FF4E00;
border-radius: 18px;
padding: 8px 16px;
margin: 5px 6px;
font-size: 8pt;
font-weight: bold;
letter-spacing: 1px;
min-width: 130px;
}
button.btn-primary:hover {
background-color: #27D980;
color: #000000;
border-color: #27D980;
}
button.btn-primary:disabled {
background-color: #301200;
color: #553322;
border-color: #301200;
}
/* Notebook / Tabs */
notebook > header {
background-color: #000000;
border-bottom: 1px solid #1C1C1C;
padding: 0px;
}
notebook > header tabs {
background-color: #000000;
}
tab {
background-color: #000000;
color: #444444;
border: none;
border-bottom: 2px solid transparent;
padding: 10px 22px;
font-size: 8pt;
letter-spacing: 1px;
font-weight: bold;
}
tab:checked {
background-color: #000000;
color: #FF4E00;
border-bottom: 2px solid #FF4E00;
}
tab:hover {
color: #AAAAAA;
}
/* Info grid */
label.info-key {
color: #555555;
font-size: 8pt;
min-width: 90px;
}
label.info-val {
color: #CCCCCC;
font-size: 8pt;
}
/* Log area */
textview.log-view {
background-color: #050505;
color: #27D980;
font-family: "DejaVu Sans Mono", "Monospace", monospace;
font-size: 7.5pt;
padding: 8px;
}
textview.log-view text {
background-color: #050505;
color: #27D980;
}
scrolledwindow.log-scroll {
border: 1px solid #1C1C1C;
border-radius: 8px;
margin: 4px 14px 8px 14px;
}
/* Separadores */
separator {
background-color: #141414;
min-height: 1px;
}
"""
# ── 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 principal ────────────────────────────────────────────────────────
class OasisPanel(Gtk.ApplicationWindow):
def __init__(self, app):
super().__init__(application=app, title="SOLAR NET HUB")
self.set_default_size(390, 610)
self.set_resizable(False)
self.set_position(Gtk.WindowPosition.CENTER)
self._build_ui()
# Poll de estado cada 3 s
GLib.timeout_add_seconds(3, self._trigger_poll)
self._trigger_poll()
# ── Construcción UI ───────────────────────────────────────────────────
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)
self.notebook = Gtk.Notebook()
self.notebook.set_show_border(False)
root.pack_start(self.notebook, True, True, 0)
self.notebook.append_page(self._make_oasis_tab(), self._tab_lbl("OASIS"))
self.notebook.append_page(self._make_ecoin_tab(), self._tab_lbl("ECOIN"))
self.notebook.append_page(self._make_sistema_tab(), self._tab_lbl("SISTEMA"))
def _tab_lbl(self, txt):
return Gtk.Label(label=txt)
# ── Header ────────────────────────────────────────────────────────────
def _make_header(self):
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
box.get_style_context().add_class("panel-header")
box.set_margin_top(14)
box.set_margin_bottom(14)
box.set_margin_start(18)
box.set_margin_end(18)
logo = SCRIPT_DIR / "oasis-logo.png"
if logo.is_file():
try:
pb = GdkPixbuf.Pixbuf.new_from_file_at_scale(str(logo), 30, 30, True)
box.pack_start(Gtk.Image.new_from_pixbuf(pb), False, False, 0)
except Exception:
pass
title = Gtk.Label(label="SOLAR NET HUB")
title.get_style_context().add_class("panel-title")
box.pack_start(title, False, False, 0)
box.pack_start(Gtk.Box(), True, True, 0) # spacer
self.header_dot = Gtk.Label(label="")
self.header_dot.get_style_context().add_class("dot")
self.header_dot.get_style_context().add_class("dot-unknown")
box.pack_end(self.header_dot, False, False, 4)
return box
# ── Tab OASIS ─────────────────────────────────────────────────────────
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 —
card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
card.get_style_context().add_class("status-card")
card.set_margin_top(16)
card.set_margin_bottom(4)
card.set_margin_start(14)
card.set_margin_end(14)
card.set_margin_top(14)
for edge in ("top", "bottom", "start", "end"):
getattr(card, f"set_margin_{edge}")(14)
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
row.set_margin_top(14)
row.set_margin_start(16)
row.set_margin_end(16)
self.oasis_dot = Gtk.Label(label="")
self.oasis_dot.get_style_context().add_class("dot")
self.oasis_dot.get_style_context().add_class("dot-unknown")
row.pack_start(self.oasis_dot, False, False, 0)
self.oasis_state_lbl = Gtk.Label(label="Comprobando…")
self.oasis_state_lbl.get_style_context().add_class("status-main")
self.oasis_state_lbl.get_style_context().add_class("state-unknown")
row.pack_start(self.oasis_state_lbl, False, False, 0)
card.pack_start(row, False, False, 0)
self.oasis_sub_lbl = Gtk.Label(label="")
self.oasis_sub_lbl.get_style_context().add_class("status-sub")
self.oasis_sub_lbl.set_halign(Gtk.Align.START)
self.oasis_sub_lbl.set_margin_start(16)
self.oasis_sub_lbl.set_margin_bottom(14)
card.pack_start(self.oasis_sub_lbl, False, False, 0)
box.pack_start(card, 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(10)
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(6)
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)
# — Separator + info —
sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
sep.set_margin_top(10)
box.pack_start(sep, False, False, 0)
grid = Gtk.Grid()
grid.set_column_spacing(14)
grid.set_row_spacing(6)
grid.set_margin_start(18)
grid.set_margin_end(18)
grid.set_margin_top(10)
grid.set_margin_bottom(14)
self.oasis_ver_val = self._info_row(grid, 0, "Versión")
self.oasis_node_val = self._info_row(grid, 1, "Node.js")
self.oasis_dir_val = self._info_row(grid, 2, "Ruta")
box.pack_start(grid, False, False, 0)
return scroll
# ── Tab ECOIN ─────────────────────────────────────────────────────────
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 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
card.get_style_context().add_class("status-card")
for edge in ("top", "bottom", "start", "end"):
getattr(card, f"set_margin_{edge}")(14)
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
row.set_margin_top(14)
row.set_margin_start(16)
row.set_margin_end(16)
self.ecoin_dot = Gtk.Label(label="")
self.ecoin_dot.get_style_context().add_class("dot")
self.ecoin_dot.get_style_context().add_class("dot-unknown")
row.pack_start(self.ecoin_dot, False, False, 0)
self.ecoin_state_lbl = Gtk.Label(label="Comprobando…")
self.ecoin_state_lbl.get_style_context().add_class("status-main")
self.ecoin_state_lbl.get_style_context().add_class("state-unknown")
row.pack_start(self.ecoin_state_lbl, False, False, 0)
card.pack_start(row, False, False, 0)
self.ecoin_sub_lbl = Gtk.Label(label="")
self.ecoin_sub_lbl.get_style_context().add_class("status-sub")
self.ecoin_sub_lbl.set_halign(Gtk.Align.START)
self.ecoin_sub_lbl.set_margin_start(16)
self.ecoin_sub_lbl.set_margin_bottom(14)
card.pack_start(self.ecoin_sub_lbl, False, False, 0)
box.pack_start(card, 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(10)
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)
# — Botones fila 2 —
r2 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
r2.set_halign(Gtk.Align.CENTER)
r2.set_margin_bottom(6)
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)
# — Separator + info —
sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
sep.set_margin_top(10)
box.pack_start(sep, False, False, 0)
grid = Gtk.Grid()
grid.set_column_spacing(14)
grid.set_row_spacing(6)
grid.set_margin_start(18)
grid.set_margin_end(18)
grid.set_margin_top(10)
grid.set_margin_bottom(14)
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")
box.pack_start(grid, False, False, 0)
return scroll
# ── Tab SISTEMA ───────────────────────────────────────────────────────
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("info-key")
hdr.set_halign(Gtk.Align.START)
hdr.set_margin_start(18)
hdr.set_margin_top(12)
hdr.set_margin_bottom(4)
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_margin_start(18)
btn.set_margin_end(18)
btn.set_margin_top(8)
btn.set_margin_bottom(12)
btn.set_halign(Gtk.Align.CENTER)
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
# ── Dot / state helpers ───────────────────────────────────────────────
def _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 _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 —
hstate = "running" if o_run else ("stopped" if o_inst else "unknown")
self._dot(self.header_dot, hstate)
# — OASIS —
self._dot(self.oasis_dot, "running" if o_run else ("stopped" if o_inst else "unknown"))
if o_run:
self._state(self.oasis_state_lbl, "ACTIVO", "running")
self.oasis_sub_lbl.set_text("servidor 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._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._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 —
self._dot(self.ecoin_dot, "running" if e_qt else ("stopped" if e_inst else "unknown"))
if e_inst:
self._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._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("" if e_wall else "No")
self.ecoin_qt_val.set_text("" if e_qt else "No")
self.ecoin_daemon_val.set_text("" if e_dmn else "No")
# ── Acciones ─────────────────────────────────────────────────────────
def _run(self, *args):
"""Lanza installer.sh con args, redirige output al log."""
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 (código {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
# OASIS
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 …")
# ECOIN
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")
# Sistema
def _on_clear_log(self, _):
self.log_buf.set_text("")
# ── Aplicación ─────────────────────────────────────────────────────────────
class OasisApp(Gtk.Application):
def __init__(self):
super().__init__(application_id="net.solarnethub.panel")
def do_activate(self):
win = OasisPanel(self)
win.show_all()
def main():
# Cargar CSS
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()

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 KiB

View file

@ -1,400 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# =========================
# SOLAR NET HUB - Installer
# =========================
# --- Rutas ---
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
: "${SELF:=$SCRIPT_DIR/installer.sh}" # ruta absoluta a este script
INSTALLER_DIR="$SCRIPT_DIR"
CSS_SOURCE="$INSTALLER_DIR/gtk.css"
HERO_IMAGE="$INSTALLER_DIR/oasis-ecoin.png" # imagen con los dos logos (head)
# ICONOS INDIVIDUALES (se usan en ensure_local_icons y en el .desktop)
ICON_OASIS="$INSTALLER_DIR/oasis-logo.png"
ICON_ECOIN="$INSTALLER_DIR/ecoin.png"
APPS_DIR="$HOME/.local/share/applications"
DESKTOP_DIR="$(xdg-user-dir DESKTOP 2>/dev/null || echo "$HOME/Desktop")"
# --- Tema local (no toca sistema) ---
NAME="SolarHubInstaller"
THEME_NAME="SolarHub"
YAD_THEME="${THEME_NAME}:dark"
ORANGE="#FF4E00"
GREEN="#27D980"
# ========== Helpers ==========
have(){ command -v "$1" >/dev/null 2>&1; }
# Iconos locales (sin instalar nada en el sistema/usuario)
ensure_local_icons(){
local base="$INSTALLER_DIR/icons/hicolor/256x256/apps"
local idx="$INSTALLER_DIR/icons/hicolor/index.theme"
mkdir -p "$base"
install -m 0644 "$ICON_OASIS" "$base/oasis-logo.png"
install -m 0644 "$ICON_ECOIN" "$base/ecoin.png"
cat > "$idx" <<'EOF'
[Icon Theme]
Name=Hicolor
Comment=Local fallback for Solar Net Hub
Directories=256x256/apps
[256x256/apps]
Size=256
Context=Applications
Type=Fixed
EOF
# Hacer visible este tema SOLO para este proceso y descendientes
export GTK_ICON_THEME_PATH="$INSTALLER_DIR/icons${GTK_ICON_THEME_PATH:+:$GTK_ICON_THEME_PATH}"
}
pkg_install(){
local pkgs=("$@")
if have apt-get; then
if have pkexec; then pkexec bash -lc "apt-get update && apt-get install -y ${pkgs[*]}" || sudo apt-get update && sudo apt-get install -y "${pkgs[@]}";
else sudo apt-get update && sudo apt-get install -y "${pkgs[@]}"; fi
elif have pacman; then
if have pkexec; then pkexec bash -lc "pacman -Sy --noconfirm ${pkgs[*]}" || sudo pacman -Sy --noconfirm "${pkgs[@]}";
else sudo pacman -Sy --noconfirm "${pkgs[@]}"; fi
elif have dnf; then
if have pkexec; then pkexec bash -lc "dnf install -y ${pkgs[*]}" || sudo dnf install -y "${pkgs[@]}";
else sudo dnf install -y "${pkgs[@]}"; fi
elif have zypper; then
if have pkexec; then pkexec bash -lc "zypper --non-interactive install ${pkgs[*]}" || sudo zypper --non-interactive install "${pkgs[@]}";
else sudo zypper --non-interactive install "${pkgs[@]}"; fi
else
echo "❌ Gestor de paquetes no soportado." >&2; exit 1
fi
}
ensure_basics(){
local miss=()
have yad || miss+=("yad")
if ! command -v pkexec >/dev/null 2>&1; then
if have apt-get; then miss+=("policykit-1"); else miss+=("polkit"); fi
fi
have xdg-user-dirs-update || miss+=("xdg-user-dirs")
if command -v dpkg >/dev/null 2>&1; then
dpkg -s hicolor-icon-theme >/dev/null 2>&1 || miss+=("hicolor-icon-theme")
elif command -v rpm >/dev/null 2>&1; then
rpm -q hicolor-icon-theme >/dev/null 2>&1 || miss+=("hicolor-icon-theme")
fi
if ((${#miss[@]})); then
pkg_install "${miss[@]}"
fi
}
ensure_theme(){
local tdir="$HOME/.themes/${THEME_NAME}/gtk-3.0"
mkdir -p "$tdir"
if [ -f "$CSS_SOURCE" ]; then
sed 's/\r$//' "$CSS_SOURCE" > "$tdir/gtk.css"
else
cat > "$tdir/gtk.css" <<'CSS'
*{background:#000;color:#E6E6E6;font-family:"Cantarell","Ubuntu","DejaVu Sans",sans-serif;font-size:12pt}
button{background:#FF4E00;color:#000;border:none;border-radius:16px;padding:12px 18px;font-weight:800}
button:hover{background:#ff6a26} button:active{background:#e24a00}
CSS
fi
}
yad_cmd(){ GTK_THEME="$YAD_THEME" NO_AT_BRIDGE=1 yad --name="$NAME" --class="$NAME" "$@"; }
progress_user(){ # $1 título, $2 comando (string)
bash -lc "$2" 2>&1 | yad_cmd --progress --title="$1" --pulsate --auto-close --no-buttons \
--width=800 --height=260 --center
}
progress_root(){ # $1 título, $2 comando (string, root)
pkexec bash -lc "$2" 2>&1 | yad_cmd --progress --title="$1" --pulsate --auto-close --no-buttons \
--width=800 --height=260 --center
}
ensure_launcher(){
mkdir -p "$APPS_DIR" "$DESKTOP_DIR"
local ABS_INSTALLER="$SCRIPT_DIR/installer.sh"
local ABS_ICON="$ICON_OASIS"; [ -f "$ABS_ICON" ] || ABS_ICON="$HERO_IMAGE"
cat > "$APPS_DIR/INSTALLER.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=OASIS Installer
Comment=Install or launch OASIS & ECOIN
TryExec=/bin/bash
Exec=/bin/bash -lc '$ABS_INSTALLER'
Icon=$ABS_ICON
Terminal=false
Categories=System;Utility;
StartupNotify=true
EOF
cp -f "$APPS_DIR/INSTALLER.desktop" "$DESKTOP_DIR/INSTALLER.desktop" 2>/dev/null || true
chmod 644 "$APPS_DIR/INSTALLER.desktop" 2>/dev/null || true
chmod +x "$DESKTOP_DIR/INSTALLER.desktop" 2>/dev/null || true
command -v update-desktop-database >/dev/null 2>&1 && \
update-desktop-database "$HOME/.local/share/applications" >/dev/null 2>&1 || true
}
ensure_user_fonts(){
local FONTS_SRC_DIR="$INSTALLER_DIR"
local FONTS_DST_DIR="$HOME/.local/share/fonts"
local copied=0
mkdir -p "$FONTS_DST_DIR"
# Copia Dune Rise (OTF/TTF) si existe en INSTALLER
for f in "$FONTS_SRC_DIR"/Dune_Rise.otf "$FONTS_SRC_DIR"/Dune_Rise.ttf; do
if [ -f "$f" ]; then
cp -f "$f" "$FONTS_DST_DIR/" && copied=1
fi
done
# Refresca caché si hemos copiado algo
if [ "$copied" = 1 ]; then
fc-cache -f >/dev/null 2>&1 || true
fi
}
# ========== Estado ==========
OASIS_DIR_DEFAULT="${HOME}/oasis"
OASIS_REPO_DEFAULT="https://code.03c8.net/KrakensLab/oasis.git"
OASIS_MODEL_FILE="oasis-42-1-chat.Q4_K_M.gguf"
OASIS_MODEL_TAR="${OASIS_MODEL_FILE}.tar.gz"
OASIS_MODEL_URL="https://solarnethub.com/code/models/${OASIS_MODEL_TAR}"
oasis_installed(){ local d="${1:-$OASIS_DIR_DEFAULT}"; [[ -d "$d/src/server/node_modules" && -f "$d/AI/$OASIS_MODEL_FILE" ]]; }
ECOIN_DIR_DEFAULT="${HOME}/ecoin"
ECOIN_REPO_DEFAULT="https://github.com/epsylon/ecoin"
ecoin_installed(){ local d="${1:-$ECOIN_DIR_DEFAULT}"; [[ -x "$d/ecoin/ecoin-qt" || -x "$d/ecoin/src/ecoind" ]]; }
ecoin_wallet_exists(){ [ -f "$HOME/.ecoin/wallet.dat" ]; }
# ========== OASIS ==========
node_setup_cmd(){ cat <<EOF
set -e
apt-get update
apt-get install -y git curl tar ca-certificates gnupg
curl -fsSL https://deb.nodesource.com/setup_${1}.x | bash -
apt-get install -y nodejs
node -v && npm -v
EOF
}
oasis_install_cmd(){ cat <<EOF
set -e
if [ ! -d "$2/.git" ]; then git clone --depth 1 "$1" "$2"; else cd "$2" && git pull --rebase || true; fi
cd "$2/src/server"
npm install .
npm audit fix || true
EOF
}
oasis_model_cmd(){ cat <<EOF
set -e
MODEL_DIR="$1/AI"; mkdir -p "\$MODEL_DIR"
if [ ! -f "\$MODEL_DIR/$OASIS_MODEL_FILE" ]; then
curl -L -o "\$MODEL_DIR/$OASIS_MODEL_TAR" "$OASIS_MODEL_URL"
tar -xzf "\$MODEL_DIR/$OASIS_MODEL_TAR" -C "\$MODEL_DIR"
rm "\$MODEL_DIR/$OASIS_MODEL_TAR"
fi
EOF
}
oasis_start_cmd(){ cat <<EOF
set -e
if [ -x "$1/oasis.sh" ]; then cd "$1" && exec bash oasis.sh; else cd "$1/src/server" && exec node server.js; fi
EOF
}
# ========== ECOIN ==========
ecoin_deps_cmd(){ cat <<'EOF'
set -e
apt-get update
apt-get install -y qt5-qmake qtbase5-dev qttools5-dev-tools \
build-essential libssl-dev libssl3 libdb5.3-dev libdb5.3++-dev \
libleveldb-dev miniupnpc libminiupnpc-dev libqrencode-dev patchelf git curl
EOF
}
ecoin_clone_or_pull_cmd(){ cat <<EOF
set -e
if [ ! -d "$2/.git" ]; then git clone "$1" "$2"; else cd "$2" && git pull --rebase || true; fi
EOF
}
ecoin_build_qt_cmd(){ cat <<EOF
set -e
cd "$1/ecoin"
QMAKE="/usr/lib/x86_64-linux-gnu/qt5/bin/qmake"; [ -x "\$QMAKE" ] || QMAKE="\$(command -v qmake || true)"
[ -n "\$QMAKE" ] || { echo "qmake no encontrado"; exit 1; }
"\$QMAKE" USE_UPNP=- USE_IPV6=- -o Makefile ecoin-qt.pro
qmake USE_UPNP=- USE_IPV6=-
make -j"\$(nproc)" || make
EOF
}
ecoin_build_daemon_cmd(){ cat <<EOF
set -e
cd "$1/ecoin/src"
make -f makefile.linux USE_UPNP=- USE_IPV6=- -j"\$(nproc)" || make -f makefile.linux USE_UPNP=- USE_IPV6=-
strip ecoind || true
EOF
}
ecoin_qt_cmd(){ cat <<EOF
set -e
if [ -x "$1/ecoin/ecoin-qt" ]; then cd "$1/ecoin" && exec ./ecoin-qt; else echo "ecoin-qt no encontrado"; fi
EOF
}
ensure_ecoin_conf_cmd(){ cat <<'EOF'
set -e
CONF="$HOME/.ecoin/ecoin.conf"
mkdir -p "$HOME/.ecoin"
if [ ! -f "$CONF" ]; then
PW="$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 24)"
cat > "$CONF" <<CFG
rpcuser=ecoinrpc
rpcpassword=${PW}
rpcallowip=127.0.0.1
testnet=0
noirc=1
listen=1
server=1
daemon=1
addnode=46.163.118.220
CFG
chmod 600 "$CONF"
fi
EOF
}
ecoin_create_wallet_cmd(){ cat <<'EOF'
set -e
CONF="$HOME/.ecoin/ecoin.conf"
EOF
printf "%s\n" "$(ensure_ecoin_conf_cmd)"
cat <<'EOF'
if [ -x "$HOME/ecoin/ecoin/ecoin-qt" ]; then
cd "$HOME/ecoin/ecoin" && ./ecoin-qt || true
elif [ -x "$HOME/ecoin/ecoin/src/ecoind" ]; then
cd "$HOME/ecoin/ecoin/src" && ./ecoind -daemon || true
else
echo "ECOIN no está compilado. Instálalo primero."
fi
EOF
}
ecoin_connect_wallet_cmd(){ cat <<'EOF'
set -e
CONF="$HOME/.ecoin/ecoin.conf"
EOF
printf "%s\n" "$(ensure_ecoin_conf_cmd)"
cat <<'EOF'
if [ -x "$HOME/ecoin/ecoin/src/ecoind" ]; then
cd "$HOME/ecoin/ecoin/src" && ./ecoind -daemon
else
echo "ecoind no encontrado. Abre ECOIN (Qt) o recompila."
fi
EOF
}
# ========== Flujos rápidos ==========
oasis_quick_install(){
local major="${1:-22}" repo="${2:-$OASIS_REPO_DEFAULT}" dir="${3:-$OASIS_DIR_DEFAULT}"
progress_root "OASIS: Node $major" "$(node_setup_cmd "$major")"
progress_user "OASIS: Repo + npm" "$(oasis_install_cmd "$repo" "$dir")"
progress_user "OASIS: Modelo IA" "$(oasis_model_cmd "$dir")"
}
oasis_quick_start(){ local dir="${1:-$OASIS_DIR_DEFAULT}"; progress_user "OASIS: Arrancar" "$(oasis_start_cmd "$dir")"; }
ecoin_quick_install(){
local repo="${1:-$ECOIN_REPO_DEFAULT}" dir="${2:-$ECOIN_DIR_DEFAULT}"
progress_root "ECOIN: Dependencias" "$(ecoin_deps_cmd)"
progress_user "ECOIN: Clonar/Pull" "$(ecoin_clone_or_pull_cmd "$repo" "$dir")"
progress_user "ECOIN: Build Qt" "$(ecoin_build_qt_cmd "$dir")"
progress_user "ECOIN: Build Daemon" "$(ecoin_build_daemon_cmd "$dir")"
}
# ========== Home: 2 columnas x 4 filas (orden fijo, sin vars sueltas) ==========
# ========= Home: 2 columnas x 4 filas (orden fijo + lógica básica) =========
home_single_dialog(){
# Estados rápidos
local O_INST=0 E_INST=0 E_WAL=0
oasis_installed && O_INST=1
ecoin_installed && E_INST=1
ecoin_wallet_exists && E_WAL=1
# Versión OASIS (si existe)
local O_VER="—"
if [ -f "$OASIS_DIR_DEFAULT/src/server/package.json" ]; then
O_VER="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \
"$OASIS_DIR_DEFAULT/src/server/package.json" | head -n1)"
[ -z "$O_VER" ] && O_VER="—"
fi
# IZQUIERDA (OASIS)
local L1_NAME="OASIS:BTN"
local L1_VAL="/bin/bash -lc 'xdg-open https://oasis-project.pub >/dev/null 2>&1 &'"
local L2_NAME L2_VAL
if (( O_INST )); then L2_NAME="INSTALADO:BTN"; L2_VAL="/bin/true"
else L2_NAME="INSTALAR:BTN"; L2_VAL="/bin/bash -lc \"$SELF --oasis-install\""; fi
local L3_NAME="ABRIR:BTN"
local L3_VAL; (( O_INST )) && L3_VAL="/bin/bash -lc \"$SELF --oasis-start\"" || L3_VAL="/bin/true"
local L4_NAME="VERSIÓN ( $O_VER ):BTN"
local L4_VAL="/bin/true"
# DERECHA (ECOIN)
local R1_NAME="ECOIN:BTN"
local R1_VAL="/bin/bash -lc 'xdg-open https://ecoin.03c8.net/ >/dev/null 2>&1 &'"
local R2_NAME R2_VAL
if (( E_INST )); then R2_NAME="INSTALADO:BTN"; R2_VAL="/bin/true"
else R2_NAME="INSTALAR:BTN"; R2_VAL="/bin/bash -lc \"$SELF --ecoin-install\""; fi
local R3_NAME="CONECTAR CARTERA:BTN"
local R3_VAL; (( E_INST )) && R3_VAL="/bin/bash -lc \"$SELF --wallet-connect\"" || R3_VAL="/bin/true"
local R4_NAME="ABRIR GUI:BTN"
local R4_VAL; (( E_INST )) && R4_VAL="/bin/bash -lc \"$SELF --ecoin-gui\"" || R4_VAL="/bin/true"
# DIÁLOGO (F1 izq, F1 dcha, F2 izq, F2 dcha, ...)
local DIALOG_ARGS=(
--name="$NAME" --class="$NAME"
--title="SOLAR NET HUB" --window-icon="$ICON_OASIS"
--center --width=1200 --height=780
--image="$HERO_IMAGE" --image-on-top
--form --borders=18 --columns=2 --align=center
# Fila 1
--field="$L1_NAME" "$L1_VAL"
--field="$R1_NAME" "$R1_VAL"
# Fila 2
--field="$L2_NAME" "$L2_VAL"
--field="$R2_NAME" "$R2_VAL"
# Fila 3
--field="$L3_NAME" "$L3_VAL"
--field="$R3_NAME" "$R3_VAL"
# Fila 4
--field="$L4_NAME" "$L4_VAL"
--field="$R4_NAME" "$R4_VAL"
--buttons-layout=center
--button="Cerrar:0"
)
yad_cmd "${DIALOG_ARGS[@]}"
}
# ========== Entrypoint ==========
ensure_basics
ensure_user_fonts 2>/dev/null || true
ensure_local_icons
ensure_theme
ensure_launcher
case "${1:-}" in
--oasis-install) oasis_quick_install 22 "$OASIS_REPO_DEFAULT" "$OASIS_DIR_DEFAULT" ;;
--oasis-start) oasis_quick_start "$OASIS_DIR_DEFAULT" ;;
--ecoin-install) ecoin_quick_install "$ECOIN_REPO_DEFAULT" "$ECOIN_DIR_DEFAULT" ;;
--wallet-create) progress_user "ECOIN: Crear cartera" "$(ecoin_create_wallet_cmd)" ;;
--wallet-connect)progress_user "ECOIN: Conectar cartera" "$(ecoin_connect_wallet_cmd)" ;;
--ecoin-gui) progress_user "ECOIN: Wallet (Qt)" "$(ecoin_qt_cmd "$ECOIN_DIR_DEFAULT")" ;;
*) home_single_dialog ;;
esac

Binary file not shown.

Before

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

View file

@ -1,746 +0,0 @@
#!/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()

View file

@ -1,77 +0,0 @@
#!/usr/bin/env bash
# =============================================================
# SOLAR NET HUB — Panel de control (lanzador)
# Lanza panel.py si python3-gi está disponible;
# si no, lo instala y reintenta.
# =============================================================
set -euo pipefail
REPO_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
PANEL_PY="$REPO_DIR/INSTALLER/panel.py"
INSTALLER_SH="$REPO_DIR/INSTALLER/installer.sh"
C_RESET='\033[0m'
C_OK='\033[1;32m'
C_FAIL='\033[1;31m'
C_WARN='\033[1;33m'
C_INFO='\033[1;36m'
have(){ command -v "$1" >/dev/null 2>&1; }
# ── Instalar fuente Dune Rise (usuario, sin root) ─────────────────────────
install_font(){
local src="$REPO_DIR/INSTALLER/Dune_Rise.otf"
local dst="$HOME/.local/share/fonts/Dune_Rise.otf"
if [ -f "$src" ] && [ ! -f "$dst" ]; then
mkdir -p "$HOME/.local/share/fonts"
cp -f "$src" "$dst"
fc-cache -f >/dev/null 2>&1 || true
fi
}
# ── Verificar / instalar python3-gi ──────────────────────────────────────
check_gi(){
python3 -c "import gi; gi.require_version('Gtk','3.0'); from gi.repository import Gtk" \
>/dev/null 2>&1
}
install_gi(){
echo -e "${C_WARN}⚠ python3-gi no encontrado. Instalando…${C_RESET}"
if have apt-get; then sudo apt-get install -y python3-gi python3-gi-cairo gir1.2-gtk-3.0
elif have pacman; then sudo pacman -Sy --noconfirm python-gobject
elif have dnf; then sudo dnf install -y python3-gobject gtk3
elif have zypper; then sudo zypper --non-interactive install python3-gobject gtk3
else
echo -e "${C_FAIL}✘ Gestor de paquetes no soportado.${C_RESET}"
echo " Instala manualmente: python3-gi / python-gobject"
exit 1
fi
}
# ── Verificar DISPLAY / Wayland ───────────────────────────────────────────
check_display(){
if [ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ]; then
echo -e "${C_FAIL}✘ No hay sesión gráfica (DISPLAY/WAYLAND_DISPLAY vacíos).${C_RESET}"
echo " Ejecuta este script desde tu escritorio o una terminal gráfica."
exit 1
fi
}
# ── Main ─────────────────────────────────────────────────────────────────
echo -e "${C_INFO}== SOLAR NET HUB :: Panel de Control ==${C_RESET}"
check_display
install_font 2>/dev/null || true
if ! check_gi; then
install_gi
if ! check_gi; then
echo -e "${C_FAIL}✘ No se pudo cargar python3-gi tras la instalación.${C_RESET}"
echo " Intenta cerrar sesión y volver a entrar, o instala python3-gi manualmente."
exit 1
fi
fi
echo -e "${C_OK}✔ python3-gi listo. Lanzando panel…${C_RESET}"
exec python3 "$PANEL_PY" "$@"

View file

@ -1,114 +0,0 @@
#!/usr/bin/env bash
# =============================================================
# SOLAR NET HUB — Panel v2 (lanzador + registro en menú)
# =============================================================
set -euo pipefail
REPO_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
PANEL_PY="$REPO_DIR/INSTALLER_V2/panel.py"
FONT_SRC="$REPO_DIR/INSTALLER_V2/Dune_Rise.otf"
ICON_SRC="$REPO_DIR/INSTALLER_V2/oasis-logo.png"
APPS_DIR="$HOME/.local/share/applications"
ICONS_DIR="$HOME/.local/share/icons/hicolor/256x256/apps"
DESKTOP_FILE="$APPS_DIR/solarnethub-panel.desktop"
C_RESET='\033[0m'; C_OK='\033[1;32m'; C_FAIL='\033[1;31m'
C_WARN='\033[1;33m'; C_INFO='\033[1;36m'
have(){ command -v "$1" >/dev/null 2>&1; }
check_display(){
if [ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ]; then
echo -e "${C_FAIL}✘ Sin sesión gráfica (DISPLAY/WAYLAND_DISPLAY vacíos).${C_RESET}"
exit 1
fi
}
install_font(){
local dst="$HOME/.local/share/fonts/Dune_Rise.otf"
if [ -f "$FONT_SRC" ] && [ ! -f "$dst" ]; then
mkdir -p "$HOME/.local/share/fonts"
cp -f "$FONT_SRC" "$dst"
fc-cache -f >/dev/null 2>&1 || true
echo -e "${C_OK}✔ Fuente Dune Rise instalada.${C_RESET}"
fi
}
register_desktop(){
mkdir -p "$APPS_DIR" "$ICONS_DIR"
# Copiar icono al directorio de iconos del usuario
if [ -f "$ICON_SRC" ]; then
cp -f "$ICON_SRC" "$ICONS_DIR/solarnethub.png"
fi
# Crear .desktop apuntando a este lanzador
cat > "$DESKTOP_FILE" <<EOF
[Desktop Entry]
Type=Application
Name=Solar Net Hub
GenericName=OASIS & ECOIN Panel
Comment=Panel de control para OASIS y ECOIN
Exec=/bin/bash -lc '$REPO_DIR/start_panel_v2.sh'
Icon=solarnethub
Terminal=false
Categories=Network;System;Utility;
Keywords=oasis;ecoin;solar;hub;crypto;
StartupWMClass=solarnethub.panel.v2
StartupNotify=true
EOF
chmod 644 "$DESKTOP_FILE"
# Copiar también al Escritorio si existe
local desktop_dir
desktop_dir="$(xdg-user-dir DESKTOP 2>/dev/null || echo "$HOME/Desktop")"
if [ -d "$desktop_dir" ]; then
cp -f "$DESKTOP_FILE" "$desktop_dir/solarnethub-panel.desktop"
chmod +x "$desktop_dir/solarnethub-panel.desktop" 2>/dev/null || true
fi
# Refrescar cache de aplicaciones e iconos
have update-desktop-database && \
update-desktop-database "$APPS_DIR" >/dev/null 2>&1 || true
have gtk-update-icon-cache && \
gtk-update-icon-cache -f "$HOME/.local/share/icons/hicolor" >/dev/null 2>&1 || true
echo -e "${C_OK}✔ Registrado en el menú: 'Solar Net Hub'${C_RESET}"
}
check_gi(){
python3 -c "import gi; gi.require_version('Gtk','3.0'); from gi.repository import Gtk" \
>/dev/null 2>&1
}
install_gi(){
echo -e "${C_WARN}⚠ python3-gi no encontrado. Instalando...${C_RESET}"
if have apt-get; then sudo apt-get install -y python3-gi python3-gi-cairo gir1.2-gtk-3.0
elif have pacman; then sudo pacman -Sy --noconfirm python-gobject
elif have dnf; then sudo dnf install -y python3-gobject gtk3
elif have zypper; then sudo zypper --non-interactive install python3-gobject gtk3
else
echo -e "${C_FAIL}✘ Gestor no soportado. Instala manualmente: python3-gi${C_RESET}"
exit 1
fi
}
# ── Main ──────────────────────────────────────────────────────────────────
echo -e "${C_INFO}== SOLAR NET HUB :: Panel v2 ==${C_RESET}"
check_display
install_font 2>/dev/null || true
register_desktop 2>/dev/null || true
if ! check_gi; then
install_gi
if ! check_gi; then
echo -e "${C_FAIL}✘ No se pudo cargar python3-gi.${C_RESET}"
exit 1
fi
fi
echo -e "${C_OK}✔ Lanzando panel...${C_RESET}"
exec python3 "$PANEL_PY" "$@"