Compare commits

...
Sign in to create a new pull request.

5 commits

Author SHA1 Message Date
SITO
fe030f2066 feat(panel-v3): integración ECOIN — daemon, wallet info y GUI
- Detección de ecoind via JSON-RPC (puerto 7474, lee ~/.ecoin/ecoin.conf)
- Botones INICIAR/DETENER/ABRIR GUI/VER INFO en pestaña ECOIN
- ABRIR GUI para ecoind antes de lanzar ecoin-qt (no pueden coexistir)
- Compilación automática de ecoin-qt con qmake si no está compilado
- Grid en tiempo real: balance ECO, bloques, conexiones, wallet, daemon
- ecoin_rpc() helper para llamadas JSON-RPC al daemon
- _start_ecoin/_stop_ecoin/_open_ecoin_gui/_ecoin_info en panel.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 20:13:32 +02:00
SITO
68b3ceff8d fix(panel-v3): bridge JS→Python, DETENER por PID, NVM, cierre limpio
- Bridge reescrito: usa oasis:// URI en lugar de UserContentManager
- bash -lc para cargar NVM al lanzar OASIS (Node v22)
- DETENER mata el grupo de procesos por PID (no pkill genérico)
- _oasis_proc guarda el proceso activo entre INICIAR/DETENER
- Flag _alive evita MemoryError al llamar GLib.idle_add tras cerrar
- _on_destroy desconecta el main loop limpiamente

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 14:54:26 +02:00
SITO
1ed0aa03d9 chore: eliminar INSTALLER (v1) de main, queda solo en rama panel-v1
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 12:42:40 +02:00
SITO
56f4190d41 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>
2026-03-30 12:30:46 +02:00
SITO
67acbf1add 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>
2026-03-30 12:26:24 +02:00
26 changed files with 1226 additions and 1531 deletions

Binary file not shown.

View file

@ -1,106 +0,0 @@
/* ==========================================================
SOLAR NET HUB GTK3 Theme (YAD)
========================================================== */
@define-color bg #000000;
@define-color fg #E6E6E6;
@define-color muted #9AA0A6;
@define-color accent #FF4E00; /* Naranja corporativo */
@define-color success #27D980; /* Verde OK */
@define-color field_bg #0B0B0B;
@define-color stroke #202020;
/* ---------- Tipografía / color global ---------- */
* {
color: @fg;
font-family: "Dune Rise", "Cantarell", "Ubuntu", "DejaVu Sans", sans-serif;
font-size: 12.1pt; /* un pelín más pequeño */
}
/* ---------- Fondos de contenedores ---------- */
window, dialog, grid, box, scrolledwindow, viewport { background-color: @bg; }
grid { padding: 11px 12px 14px 12px; } /* menos padding para que quepa más */
/* ---------- Evitar fondos dentro de textos/botones ---------- */
label, accellabel,
button label, button accellabel,
button * { background: transparent; background-color: transparent; }
/* ==========================================================
BOTONES: negro + borde/ texto naranja hover VERDE + texto negro
========================================================== */
/* ==========================================================
CTAs (botones de acción) versión compacta
========================================================== */
button {
background: @bg; /* fondo negro */
color: @accent; /* texto naranja */
border: 2px solid @accent; /* borde naranja */
border-radius: 10px;
padding: 5px 5px; /* ↓ padding */
font-weight: 200;
font-size: 5pt; /* ↓ tamaño de fuente */
letter-spacing: .2px;
min-width: 100px; /* ↓ ancho mínimo */
min-height: 20px; /* ↓ alto mínimo */
margin: 3px 6px; /* ↓ separación vertical */
}
/* Hover: botón verde y texto negro (como pediste) */
button:hover {
background-color: @success; /* verde */
color: #000; /* texto negro */
border-color: @success;
}
button:active {
background-color: shade(@success, 0.85);
color: #000;
border-color: shade(@success, 0.85);
}
button:disabled {
background-color: #101010;
color: #444;
border-color: #222;
}
/* Icono dentro del botón (si lo hay), más contenido y pequeño */
button image {
margin-right: 6px;
-gtk-icon-transform: scale(0.95);
}
/* ==========================================================
Entradas / listas (por si las usas)
========================================================== */
entry, spinbutton, combobox, textview, treeview {
background-color: @field_bg;
color: @fg;
border: 1px solid @stroke;
border-radius: 10px;
padding: 8px 10px;
}
entry:focus, spinbutton:focus, combobox:focus, textview:focus, treeview:focus {
border-color: @accent;
}
/* ==========================================================
Progreso YAD
========================================================== */
trough { background-color: #121212; border-radius: 6px; }
progressbar progress { background-color: @accent; border-radius: 6px; }
/* ==========================================================
Tooltips
========================================================== */
tooltip, tooltip * {
background-color: #111111;
color: @fg;
border: 1px solid @stroke;
border-radius: 8px;
padding: 6px 8px;
}
/* Labels sueltos dentro del grid con separación mínima */
grid label { margin: 3px 0 5px; }

View file

@ -1,42 +0,0 @@
/* ==========================================================
SOLAR NET HUB GTK3 Theme (YAD) OASIS simple (fluido)
========================================================== */
@define-color bg #000000;
@define-color fg #E6E6E6;
@define-color accent #FF4E00;
@define-color accent_hover #27D980;
/* Tipografía y color global: tamaño moderado (mejor en HiDPI) */
* {
color: @fg;
font-family: "Dune Rise", "Cantarell", "Ubuntu", "DejaVu Sans", sans-serif;
font-size: 11pt; /* antes 13pt */
}
window, dialog, grid, box, scrolledwindow, viewport {
background-color: @bg;
}
grid { padding: 16px; } /* un poco menos de padding */
/* Botones sin tamaños rígidos: usa padding, no min-width/height */
button {
background: #000000;
color: @accent;
border: 2px solid @accent;
border-radius: 14px;
padding: 10px 14px; /* sustituye min-width/height por padding */
margin: 8px 0;
letter-spacing: 0.5px;
}
button:hover { background: @accent_hover; color: #000; }
button:active { background: @accent; color: #000; }
/* Limpieza de fondos parásitos en textos dentro de botones */
label, accellabel,
button label, button accellabel,
button * {
background: transparent;
background-color: transparent;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

View file

@ -1,346 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# =========================
# SOLAR NET HUB - Installer
# =========================
# --- Rutas ---
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
INSTALLER_DIR="$SCRIPT_DIR"
CSS_SOURCE="$INSTALLER_DIR/gtk.css" # (lo añadimos luego)
ICON_OASIS="$INSTALLER_DIR/oasis-logo.webp"
ICON_ECOIN="$INSTALLER_DIR/ecoin.png"
APPS_DIR="$HOME/.local/share/applications"
DESKTOP_DIR="$(xdg-user-dir DESKTOP 2>/dev/null || echo "$HOME/Desktop")"
# --- Theming local (no toca el sistema) ---
NAME="SolarHubInstaller" # --name/--class (CSS scope)
THEME_NAME="SolarHub" # ~/.themes/SolarHub/gtk-3.0/gtk.css
YAD_THEME="${THEME_NAME}:dark"
ORANGE="#FF4E00"
GREEN="#27D980"
GREY="#b9b9b9"
# --- Helpers básicos ---
have(){ command -v "$1" >/dev/null 2>&1; }
pkg_install(){
local pkgs=("$@")
if have apt-get; then
pkexec bash -lc "apt-get update && apt-get install -y ${pkgs[*]}"
elif have pacman; then
pkexec bash -lc "pacman -Sy --noconfirm ${pkgs[*]}"
elif have dnf; then
pkexec bash -lc "dnf install -y ${pkgs[*]}"
elif have zypper; then
pkexec bash -lc "zypper --non-interactive install ${pkgs[*]}"
else
echo "❌ Gestor de paquetes no soportado. Instala manualmente: yad, policykit-1, hicolor-icon-theme, xdg-user-dirs" >&2
exit 1
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
# CSS mínimo por si aún no creaste gtk.css
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" yad --name="$NAME" --class="$NAME" "$@"; }
progress_user(){ # $1=title $2=cmd (como string)
bash -lc "$2" 2>&1 | yad_cmd --progress --title="$1" --pulsate --auto-close --no-buttons \
--width=940 --height=260 --center
}
progress_root(){ # $1=title $2=cmd (string) -> pkexec
pkexec bash -lc "$2" 2>&1 | yad_cmd --progress --title="$1" --pulsate --auto-close --no-buttons \
--width=940 --height=260 --center
}
ensure_launcher(){
mkdir -p "$APPS_DIR" "$DESKTOP_DIR"
cat > "$APPS_DIR/INSTALLER.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=INSTALLER
Comment=OASIS & ECOIN
Exec=/bin/bash -lc '$SCRIPT_DIR/install.sh'
Icon=${ICON_OASIS:-system-software-install}
Terminal=false
Categories=System;Utility;
StartupNotify=true
EOF
cp -f "$APPS_DIR/INSTALLER.desktop" "$DESKTOP_DIR/INSTALLER.desktop" 2>/dev/null || true
chmod +x "$DESKTOP_DIR/INSTALLER.desktop" 2>/dev/null || true
}
# ========= Detección de 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" ]; }
# ========= Comandos OASIS =========
node_setup_cmd(){ # $1=major (22/18)
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(){ # $1 repo $2 dir
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(){ # $1 dir
cat <<EOF
set -e
MODEL_DIR="$1/AI"; mkdir -p "\$MODEL_DIR"
if [ ! -f "\$MODEL_DIR/$OASIS_MODEL_FILE" ]; then
echo "Descargando modelo..."
curl -L -o "\$MODEL_DIR/$OASIS_MODEL_TAR" "$OASIS_MODEL_URL"
echo "Descomprimiendo..."
tar -xzf "\$MODEL_DIR/$OASIS_MODEL_TAR" -C "\$MODEL_DIR"
rm "\$MODEL_DIR/$OASIS_MODEL_TAR"
fi
EOF
}
oasis_start_cmd(){ # $1 dir
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
}
oasis_stop_cmd(){ cat <<'EOF'
set -e
pkill -f 'node .*server\.js' || true
pkill -f 'bash oasis\.sh' || true
EOF
}
# ========= Comandos 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(){ # $1 repo $2 dir
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(){ # $1 dir
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(){ # $1 dir
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(){ # $1 dir
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(){ # major repo dir
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(){ # repo dir
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 (dos mitades) =========
home(){
# Clave segura para paned/plug (≤2e9)
if command -v shuf >/dev/null 2>&1; then
KEY="$(shuf -i 1-2000000000 -n 1)"
else
KEY=$(( (RANDOM<<15) | RANDOM )); KEY=$((KEY%2000000000+1))
fi
# --- MITAD IZQUIERDA: OASIS ---
local O_TEXT O_BTN
if oasis_installed; then
O_TEXT="<span size='x-large' weight='heavy' foreground='$ORANGE'>OASIS</span>\n<span foreground='$GREEN'>Instalado</span>"
O_BTN="bash -lc '$0 --oasis-start'!$ICON_OASIS!Abrir OASIS"
else
O_TEXT="<span size='x-large' weight='heavy' foreground='$ORANGE'>OASIS</span>\n<span foreground='$ORANGE'>No instalado</span>"
O_BTN="bash -lc '$0 --oasis-install'!$ICON_OASIS!Instalar OASIS"
fi
yad_cmd --plug="$KEY" --form --borders=28 --columns=1 \
${ICON_OASIS:+--image="$ICON_OASIS"} --image-on-top \
--text="$O_TEXT" \
--field=":BTN" "$O_BTN" \
--no-buttons --buttons-layout=center &
PID_L=$!
# --- MITAD DERECHA: ECOIN ---
local E_TEXT E_BTN1 E_BTN2
if ! ecoin_installed; then
E_TEXT="<span size='x-large' weight='heavy' foreground='$ORANGE'>ECOIN</span>\n<span foreground='$ORANGE'>No instalado</span>"
E_BTN1="bash -lc '$0 --ecoin-install'!$ICON_ECOIN!Instalar ECOIN"
E_BTN2=""
else
if ecoin_wallet_exists; then
E_TEXT="<span size='x-large' weight='heavy' foreground='$ORANGE'>ECOIN</span>\n<span foreground='$GREEN'>Cartera detectada</span>"
E_BTN1="bash -lc '$0 --wallet-connect'!$ICON_ECOIN!Conectar cartera"
E_BTN2="bash -lc '$0 --ecoin-gui'!$ICON_ECOIN!Abrir ECOIN GUI"
else
E_TEXT="<span size='x-large' weight='heavy' foreground='$ORANGE'>ECOIN</span>\n<span foreground='$ORANGE'>Sin cartera</span>"
E_BTN1="bash -lc '$0 --wallet-create'!$ICON_ECOIN!Crear cartera"
E_BTN2=""
fi
fi
if [ -n "$E_BTN2" ]; then
yad_cmd --plug="$KEY" --form --borders=28 --columns=1 \
${ICON_ECOIN:+--image="$ICON_ECOIN"} --image-on-top \
--text="$E_TEXT" \
--field=":BTN" "$E_BTN1" \
--field=":BTN" "$E_BTN2" \
--no-buttons --buttons-layout=center &
else
yad_cmd --plug="$KEY" --form --borders=28 --columns=1 \
${ICON_ECOIN:+--image="$ICON_ECOIN"} --image-on-top \
--text="$E_TEXT" \
--field=":BTN" "$E_BTN1" \
--no-buttons --buttons-layout=center &
fi
PID_R=$!
sleep 0.35
yad_cmd --paned --key="$KEY" \
--title="SOLAR NET HUB" --width=1180 --height=740 --center \
--button="Cerrar:0"
kill "$PID_L" "$PID_R" 2>/dev/null || true
}
# ========= Entrypoint =========
ensure_basics
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 ;;
esac
ensure_basics(){
local miss=()
command -v yad >/dev/null 2>&1 || miss+=("yad")
command -v pkexec >/dev/null 2>&1 || miss+=("policykit-1")
command -v xdg-user-dirs-update >/dev/null 2>&1 || 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
((${#miss[@]})) && pkg_install "${miss[@]}"
}

View file

@ -1,265 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# =========================
# OASIS Installer (simple)
# =========================
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
: "${SELF:=$SCRIPT_DIR/installer_oasis.sh}"
INSTALLER_DIR="$SCRIPT_DIR"
CSS_SOURCE="$INSTALLER_DIR/gtk_oasis.css"
# Imagen/icono
HERO_FILE="$INSTALLER_DIR/oasis-logito.png"; [ -f "$HERO_FILE" ] || HERO_FILE="$INSTALLER_DIR/oasis-logo.png"
WINDOW_ICON="$INSTALLER_DIR/oasis-logo.png"
# Tema / ventana
NAME="OasisInstaller"
THEME_NAME="SolarOasis"
YAD_THEME="${THEME_NAME}:dark"
# Repo (se mantiene por compatibilidad con funciones existentes)
OASIS_REPO_DEFAULT="https://code.03c8.net/KrakensLab/oasis.git"
# Estado persistente
STATE_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/oasis-installer"
STATE_FILE="$STATE_DIR/state"
mkdir -p "$STATE_DIR"
have(){ command -v "$1" >/dev/null 2>&1; }
# ===== NUEVO: chequeo de npm (único cambio funcional pedido) =====
npm_ok(){
# Permite forzar el estado "no instalado" para pruebas:
# OASIS_FORCE_INSTALL=1 ./INSTALLER/installer_oasis.sh
if [ "${OASIS_FORCE_INSTALL:-0}" = "1" ]; then
return 1
fi
have npm && npm -v >/dev/null 2>&1
}
# ---------- Preflight ----------
preflight(){
# DISPLAY “de cortesía”
if [ -z "${DISPLAY:-}" ]; then export DISPLAY=":0"; fi
# YAD disponible?
if ! have yad; then
echo "✘ Falta 'yad'. Instálalo y vuelve a ejecutar." >&2
exit 1
fi
}
ensure_basics(){
local miss=()
have yad || miss+=("yad")
command -v pkexec >/dev/null 2>&1 || miss+=("policykit-1")
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")
fi
if ((${#miss[@]})); then
if have apt-get; then
if have pkexec; then pkexec bash -lc "apt-get update && apt-get install -y ${miss[*]}";
else sudo apt-get update && sudo apt-get install -y "${miss[@]}"; fi
fi
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:"Dune Rise","Cantarell","Ubuntu","DejaVu Sans",sans-serif;font-size:13pt}
grid{padding:18px}
button{
border:2px solid #FF4E00; border-radius:14px; background:#000; color:#FF4E00;
min-width:380px; min-height:46px; margin:10px 0; letter-spacing:.5px;
}
button:hover{ background:#27D980; color:#000; }
button:active{ background:#FF4E00; color:#000; }
image{ margin:8px auto 6px auto; }
CSS
fi
}
# YAD wrapper
yad_cmd(){ GTK_THEME="$YAD_THEME" NO_AT_BRIDGE=1 yad --name="$NAME" --class="$NAME" "$@"; }
# -------- Detección / estado de OASIS --------
oasis_is_installed_dir(){
local d="$1"
[[ -f "$d/oasis.sh" ]] || [[ -f "$d/src/server/server.js" ]] || [[ -d "$d/src/server/node_modules" ]]
}
save_oasis_dir(){
local d="$1"
mkdir -p "$STATE_DIR"
{
echo "OASIS_DIR=$d"
date +"SAVED=%Y-%m-%dT%H:%M:%S"
} > "$STATE_FILE"
}
read_saved_dir(){
[ -f "$STATE_FILE" ] && sed -n 's/^OASIS_DIR=//p' "$STATE_FILE" | head -n1 || true
}
guess_oasis_dir(){
local cands=()
# 1) Padre del installer (estructura estándar)
cands+=("$SCRIPT_DIR/..")
# 2) Guardado previo
local saved; saved="$(read_saved_dir || true)"
[ -n "${saved:-}" ] && cands+=("$saved")
# 3) Candidatas típicas
cands+=("$HOME/COFRE/CODERS/oasis" "$HOME/oasis" "$HOME/Projects/oasis" "$HOME/Documentos/oasis")
local d
for d in "${cands[@]}"; do
d="$(cd "$d" 2>/dev/null && pwd || true)"
[ -n "$d" ] || continue
if oasis_is_installed_dir "$d"; then
echo "$d"; return 0
fi
done
echo "$HOME/oasis"
}
# -------- Comandos auxiliares existentes (sin cambios) --------
node_setup_cmd(){ cat <<'EOF'
set -e
apt-get update
apt-get install -y git curl ca-certificates tar gnupg
curl -fsSL https://deb.nodesource.com/setup_22.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_start_cmd(){ cat <<'EOF'
set -e
DIR="$1"
PORT="${2:-3000}"
# ¿Ya está levantado?
if ss -lnt sport = :$PORT | grep -q "$PORT"; then
echo "Oasis ya está escuchando en $PORT"
else
nohup bash -lc "cd \"$DIR\" && exec bash oasis.sh" > /tmp/oasis_gui.log 2>&1 &
for i in $(seq 1 30); do
if ss -lnt sport = :$PORT | grep -q "$PORT"; then
echo "Oasis levantado en $PORT"
break
fi
sleep 1
done
fi
# Abrir navegador siempre
xdg-open "http://localhost:$PORT" >/dev/null 2>&1 || true
EOF
}
progress_user(){ bash -lc "$2" 2>&1 | yad_cmd --progress --title="$1" --pulsate --auto-close --no-buttons --width=700 --height=180 --center; }
progress_root(){ pkexec bash -lc "$2" 2>&1 | yad_cmd --progress --title="$1" --pulsate --auto-close --no-buttons --width=700 --height=180 --center; }
oasis_quick_install(){
local dir="$1"
progress_root "OASIS: Node.js 22" "$(node_setup_cmd)"
progress_user "OASIS: Clonar + npm" "$(oasis_install_cmd "$OASIS_REPO_DEFAULT" "$dir")"
save_oasis_dir "$dir"
}
oasis_quick_start(){
local dir="$1"
progress_user "OASIS: Abrir" "$(oasis_start_cmd "$dir" 3000)"
}
# -------- UI --------
home_oasis_dialog(){
local OASIS_DIR; OASIS_DIR="$(guess_oasis_dir)"
save_oasis_dir "$OASIS_DIR"
local O_VER="—"
if [ -f "$OASIS_DIR/src/server/package.json" ]; then
O_VER="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$OASIS_DIR/src/server/package.json" | head -n1)"
[ -z "$O_VER" ] && O_VER="—"
fi
# Subcomandos (OPEN se mantiene EXACTO como antes)
local BTN_OPEN_SITE="/bin/bash -lc '$SELF --open-site'"
local BTN_INSTALL="/bin/bash -lc '$SELF --do-install'"
local BTN_OPEN="/bin/bash -lc '$SELF --open-now'"
# ===== ÚNICO CAMBIO: decidir etiqueta/acción de INSTALL según npm =====
local INSTALL_LABEL="INSTALL:BTN"
if npm_ok; then
INSTALL_LABEL="ALREADY INSTALLED:BTN"
BTN_INSTALL="/bin/true"
fi
# (No tocamos BTN_OPEN)
yad_cmd \
--title="SOLAR NET HUB" --window-icon="$WINDOW_ICON" \
--center --width=720 --height=600 \
--image="$HERO_FILE" --image-on-top \
--form --borders=16 --columns=1 --align=center \
\
--field="OASIS PROJECT:BTN" "$BTN_OPEN_SITE" \
--field="$INSTALL_LABEL" "$BTN_INSTALL" \
--field="OPEN:BTN" "$BTN_OPEN" \
--field="VERSION ( $O_VER ):BTN" "/bin/true" \
\
--buttons-layout=center \
--button="CERRAR:0"
}
# -------- Subcomandos --------
case "${1:-}" in
--open-site)
xdg-open "https://oasis-project.pub" >/dev/null 2>&1 || true
exit 0
;;
--do-install)
preflight; ensure_basics; ensure_theme
# Ejecutar el install.sh del raíz del repo (padre de INSTALLER)
if [ -f "$SCRIPT_DIR/../install.sh" ]; then
/bin/bash -lc "cd \"$SCRIPT_DIR/..\" && bash install.sh"
else
yad_cmd --error --title="OASIS Installer" --text="No se encontró install.sh en $SCRIPT_DIR/.."
exit 1
fi
# Guardar ruta para OPEN
dir="$(cd "$SCRIPT_DIR/.." && pwd)"
save_oasis_dir "$dir"
exit 0
;;
--open-now)
preflight; ensure_basics; ensure_theme
dir="$(read_saved_dir)"; [ -n "${dir:-}" ] || dir="$(guess_oasis_dir)"
save_oasis_dir "$dir"
oasis_quick_start "$dir"
exit 0
;;
*)
preflight; ensure_basics; ensure_theme
home_oasis_dialog
;;
esac

Binary file not shown.

Before

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

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.

View file

Before

Width:  |  Height:  |  Size: 469 KiB

After

Width:  |  Height:  |  Size: 469 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 469 KiB

After

Width:  |  Height:  |  Size: 469 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Before After
Before After

532
INSTALLER_V3/panel.py Executable file
View file

@ -0,0 +1,532 @@
#!/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 ecoin_rpc_config() -> dict:
"""Lee credenciales RPC de ~/.ecoin/ecoin.conf."""
conf = Path.home() / ".ecoin" / "ecoin.conf"
cfg = {"rpcuser": "", "rpcpassword": "", "rpcport": "7474", "rpchost": "127.0.0.1"}
if conf.is_file():
for line in conf.read_text().splitlines():
line = line.strip()
if line.startswith("#") or "=" not in line:
continue
k, v = line.split("=", 1)
k = k.strip()
if k in cfg:
cfg[k] = v.strip()
return cfg
def ecoin_rpc(method, params=None):
"""Llama al RPC de ecoind. Devuelve (result, error)."""
import http.client, base64 as _b64
cfg = ecoin_rpc_config()
auth = _b64.b64encode(f"{cfg['rpcuser']}:{cfg['rpcpassword']}".encode()).decode()
body = json.dumps({"method": method, "params": params or [], "id": 1}).encode()
try:
conn = http.client.HTTPConnection(cfg["rpchost"], int(cfg["rpcport"]), timeout=3)
conn.request("POST", "/", body,
{"Content-Type": "application/json",
"Authorization": f"Basic {auth}"})
resp = conn.getresponse()
data = json.loads(resp.read())
return data.get("result"), data.get("error")
except Exception as e:
return None, str(e)
def ecoin_running() -> bool:
result, _ = ecoin_rpc("getinfo")
return result is not None
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._alive = True
self._oasis_proc = None # proceso OASIS activo
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", self._on_destroy)
# 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 ───────────────────────────────────────────────────────
settings = WebKit2.Settings()
settings.set_enable_javascript(True)
settings.set_allow_file_access_from_file_urls(True)
settings.set_allow_universal_access_from_file_urls(True)
settings.set_enable_page_cache(False)
self.webview = WebKit2.WebView()
self.webview.set_settings(settings)
self.webview.set_background_color(Gdk.RGBA(0, 0, 0, 1))
# Bridge JS→Python: interceptar navegaciones a oasis://accion
self.webview.connect("decide-policy", self._on_policy)
self.win.add(self.webview)
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)
def _on_destroy(self, *_):
self._alive = False
Gtk.main_quit()
# ── 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()
e_run = ecoin_running()
# Info RPC solo si el daemon está corriendo
e_info = {}
e_balance = None
if e_run:
info, _ = ecoin_rpc("getinfo")
bal, _ = ecoin_rpc("getbalance")
e_info = info or {}
e_balance = bal
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_running": e_run,
"ecoin_wallet": e_wall,
"ecoin_qt": e_qt,
"ecoin_daemon": e_dmn,
"ecoin_dir": str(e_dir) if e_inst else "",
"ecoin_balance": e_balance,
"ecoin_blocks": e_info.get("blocks", None),
"ecoin_connections": e_info.get("connections", None),
}
GLib.idle_add(self._js, f"updateStatus({json.dumps(data)})")
# ── JS helpers ────────────────────────────────────────────────────────
def _js(self, code):
if self._alive:
self.webview.run_javascript(code, None, None, None)
return False
def _log(self, text):
if self._alive:
self._js(f"appendLog({json.dumps(text)})")
# ── Bridge JS→Python via navegación oasis://accion ───────────────────
def _on_policy(self, webview, decision, decision_type):
if decision_type == WebKit2.PolicyDecisionType.NAVIGATION_ACTION:
uri = decision.get_navigation_action().get_request().get_uri()
if uri.startswith("oasis://"):
action = uri[len("oasis://"):]
print(f"[bridge] action={action}", flush=True)
GLib.idle_add(self._dispatch, action)
decision.ignore()
return True
decision.use()
return False
def _dispatch(self, action):
installer_cmds = {
"oasis-install": ["--oasis-install"],
"ecoin-install": ["--ecoin-install"],
}
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 == "ecoin-start":
GLib.idle_add(self._js, "switchTab('sistema')")
threading.Thread(target=self._start_ecoin, daemon=True).start()
elif action == "ecoin-stop":
GLib.idle_add(self._js, "switchTab('sistema')")
threading.Thread(target=self._stop_ecoin, daemon=True).start()
elif action == "ecoin-gui":
GLib.idle_add(self._js, "switchTab('sistema')")
threading.Thread(target=self._open_ecoin_gui, daemon=True).start()
elif action == "ecoin-info":
GLib.idle_add(self._js, "switchTab('sistema')")
threading.Thread(target=self._ecoin_info, daemon=True).start()
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
oasis_dir = find_oasis_dir()
GLib.idle_add(self._log, f"$ cd {oasis_dir} && bash oasis.sh")
try:
proc = subprocess.Popen(
["bash", "-lc", f"cd '{oasis_dir}' && bash oasis.sh"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
start_new_session=True,
)
except Exception as e:
GLib.idle_add(self._log, f"[Error]: {e}")
return
self._oasis_proc = proc
GLib.idle_add(self._log, f"PID {proc.pid} — esperando puerto 3000...")
# Leer salida del proceso y mostrarla en el log
def _read_output():
for line in iter(proc.stdout.readline, b""):
if self._alive:
GLib.idle_add(self._log, line.decode("utf-8", errors="replace").rstrip())
threading.Thread(target=_read_output, daemon=True).start()
# Esperar hasta 30s a que el puerto esté activo y abrir navegador
for i in range(30):
time.sleep(1)
if oasis_running():
GLib.idle_add(self._log, "Puerto 3000 activo — abriendo navegador")
subprocess.Popen(["xdg-open", "http://localhost:3000"])
GLib.idle_add(self._poll)
return
GLib.idle_add(self._log, "[Timeout] OASIS no levantó en 30s")
GLib.idle_add(self._poll)
# ── ECOIN actions ─────────────────────────────────────────────────────
def _start_ecoin(self):
if ecoin_running():
GLib.idle_add(self._log, "ecoind ya está corriendo.")
GLib.idle_add(self._poll)
return
e_dir = find_ecoin_dir()
ecoind = e_dir / "ecoin" / "src" / "ecoind"
if not ecoind.is_file():
GLib.idle_add(self._log, f"[Error]: ecoind no encontrado en {ecoind}")
return
GLib.idle_add(self._log, f"$ {ecoind} -daemon")
try:
subprocess.Popen(
[str(ecoind), "-daemon"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
start_new_session=True,
)
import time
for i in range(15):
time.sleep(1)
if ecoin_running():
GLib.idle_add(self._log, "ecoind iniciado correctamente.")
GLib.idle_add(self._poll)
return
GLib.idle_add(self._log, "[Timeout] ecoind no respondió en 15s.")
except Exception as e:
GLib.idle_add(self._log, f"[Error]: {e}")
GLib.idle_add(self._poll)
def _stop_ecoin(self):
GLib.idle_add(self._log, "Deteniendo ecoind...")
result, err = ecoin_rpc("stop")
if err:
GLib.idle_add(self._log, f"[Error RPC]: {err}")
else:
GLib.idle_add(self._log, "ecoind detenido.")
GLib.idle_add(self._poll)
def _open_ecoin_gui(self):
e_dir = find_ecoin_dir()
ecoin_qt = e_dir / "ecoin" / "ecoin-qt"
src_dir = e_dir / "ecoin"
if ecoin_qt.is_file():
# Si ecoind está corriendo, pararlo antes (no pueden coexistir)
if ecoin_running():
GLib.idle_add(self._log, "Parando ecoind antes de abrir la GUI...")
ecoin_rpc("stop")
import time
for _ in range(10):
time.sleep(1)
if not ecoin_running():
break
GLib.idle_add(self._log, f"$ {ecoin_qt}")
subprocess.Popen([str(ecoin_qt)], start_new_session=True)
GLib.idle_add(self._poll)
return
# No compilado — compilar primero
GLib.idle_add(self._log, "ecoin-qt no encontrado. Compilando...")
# Comprobar que qmake está disponible
qmake = None
for q in ("qmake", "qmake-qt5", "qmake6"):
r = subprocess.run(["which", q], capture_output=True)
if r.returncode == 0:
qmake = q
break
if not qmake:
GLib.idle_add(self._log, "[Error]: qmake no encontrado.")
GLib.idle_add(self._log, "Instala Qt5: sudo apt install qtbase5-dev qt5-qmake")
return
GLib.idle_add(self._log, f"$ cd {src_dir} && {qmake} && make -j$(nproc)")
try:
proc = subprocess.Popen(
["bash", "-c", f"cd '{src_dir}' && {qmake} && make -j$(nproc)"],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
for line in iter(proc.stdout.readline, b""):
if self._alive:
GLib.idle_add(self._log, line.decode("utf-8", errors="replace").rstrip())
proc.wait()
except Exception as e:
GLib.idle_add(self._log, f"[Error compilando]: {e}")
return
if ecoin_qt.is_file():
GLib.idle_add(self._log, "Compilación completada. Lanzando ecoin-qt...")
subprocess.Popen([str(ecoin_qt)], start_new_session=True)
else:
GLib.idle_add(self._log, "[Error]: compilación falló. Revisa las dependencias.")
GLib.idle_add(self._log, "Dependencias necesarias:")
GLib.idle_add(self._log, " sudo apt install qtbase5-dev qt5-qmake libboost-all-dev libssl-dev libdb++-dev")
def _ecoin_info(self):
if not ecoin_running():
GLib.idle_add(self._log, "ecoind no está corriendo. Inicia el daemon primero.")
return
info, err = ecoin_rpc("getinfo")
if err:
GLib.idle_add(self._log, f"[RPC Error]: {err}")
return
bal, _ = ecoin_rpc("getbalance")
GLib.idle_add(self._log, "── ECOIN INFO ──────────────────")
GLib.idle_add(self._log, f" Balance: {bal} ECO")
GLib.idle_add(self._log, f" Bloques: {info.get('blocks')}")
GLib.idle_add(self._log, f" Conexiones: {info.get('connections')}")
GLib.idle_add(self._log, f" Versión: {info.get('version')}")
GLib.idle_add(self._log, f" Dificultad: {info.get('difficulty')}")
GLib.idle_add(self._log, "────────────────────────────────")
GLib.idle_add(self._poll)
def _kill_oasis(self):
GLib.idle_add(self._log, "Deteniendo OASIS...")
try:
import os, signal as sig
if self._oasis_proc and self._oasis_proc.poll() is None:
# Matar el grupo de procesos completo (bash -lc + node hijo)
os.killpg(os.getpgid(self._oasis_proc.pid), sig.SIGTERM)
self._oasis_proc.wait(timeout=5)
self._oasis_proc = None
GLib.idle_add(self._log, "OASIS detenido.")
else:
# Fallback: matar cualquier node backend.js corriendo
subprocess.run(["pkill", "-f", "node.*backend.js"], timeout=5)
GLib.idle_add(self._log, "OASIS detenido.")
except Exception as e:
GLib.idle_add(self._log, f"[Error al detener]: {e}")
GLib.idle_add(self._poll)
# ── Main ───────────────────────────────────────────────────────────────────
def main():
signal.signal(signal.SIGINT, signal.SIG_DFL)
OasisPanel()
Gtk.main()
if __name__ == "__main__":
main()

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

@ -0,0 +1,119 @@
// =============================================================
// Solar Net Hub — app.js
// Bridge JS ↔ Python y lógica de UI
// =============================================================
// ── Bridge JS → Python ────────────────────────────────────────
function send(action) {
window.location.href = 'oasis://' + 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');
eCard.dataset.state = data.ecoin_running ? 'running' : (data.ecoin_installed ? 'stopped' : 'unknown');
if (data.ecoin_running) {
document.getElementById('ecoinState').textContent = 'ACTIVO';
document.getElementById('ecoinSub').textContent = 'daemon corriendo (headless)';
_btn('btnEStart', true);
_btn('btnEStop', false);
_btn('btnEGui', false);
_btn('btnEInfo', false);
} else if (data.ecoin_installed) {
document.getElementById('ecoinState').textContent = 'INSTALADO';
document.getElementById('ecoinSub').textContent = 'daemon detenido';
_btn('btnEStart', false);
_btn('btnEStop', true);
_btn('btnEGui', false);
_btn('btnEInfo', true);
} else {
document.getElementById('ecoinState').textContent = 'NO INSTALADO';
document.getElementById('ecoinSub').textContent = 'instala ECOIN para comenzar';
_btn('btnEStart', true);
_btn('btnEStop', true);
_btn('btnEGui', true);
_btn('btnEInfo', true);
}
// Info grid ecoin
document.getElementById('eBalance').textContent = data.ecoin_balance != null ? data.ecoin_balance + ' ECO' : '—';
document.getElementById('eBlocks').textContent = data.ecoin_blocks != null ? data.ecoin_blocks : '—';
document.getElementById('eConns').textContent = data.ecoin_connections != null ? data.ecoin_connections : '—';
document.getElementById('eWallet').textContent = data.ecoin_wallet ? '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;
}

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

@ -0,0 +1,104 @@
<!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="btnEStart" onclick="send('ecoin-start')" disabled>▶ INICIAR</button>
<button class="btn" id="btnEStop" onclick="send('ecoin-stop')" disabled>■ DETENER</button>
</div>
<div class="action-row">
<button class="btn" id="btnEGui" onclick="send('ecoin-gui')" disabled>◈ ABRIR GUI</button>
<button class="btn" id="btnEInfo" onclick="send('ecoin-info')" disabled>≡ VER INFO</button>
</div>
</div>
<div class="infobox">
<div class="info-row"><span class="ik">BALANCE</span> <span class="iv" id="eBalance"></span></div>
<div class="info-row"><span class="ik">BLOQUES</span> <span class="iv" id="eBlocks"></span></div>
<div class="info-row"><span class="ik">CONEXIONES</span> <span class="iv" id="eConns"></span></div>
<div class="info-row"><span class="ik">WALLET</span> <span class="iv" id="eWallet"></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; }

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" "$@"

124
start_panel_v3.sh Executable file
View file

@ -0,0 +1,124 @@
#!/usr/bin/env bash
# =============================================================
# SOLAR NET HUB — Panel v3 (WebKit2GTK)
# =============================================================
set -euo pipefail
REPO_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
PANEL_PY="$REPO_DIR/INSTALLER_V3/panel.py"
FONT_SRC="$REPO_DIR/INSTALLER_V3/Dune_Rise.otf"
ICON_SRC="$REPO_DIR/INSTALLER_V3/oasis-logo.png"
APPS_DIR="$HOME/.local/share/applications"
ICONS_DIR="$HOME/.local/share/icons/hicolor/256x256/apps"
C_OK='\033[1;32m'; C_FAIL='\033[1;31m'; C_WARN='\033[1;33m'
C_INFO='\033[1;36m'; C_RESET='\033[0m'
have(){ command -v "$1" >/dev/null 2>&1; }
check_display(){
[ -n "${DISPLAY:-}" ] || [ -n "${WAYLAND_DISPLAY:-}" ] && return
echo -e "${C_FAIL}✘ Sin sesión gráfica.${C_RESET}"; exit 1
}
install_font(){
local dst="$HOME/.local/share/fonts/Dune_Rise.otf"
[ -f "$FONT_SRC" ] && [ ! -f "$dst" ] && {
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}"
}
}
check_webkit(){
python3 -c "
import gi
for v in ('4.1','4.0'):
try:
gi.require_version('WebKit2', v)
from gi.repository import WebKit2
print(v)
break
except Exception:
pass
" 2>/dev/null
}
install_webkit(){
echo -e "${C_WARN}⚠ WebKit2GTK no encontrado. Instalando...${C_RESET}"
if have apt-get; then
sudo apt-get install -y python3-gi python3-gi-cairo \
gir1.2-webkit2-4.0 libwebkit2gtk-4.0-dev 2>/dev/null || \
sudo apt-get install -y python3-gi python3-gi-cairo \
gir1.2-webkit2-4.1 libwebkit2gtk-4.1-dev 2>/dev/null
elif have pacman; then
sudo pacman -Sy --noconfirm webkit2gtk python-gobject
elif have dnf; then
sudo dnf install -y python3-gobject webkit2gtk4.0
elif have zypper; then
sudo zypper --non-interactive install python3-gobject webkit2gtk3
else
echo -e "${C_FAIL}✘ Instala manualmente: gir1.2-webkit2-4.0${C_RESET}"
exit 1
fi
}
register_desktop(){
mkdir -p "$APPS_DIR" "$ICONS_DIR"
[ -f "$ICON_SRC" ] && cp -f "$ICON_SRC" "$ICONS_DIR/oasis.png"
# Borrar entradas antiguas con nombres distintos
rm -f "$APPS_DIR/solarnethub.desktop" \
"$APPS_DIR/solarnethub-panel.desktop" \
"$APPS_DIR/INSTALLER.desktop" \
"$APPS_DIR/oasis.desktop"
cat > "$APPS_DIR/oasis-panel.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=OASIS
GenericName=OASIS Panel
Comment=Panel de control OASIS y ECOIN
Exec=/bin/bash -lc '$REPO_DIR/start_panel_v3.sh'
Icon=oasis
Terminal=false
Categories=Network;System;Utility;
Keywords=oasis;ecoin;solar;hub;
StartupWMClass=oasis-panel
StartupNotify=true
EOF
# Copiar al Escritorio si existe
local desk
desk="$(xdg-user-dir DESKTOP 2>/dev/null || echo "$HOME/Desktop")"
[ -d "$desk" ] && cp -f "$APPS_DIR/oasis-panel.desktop" "$desk/" && \
chmod +x "$desk/oasis-panel.desktop" 2>/dev/null || true
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ú como 'Solar Net Hub'${C_RESET}"
}
# ── Main ──────────────────────────────────────────────────────
echo -e "${C_INFO}== SOLAR NET HUB :: Panel v3 (WebKit2GTK) ==${C_RESET}"
check_display
install_font 2>/dev/null || true
register_desktop 2>/dev/null || true
WK_VER="$(check_webkit)"
if [ -z "$WK_VER" ]; then
install_webkit
WK_VER="$(check_webkit)"
[ -z "$WK_VER" ] && {
echo -e "${C_FAIL}✘ WebKit2GTK no disponible tras instalación.${C_RESET}"
exit 1
}
fi
echo -e "${C_OK}✔ WebKit2 $WK_VER listo. Lanzando panel...${C_RESET}"
exec python3 "$PANEL_PY" "$@"