fix(panel-v3): detección de puerto, botones y limpieza de warnings

- oasis_running() usa ss en lugar de pgrep (evita falsos positivos)
- _start_oasis() lanza oasis.sh en background y espera el puerto
- INSTALAR se deshabilita cuando OASIS ya está instalado/activo
- Corrige DeprecationWarning: override_background_color → CssProvider
- Corrige DeprecationWarning: run_javascript → evaluate_javascript
- Desactiva caché de WebKit, carga por URI directa
- Inspector de WebKit activado para depuración

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
SITO 2026-03-30 12:26:24 +02:00
parent ae79e45c19
commit 67acbf1add
17 changed files with 2692 additions and 0 deletions

BIN
INSTALLER_V3/Dune_Rise.otf Normal file

Binary file not shown.

BIN
INSTALLER_V3/ecoin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

400
INSTALLER_V3/installer.sh Executable file
View file

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

BIN
INSTALLER_V3/oasis-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

349
INSTALLER_V3/panel.py Executable file
View file

@ -0,0 +1,349 @@
#!/usr/bin/env python3
"""
OASIS Control Panel v3 WebKit2GTK
UI en HTML/CSS/JS dentro de ventana GTK nativa.
Sin interferencias del tema GTK control visual total como Electron.
"""
import gi
gi.require_version('Gtk', '3.0')
# Intentar WebKit2 4.1 primero, luego 4.0
_wk = None
for _v in ('4.1', '4.0'):
try:
gi.require_version('WebKit2', _v)
_wk = _v
break
except ValueError:
pass
if _wk is None:
print("ERROR: WebKit2GTK no encontrado. Instala: gir1.2-webkit2-4.0")
raise SystemExit(1)
from gi.repository import Gtk, WebKit2, GLib, Gdk
import subprocess
import sys
import signal
import threading
import json
from pathlib import Path
# ── Rutas base ─────────────────────────────────────────────────────────────
SCRIPT_DIR = Path(__file__).parent.resolve()
INSTALLER_SH = SCRIPT_DIR / "installer.sh"
ECOIN_DIR = Path.home() / "ecoin"
# Estado persistente (mismo fichero que usa el installer original)
_STATE_FILE = Path.home() / ".config" / "oasis-installer" / "state"
# Candidatos donde puede estar OASIS instalado
_OASIS_CANDIDATES = [
Path.home() / "COFRE" / "CODERS" / "oasis",
Path.home() / "oasis",
Path.home() / "Projects" / "oasis",
Path.home() / "Proyectos" / "oasis",
Path.home() / "Documentos" / "oasis",
Path.home() / "Documents" / "oasis",
Path.home() / "src" / "oasis",
]
# ── Detección OASIS ────────────────────────────────────────────────────────
def _oasis_valid(d: Path) -> bool:
"""Flexible: cualquiera de estas señales indica que OASIS está ahí."""
return (
(d / "oasis.sh").is_file()
or (d / "src" / "server" / "server.js").is_file()
or (d / "src" / "server" / "node_modules").is_dir()
)
def _read_saved_dir() -> Path | None:
"""Lee la ruta guardada por el installer original."""
if _STATE_FILE.is_file():
for line in _STATE_FILE.read_text().splitlines():
if line.startswith("OASIS_DIR="):
p = Path(line[10:].strip())
if p.is_dir():
return p
return None
def find_oasis_dir() -> Path:
"""Devuelve la ruta de OASIS encontrada o ~/oasis como fallback."""
candidates = []
saved = _read_saved_dir()
if saved:
candidates.append(saved)
candidates.extend(_OASIS_CANDIDATES)
for d in candidates:
if d.is_dir() and _oasis_valid(d):
return d
return Path.home() / "oasis"
def oasis_installed() -> bool:
return _oasis_valid(find_oasis_dir())
def oasis_version() -> str:
pkg = find_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(port: int = 3000) -> bool:
"""Comprueba si hay algo escuchando en el puerto de OASIS (igual que el installer)."""
try:
r = subprocess.run(
["ss", "-lnt", f"sport = :{port}"],
capture_output=True, text=True, timeout=2
)
return str(port) in r.stdout
except Exception:
return False
# ── Detección ECOIN ────────────────────────────────────────────────────────
_ECOIN_CANDIDATES = [
Path.home() / "ecoin",
Path.home() / "COFRE" / "CODERS" / "ecoin",
Path.home() / "Projects" / "ecoin",
]
def find_ecoin_dir() -> Path:
for d in _ECOIN_CANDIDATES:
if (d / "ecoin" / "ecoin-qt").is_file() or \
(d / "ecoin" / "src" / "ecoind").is_file():
return d
return Path.home() / "ecoin"
def ecoin_installed() -> bool:
d = find_ecoin_dir()
return (d / "ecoin" / "ecoin-qt").is_file() or \
(d / "ecoin" / "src" / "ecoind").is_file()
def ecoin_wallet_exists() -> bool:
return (Path.home() / ".ecoin" / "wallet.dat").is_file()
def node_version() -> str:
try:
r = subprocess.run(["node", "--version"],
capture_output=True, text=True, timeout=3)
return r.stdout.strip()
except Exception:
return ""
# ── Panel principal ────────────────────────────────────────────────────────
class OasisPanel:
def __init__(self):
self.win = Gtk.Window()
self.win.set_title("SOLAR NET HUB")
self.win.set_default_size(390, 620)
self.win.set_resizable(False)
self.win.set_position(Gtk.WindowPosition.CENTER)
self.win.connect("destroy", Gtk.main_quit)
# Fondo negro en la ventana GTK para evitar parpadeo blanco al cargar
css = b"window { background-color: #000000; }"
provider = Gtk.CssProvider()
provider.load_from_data(css)
self.win.get_style_context().add_provider(
provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
# ── WebKit2 ───────────────────────────────────────────────────────
self.manager = WebKit2.UserContentManager()
self.manager.connect("script-message-received::bridge", self._on_message)
self.manager.register_script_message_handler("bridge")
settings = WebKit2.Settings()
settings.set_enable_javascript(True)
settings.set_enable_developer_extras(True) # activa inspector por si hace falta depurar
settings.set_allow_file_access_from_file_urls(True)
settings.set_allow_universal_access_from_file_urls(True)
settings.set_enable_page_cache(False) # sin caché de página
self.webview = WebKit2.WebView.new_with_user_content_manager(self.manager)
self.webview.set_settings(settings)
self.webview.set_background_color(Gdk.RGBA(0, 0, 0, 1))
self.win.add(self.webview)
# Cargar HTML directamente por URI (evita caché de recursos)
html_file = SCRIPT_DIR / "ui" / "index.html"
self.webview.load_uri(f"file://{html_file}")
self.win.show_all()
# Poll inicial tras 1.5 s (tiempo para que cargue el HTML)
GLib.timeout_add(1500, self._initial_poll)
# Poll continuo cada 3 s
GLib.timeout_add_seconds(3, self._poll)
# ── Polling ───────────────────────────────────────────────────────────
def _initial_poll(self):
self._poll()
return False
def _poll(self):
threading.Thread(target=self._poll_thread, daemon=True).start()
return True
def _poll_thread(self):
o_dir = find_oasis_dir()
o_inst = _oasis_valid(o_dir)
o_run = oasis_running()
o_ver = oasis_version()
o_node = node_version()
e_dir = find_ecoin_dir()
e_inst = ecoin_installed()
e_wall = ecoin_wallet_exists()
e_qt = (e_dir / "ecoin" / "ecoin-qt").is_file()
e_dmn = (e_dir / "ecoin" / "src" / "ecoind").is_file()
data = {
"oasis_installed": o_inst,
"oasis_running": o_run,
"oasis_version": o_ver,
"node_version": o_node,
"oasis_dir": str(o_dir) if o_inst else "",
"ecoin_installed": e_inst,
"ecoin_wallet": e_wall,
"ecoin_qt": e_qt,
"ecoin_daemon": e_dmn,
"ecoin_dir": str(e_dir) if e_inst else "",
}
GLib.idle_add(self._js, f"updateStatus({json.dumps(data)})")
# ── JS helpers ────────────────────────────────────────────────────────
def _js(self, code):
# evaluate_javascript (WebKit2 4.1+) o run_javascript (4.0 fallback)
if hasattr(self.webview, "evaluate_javascript"):
self.webview.evaluate_javascript(code, -1, None, None, None, None, None)
else:
self.webview.run_javascript(code, None, None, None)
return False
def _log(self, text):
self._js(f"appendLog({json.dumps(text)})")
# ── Mensaje desde JS ──────────────────────────────────────────────────
def _on_message(self, _manager, result):
try:
data = json.loads(result.get_value().to_string())
action = data.get("action", "")
except Exception:
return
self._dispatch(action)
def _dispatch(self, action):
installer_cmds = {
"oasis-install": ["--oasis-install"],
"ecoin-install": ["--ecoin-install"],
"ecoin-gui": ["--ecoin-gui"],
"ecoin-wallet": ["--wallet-create"],
"ecoin-connect": ["--wallet-connect"],
}
if action == "oasis-stop":
threading.Thread(target=self._kill_oasis, daemon=True).start()
GLib.idle_add(self._js, "switchTab('sistema')")
elif action == "oasis-start":
GLib.idle_add(self._js, "switchTab('sistema')")
threading.Thread(target=self._start_oasis, daemon=True).start()
elif action == "oasis-browser":
subprocess.Popen(["xdg-open", "http://localhost:3000"])
GLib.idle_add(self._log, "Abriendo http://localhost:3000 ...")
elif action in installer_cmds:
GLib.idle_add(self._js, "switchTab('sistema')")
args = installer_cmds[action]
threading.Thread(target=self._run_cmd, args=(args,), daemon=True).start()
def _run_cmd(self, args):
cmd = [str(INSTALLER_SH)] + args
GLib.idle_add(self._log, "$ " + " ".join(cmd))
try:
proc = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, bufsize=1
)
for line in iter(proc.stdout.readline, ""):
line = line.rstrip()
if line:
GLib.idle_add(self._log, line)
proc.wait()
GLib.idle_add(self._log, f"── fin (código {proc.returncode}) ──")
except Exception as e:
GLib.idle_add(self._log, f"[Error]: {e}")
GLib.idle_add(self._poll)
def _start_oasis(self):
import time
PORT = 3000
oasis_dir = find_oasis_dir()
oasis_sh = oasis_dir / "oasis.sh"
if not oasis_sh.is_file():
GLib.idle_add(self._log, f"[Error]: no se encontró oasis.sh en {oasis_dir}")
return
# ¿Ya está escuchando?
if oasis_running(PORT):
GLib.idle_add(self._log, f"OASIS ya está escuchando en el puerto {PORT}")
subprocess.Popen(["xdg-open", f"http://localhost:{PORT}"])
GLib.idle_add(self._poll)
return
# Lanzar en background igual que el installer original
cmd = f'cd "{oasis_dir}" && exec bash oasis.sh'
GLib.idle_add(self._log, f"$ nohup bash -lc '{cmd}' &")
try:
subprocess.Popen(
["bash", "-lc", cmd],
stdout=open("/tmp/oasis_gui.log", "w"),
stderr=subprocess.STDOUT,
start_new_session=True,
)
except Exception as e:
GLib.idle_add(self._log, f"[Error al lanzar]: {e}")
return
# Esperar hasta 30 s a que el puerto esté activo
GLib.idle_add(self._log, "Esperando que OASIS levante en el puerto 3000...")
for i in range(30):
time.sleep(1)
if oasis_running(PORT):
GLib.idle_add(self._log, f"OASIS levantado en {PORT}. Abriendo navegador...")
subprocess.Popen(["xdg-open", f"http://localhost:{PORT}"])
GLib.idle_add(self._poll)
return
if (i + 1) % 5 == 0:
GLib.idle_add(self._log, f" ... {i+1}s")
GLib.idle_add(self._log, "[Error]: OASIS no levantó en 30 s. Revisa /tmp/oasis_gui.log")
GLib.idle_add(self._poll)
def _kill_oasis(self):
GLib.idle_add(self._log, "Deteniendo OASIS...")
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]: {e}")
GLib.idle_add(self._poll)
# ── Main ───────────────────────────────────────────────────────────────────
def main():
signal.signal(signal.SIGINT, signal.SIG_DFL)
OasisPanel()
Gtk.main()
if __name__ == "__main__":
main()

110
INSTALLER_V3/ui/app.js Normal file
View file

@ -0,0 +1,110 @@
// =============================================================
// Solar Net Hub — app.js
// Bridge JS ↔ Python y lógica de UI
// =============================================================
// ── Bridge JS → Python ────────────────────────────────────────
function send(action) {
window.webkit.messageHandlers.bridge.postMessage(
JSON.stringify({ action })
);
}
// ── Tabs ──────────────────────────────────────────────────────
function switchTab(name) {
document.querySelectorAll('.tab-panel').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.tabBtn').forEach(el => el.classList.remove('active'));
const panel = document.getElementById('tab-' + name);
const btn = document.querySelector('[data-tab="' + name + '"]');
if (panel) panel.classList.add('active');
if (btn) btn.classList.add('active');
}
// ── Actualizar estado (llamado desde Python) ──────────────────
function updateStatus(data) {
// --- OASIS ---
const oCard = document.getElementById('oasisCard');
const state = data.oasis_running ? 'running'
: data.oasis_installed ? 'stopped'
: 'unknown';
oCard.dataset.state = state;
if (data.oasis_running) {
document.getElementById('oasisState').textContent = 'ACTIVO';
document.getElementById('oasisSub').textContent = 'servidor corriendo en puerto 3000';
_btn('btnStart', true);
_btn('btnStop', false);
_btn('btnInstall', true);
_btn('btnBrowser', false);
} else if (data.oasis_installed) {
document.getElementById('oasisState').textContent = 'INSTALADO';
document.getElementById('oasisSub').textContent = 'servidor detenido';
_btn('btnStart', false);
_btn('btnStop', true);
_btn('btnInstall', true);
_btn('btnBrowser', true);
} else {
document.getElementById('oasisState').textContent = 'NO INSTALADO';
document.getElementById('oasisSub').textContent = 'instala OASIS para comenzar';
_btn('btnStart', true);
_btn('btnStop', true);
_btn('btnInstall', false);
_btn('btnBrowser', true);
}
document.getElementById('iVer').textContent = data.oasis_version ? 'v' + data.oasis_version : '—';
document.getElementById('iNode').textContent = data.node_version || '—';
document.getElementById('iDir').textContent = data.oasis_dir || '—';
// Global dot
document.getElementById('globalDot').dataset.state = state;
// --- ECOIN ---
const eCard = document.getElementById('ecoinCard');
const estate = data.ecoin_installed ? 'running' : 'unknown';
eCard.dataset.state = estate;
if (data.ecoin_installed) {
document.getElementById('ecoinState').textContent = 'COMPILADO';
document.getElementById('ecoinSub').textContent = 'wallet ECOIN disponible';
_btn('btnEGui', false);
_btn('btnEWallet', false);
_btn('btnEConnect', false);
} else {
document.getElementById('ecoinState').textContent = 'NO INSTALADO';
document.getElementById('ecoinSub').textContent = 'instala ECOIN para comenzar';
_btn('btnEGui', true);
_btn('btnEWallet', true);
_btn('btnEConnect', true);
}
document.getElementById('eWallet').textContent = data.ecoin_wallet ? 'Si' : 'No';
document.getElementById('eQt').textContent = data.ecoin_qt ? 'Si' : 'No';
document.getElementById('eDaemon').textContent = data.ecoin_daemon ? 'Si' : 'No';
}
// ── Log (llamado desde Python) ────────────────────────────────
function appendLog(text) {
const area = document.getElementById('logArea');
const div = document.createElement('div');
div.className = 'log-line';
if (text.startsWith('$')) div.classList.add('cmd');
else if (text.startsWith('──')) div.classList.add('end');
else if (text.startsWith('[Error')) div.classList.add('error');
div.textContent = text;
area.appendChild(div);
area.scrollTop = area.scrollHeight;
}
function clearLog() {
document.getElementById('logArea').innerHTML = '';
}
// ── Helper: deshabilitar/habilitar botón ──────────────────────
function _btn(id, disabled) {
const el = document.getElementById(id);
if (el) el.disabled = disabled;
}

102
INSTALLER_V3/ui/index.html Normal file
View file

@ -0,0 +1,102 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OASIS</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- HEADER -->
<header>
<div class="header-left">
<img src="../oasis-logo.png" class="logo" alt="OASIS">
<div class="header-titles">
<span class="app-title">OASIS</span>
<span class="app-sub">SOLAR NET HUB PANEL</span>
</div>
</div>
<div class="status-dot" id="globalDot" data-state="unknown"></div>
</header>
<!-- TABS -->
<nav class="tabbar">
<button class="tabBtn active" data-tab="oasis" onclick="switchTab('oasis')">OASIS</button>
<button class="tabBtn" data-tab="ecoin" onclick="switchTab('ecoin')">ECOIN</button>
<button class="tabBtn" data-tab="sistema" onclick="switchTab('sistema')">SISTEMA</button>
</nav>
<!-- CONTENIDO -->
<main>
<!-- ── OASIS ── -->
<section class="tab-panel active" id="tab-oasis">
<div class="status-card" id="oasisCard" data-state="unknown">
<div class="card-stripe"></div>
<div class="card-body">
<div class="card-state" id="oasisState">COMPROBANDO</div>
<div class="card-sub" id="oasisSub">iniciando...</div>
</div>
</div>
<div class="actions">
<div class="action-row">
<button class="btn primary" id="btnStart" onclick="send('oasis-start')" disabled>▶ INICIAR</button>
<button class="btn" id="btnStop" onclick="send('oasis-stop')" disabled>■ DETENER</button>
</div>
<div class="action-row">
<button class="btn" id="btnInstall" onclick="send('oasis-install')">⬇ INSTALAR</button>
<button class="btn" id="btnBrowser" onclick="send('oasis-browser')" disabled>◎ ABRIR WEB</button>
</div>
</div>
<div class="infobox">
<div class="info-row"><span class="ik">VERSION</span><span class="iv" id="iVer"></span></div>
<div class="info-row"><span class="ik">NODE.JS</span><span class="iv" id="iNode"></span></div>
<div class="info-row"><span class="ik">RUTA</span> <span class="iv" id="iDir"></span></div>
</div>
</section>
<!-- ── ECOIN ── -->
<section class="tab-panel" id="tab-ecoin">
<div class="status-card" id="ecoinCard" data-state="unknown">
<div class="card-stripe"></div>
<div class="card-body">
<div class="card-state" id="ecoinState">COMPROBANDO</div>
<div class="card-sub" id="ecoinSub">iniciando...</div>
</div>
</div>
<div class="actions">
<div class="action-row">
<button class="btn primary" id="btnEInstall" onclick="send('ecoin-install')">⬇ INSTALAR</button>
<button class="btn" id="btnEGui" onclick="send('ecoin-gui')" disabled>◈ ABRIR GUI</button>
</div>
<div class="action-row">
<button class="btn" id="btnEWallet" onclick="send('ecoin-wallet')" disabled>✦ CREAR WALLET</button>
<button class="btn" id="btnEConnect" onclick="send('ecoin-connect')" disabled>⟳ CONECTAR</button>
</div>
</div>
<div class="infobox">
<div class="info-row"><span class="ik">WALLET</span> <span class="iv" id="eWallet"></span></div>
<div class="info-row"><span class="ik">ECOIN-QT</span><span class="iv" id="eQt"></span></div>
<div class="info-row"><span class="ik">ECOIND</span> <span class="iv" id="eDaemon"></span></div>
</div>
</section>
<!-- ── SISTEMA ── -->
<section class="tab-panel" id="tab-sistema">
<div class="log-header">LOG DE ACTIVIDAD</div>
<div class="log-area" id="logArea"></div>
<button class="btn btn-clear" onclick="clearLog()">LIMPIAR LOG</button>
</section>
</main>
<script src="app.js"></script>
</body>
</html>

347
INSTALLER_V3/ui/style.css Normal file
View file

@ -0,0 +1,347 @@
/* ================================================================
OASIS Panel v3
CSS puro, sin limitaciones GTK. Control total como Electron/web.
================================================================ */
@font-face {
font-family: 'Dune Rise';
src: url('../Dune_Rise.otf') format('opentype');
font-weight: normal;
font-style: normal;
}
:root {
--bg: #000000;
--bg2: #080808;
--bg3: #0D0D0D;
/* OASIS — naranja */
--orange: #FF4E00;
--orange-dim: #882200;
--orange-mute: #441100;
/* ECOIN — amarillo */
--yellow: #FFB300;
--yellow-dim: #886000;
--yellow-mute: #443000;
/* LOG — verde neon */
--neon: #39FF14;
--neon-dim: #1a7a09;
/* General */
--green: #27D980;
--border: #1A1A1A;
--font: 'Dune Rise', 'Cantarell', 'Ubuntu', sans-serif;
--mono: 'DejaVu Sans Mono', 'Courier New', monospace;
}
/* ── Reset ── */
*, *::before, *::after {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
}
html, body {
width: 390px;
height: 620px;
overflow: hidden;
background: var(--bg);
color: var(--orange);
font-family: var(--font);
font-size: 10pt;
display: flex;
flex-direction: column;
user-select: none;
}
/* ── Header ─────────────────────────────────────────────────── */
header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: var(--bg2);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.header-left {
display: flex;
align-items: center;
gap: 10px;
}
.logo {
width: 38px;
height: 38px;
object-fit: contain;
flex-shrink: 0;
}
.header-titles {
display: flex;
flex-direction: column;
gap: 2px;
}
.app-title {
font-size: 15pt;
color: var(--orange);
letter-spacing: 5px;
line-height: 1;
}
.app-sub {
font-size: 6pt;
color: var(--orange-mute);
letter-spacing: 2px;
}
.status-dot {
font-size: 11pt;
transition: color 0.4s;
line-height: 1;
}
.status-dot[data-state="running"] { color: var(--green); }
.status-dot[data-state="stopped"] { color: var(--orange); }
.status-dot[data-state="unknown"] { color: var(--orange-mute); }
/* ── Tab bar ─────────────────────────────────────────────────── */
nav.tabbar {
display: flex;
background: var(--bg);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.tabBtn {
flex: 1;
background: none;
border: none;
border-bottom: 3px solid transparent;
padding: 11px 0;
font-size: 7.5pt;
letter-spacing: 2px;
font-family: var(--font);
cursor: pointer;
transition: color 0.2s, border-color 0.2s;
}
/* Tab OASIS — naranja */
.tabBtn[data-tab="oasis"] { color: var(--orange-dim); }
.tabBtn[data-tab="oasis"]:hover { color: var(--orange); }
.tabBtn[data-tab="oasis"].active { color: var(--orange); border-bottom-color: var(--orange); }
/* Tab ECOIN — amarillo */
.tabBtn[data-tab="ecoin"] { color: var(--yellow-dim); }
.tabBtn[data-tab="ecoin"]:hover { color: var(--yellow); }
.tabBtn[data-tab="ecoin"].active { color: var(--yellow); border-bottom-color: var(--yellow); }
/* Tab SISTEMA — verde neon */
.tabBtn[data-tab="sistema"] { color: var(--neon-dim); }
.tabBtn[data-tab="sistema"]:hover { color: var(--neon); }
.tabBtn[data-tab="sistema"].active { color: var(--neon); border-bottom-color: var(--neon); }
/* ── Main content ────────────────────────────────────────────── */
main {
flex: 1;
overflow: hidden;
position: relative;
background: var(--bg);
}
.tab-panel {
display: none;
flex-direction: column;
height: 100%;
overflow-y: auto;
background: var(--bg);
}
.tab-panel.active { display: flex; }
/* ── Scrollbar ───────────────────────────────────────────────── */
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: #222; border-radius: 2px; }
::-webkit-scrollbar-thumb:hover { background: #333; }
/* ================================================================
OASIS TAB naranja
================================================================ */
#tab-oasis .card-state { color: var(--orange-dim); }
#tab-oasis .status-card[data-state="running"] .card-state { color: var(--green); }
#tab-oasis .status-card[data-state="stopped"] .card-state { color: var(--orange); }
#tab-oasis .status-card[data-state="running"] .card-stripe { background: var(--green); }
#tab-oasis .status-card[data-state="stopped"] .card-stripe { background: var(--orange); }
#tab-oasis .card-sub { color: var(--orange-mute); }
#tab-oasis .btn { color: var(--orange); border-color: var(--orange); }
#tab-oasis .btn.primary { background: var(--orange); color: #000; border-color: var(--orange); }
#tab-oasis .ik { color: var(--orange-dim); }
#tab-oasis .iv { color: var(--orange); }
/* ================================================================
ECOIN TAB amarillo
================================================================ */
#tab-ecoin .card-state { color: var(--yellow-dim); }
#tab-ecoin .status-card[data-state="running"] .card-state { color: var(--yellow); }
#tab-ecoin .status-card[data-state="stopped"] .card-state { color: var(--yellow-dim); }
#tab-ecoin .status-card[data-state="running"] .card-stripe { background: var(--yellow); }
#tab-ecoin .status-card[data-state="stopped"] .card-stripe { background: var(--yellow-dim); }
#tab-ecoin .card-sub { color: var(--yellow-mute); }
#tab-ecoin .btn { color: var(--yellow); border-color: var(--yellow); }
#tab-ecoin .btn.primary { background: var(--yellow); color: #000; border-color: var(--yellow); }
#tab-ecoin .ik { color: var(--yellow-dim); }
#tab-ecoin .iv { color: var(--yellow); }
/* ================================================================
STATUS CARD base
================================================================ */
.status-card {
display: flex;
margin: 14px 14px 6px;
background: var(--bg2);
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
flex-shrink: 0;
transition: border-color 0.3s;
}
.card-stripe {
width: 5px;
flex-shrink: 0;
background: #1A1A1A;
transition: background 0.4s;
}
.status-card[data-state="unknown"] .card-stripe { background: #1A1A1A; }
.card-body {
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 5px;
}
.card-state {
font-size: 18pt;
font-weight: bold;
letter-spacing: 2px;
transition: color 0.4s;
}
.card-sub {
font-size: 7.5pt;
letter-spacing: 1px;
}
/* ── Botones base ────────────────────────────────────────────── */
.actions {
padding: 6px 14px 2px;
flex-shrink: 0;
}
.action-row {
display: flex;
gap: 8px;
margin-bottom: 8px;
}
.btn {
flex: 1;
background: var(--bg);
border: 1px solid currentColor;
border-radius: 20px;
padding: 9px 10px;
font-size: 7.5pt;
font-weight: bold;
letter-spacing: 1px;
font-family: var(--font);
cursor: pointer;
transition: background 0.2s, color 0.2s, border-color 0.2s, opacity 0.2s;
white-space: nowrap;
text-align: center;
}
.btn:hover:not(:disabled) {
background: var(--green);
color: #000;
border-color: var(--green);
}
.btn:active:not(:disabled) {
background: #1fa865;
border-color: #1fa865;
color: #000;
}
.btn:disabled {
opacity: 0.2;
cursor: not-allowed;
}
.btn.btn-clear {
flex: none;
width: calc(100% - 28px);
margin: 6px 14px 12px;
color: var(--neon);
border-color: var(--neon);
}
.btn.btn-clear:hover:not(:disabled) {
background: var(--neon);
color: #000;
border-color: var(--neon);
}
/* ── Info box ────────────────────────────────────────────────── */
.infobox {
margin: 8px 14px 14px;
border-top: 1px solid var(--border);
padding-top: 8px;
flex-shrink: 0;
}
.info-row {
display: flex;
align-items: center;
padding: 5px 2px;
border-bottom: 1px solid #080808;
}
.info-row:last-child { border-bottom: none; }
.ik {
font-size: 7.5pt;
letter-spacing: 1px;
min-width: 82px;
flex-shrink: 0;
}
.iv {
font-size: 7.5pt;
word-break: break-all;
}
/* ================================================================
SISTEMA / LOG verde neon
================================================================ */
.log-header {
font-size: 6.5pt;
color: var(--neon-dim);
letter-spacing: 2px;
padding: 12px 16px 6px;
flex-shrink: 0;
}
.log-area {
flex: 1;
background: #030303;
border: 1px solid #0a1a0a;
border-radius: 8px;
margin: 0 14px 8px;
padding: 10px 12px;
overflow-y: auto;
font-family: var(--mono);
font-size: 7.5pt;
color: var(--neon);
line-height: 1.55;
min-height: 0;
}
.log-line { word-break: break-all; padding: 1px 0; color: var(--neon); }
.log-line.cmd { color: var(--orange); }
.log-line.end { color: var(--neon-dim); margin-top: 4px; }
.log-line.error { color: #ff4444; }