Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,16 @@ Dependencias Go (mantenidas a 2 a propósito):

## Registro de cambios

### v2.9.22

- **Nuevo sistema de temas con cuatro skins (#130)**: la interfaz incorpora cuatro looks distintos, conmutables desde *Ajustes → Apariencia*, manteniendo el clásico intacto para los usuarios actuales.
- **Clásico** — el look de siempre (claro/oscuro/sistema; es el default para usuarios existentes).
- **Moderno** — el default en instalaciones nuevas (oscuro o claro según tu SO): acento violeta, tarjetas sin borde con elevación suave, navegación activa en píldora y un halo radial sutil.
- **Fósforo** — estética CRT/terminal: JetBrains Mono en todo, radio 0, scanlines y rejilla de fondo, tarjetas con esquinas HUD, medidores segmentados y botones/badges cuadrados en mayúsculas, con identidad verde fósforo.
- **Brutalista** — papel crema con bordes negros de 2 px, sombras duras desplazadas, botón primario amarillo señal, títulos subrayados "marcador" y navegación activa negra con texto amarillo.
- **Tema y modo ahora van separados**: eliges una familia (*Clásico / Moderno / Fósforo / Brutalista*) y, para Clásico y Moderno, un modo (*Sistema / Claro / Oscuro*). Cada familia ofrece su propia paleta de acentos (con su acento default preseleccionado) y el picker funciona en todos los temas, mientras los colores de estado siguen siendo semánticos. Densidad y reducción de animaciones se conservan bajo cada skin.
- **Tipografía algo más grande** en el Dashboard y las secciones tipo Disk health, y el banner de rebuild ahora usa acento azul (info) en el tema Moderno.

### v2.9.21

- **Reduce la recolección de ZFS cuando la UI está cerrada (#126)**: la web UI mantiene un stream SSE abierto contra `/api/events`, así que `ZpoolCollector` usa el número de suscriptores del hub como señal de presencia. Con la UI abierta ejecuta una recolección completa cada `EASYZFS_ZPOOL_INTERVAL` (10 s por defecto). Con la UI cerrada pasa a un heartbeat ligero de salud/alertas cada `EASYZFS_ZPOOL_ALERT_INTERVAL` (60 s por defecto) y solo hace una recolección completa cada `EASYZFS_ZPOOL_IDLE_INTERVAL` (300 s por defecto). Las alertas en segundo plano (DEGRADED, scrub, capacidad) siguen evaluándose al menos cada minuto, y las series de capacidad siguen persistiendo cada 10 minutos, mientras que los nodos en reposo emiten muchísimos menos comandos `zpool`/`zfs`.
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,16 @@ Go dependencies (kept to 2 on purpose):

## Changelog

### v2.9.22

- **New theme system with four visual skins (#130)**: the interface now ships four distinct looks, switchable from *Settings → Appearance* while keeping the classic one intact for existing users.
- **Classic** — the familiar light/dark/system look (the default for existing users).
- **Modern** — the default for new installs (dark or light following your OS): violet accent, borderless cards with soft elevation, pill-shaped active nav and a subtle radial halo.
- **Phosphor** — a CRT/terminal aesthetic: JetBrains Mono everywhere, zero radius, scanlines and a grid background, HUD-cornered cards, segmented meters and squared uppercase buttons/badges, with a phosphor-green identity.
- **Brutalist** — cream paper with 2px black borders, hard offset shadows, signal-yellow primary buttons, marker-underlined titles and black active nav with yellow text.
- **Theme and mode are now separate**: pick a family (*Classic / Modern / Phosphor / Brutalist*) and, for Classic and Modern, a mode (*System / Light / Dark*). Each family offers its own accent palette (its default accent preselected) and the accent picker works on every theme while state colors stay semantic. Density and reduced-motion are preserved under every skin.
- **Slightly larger type** in the Dashboard and the disk-health sections, and the rebuild banner now uses a blue (info) accent in the Modern theme.

### v2.9.21

- **Throttle ZFS collector when no UI is connected (#126)**: the web UI keeps an SSE stream open to `/api/events`, so the `ZpoolCollector` now uses the hub's subscriber count as a presence signal. While the UI is open it runs a full collection every `EASYZFS_ZPOOL_INTERVAL` (default 10 s). When the UI is closed it falls back to a cheap health/alert heartbeat every `EASYZFS_ZPOOL_ALERT_INTERVAL` (default 60 s) and only runs a full collection every `EASYZFS_ZPOOL_IDLE_INTERVAL` (default 300 s). Background alerts (DEGRADED, scrub, capacity) keep firing at least every minute, and capacity series still persist every 10 minutes, while idle nodes issue dramatically fewer `zpool`/`zfs` commands.
Expand Down
43 changes: 34 additions & 9 deletions web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,47 @@
if(o!==null){ls.setItem(nuevo,o);ls.removeItem(legacy);}
return o;
}
var t=getk('easyzfs-theme-mode','zfc-theme');
var dark=t==='dark'||(t!=='light'&&window.matchMedia('(prefers-color-scheme: dark)').matches);
if(dark)document.documentElement.dataset.theme='dark';
// ESPEJO de ACCENTS en ui/theme.ts: si cambia una, cambia la otra.
var ACC={
cyan:{l:['#0e7c93','#dff0f4'],d:['#4cc3d9','#13292f']},
steel:{l:['#3a6ea5','#e2ebf4'],d:['#7ba7d9','#1b2634']},
emerald:{l:['#2f7d5f','#e3f0e9'],d:['#5cb893','#1d2f27']},
amber:{l:['#a8741f','#f6ecd9'],d:['#d9a84e','#33291a']}
};
var a=ACC[getk('easyzfs-accent','zfc-accent')]||ACC.emerald;
var v=dark?a.d:a.l,st=document.documentElement.style;
st.setProperty('--accent',v[0]);st.setProperty('--accent-soft',v[1]);
// --ok NO sigue al acento: es semántico (salud OK); vive solo en el CSS.
// ESPEJO de SKIN_BG en ui/theme.ts: si cambia una, cambia la otra.
var SKINBG={'modern-dark':'#0d0e12','modern-light':'#f4f5f8','phosphor':'#050810','brutalist':'#f2ede1'};
var st=document.documentElement.style, ms=document.querySelector('meta[name="theme-color"]');
// Modelo: familia (easyzfs-theme) + modo (easyzfs-theme-mode). Migración
// desde el modelo combinado anterior (auto|light|dark|modern-*|phosphor|brutalist).
var fam=ls.getItem('easyzfs-theme'), mod=ls.getItem('easyzfs-theme-mode');
if(fam===null){
var legacy=ls.getItem('easyzfs-theme-mode')||ls.getItem('zfc-theme');
if(legacy==='light'||legacy==='dark'||legacy==='auto'){fam='classic';mod=legacy==='auto'?'system':legacy;}
else if(legacy==='modern-dark'){fam='modern';mod='dark';}
else if(legacy==='modern-light'){fam='modern';mod='light';}
else if(legacy==='phosphor'||legacy==='brutalist'){fam=legacy;mod='system';}
else {fam='modern';mod='system';}
ls.setItem('easyzfs-theme',fam);ls.setItem('easyzfs-theme-mode',mod);
}
var systemDark=window.matchMedia('(prefers-color-scheme: dark)').matches;
var dark=(mod==='dark'||(mod==='system'&&systemDark));
var dt;
if(fam==='classic'){dt=dark?'dark':'';}
else if(fam==='modern'){dt=dark?'modern-dark':'modern-light';}
else {dt=fam;} // phosphor | brutalist
document.documentElement.dataset.theme=dt;
if(ms)ms.setAttribute('content',SKINBG[dt]||(dt==='dark'?'#0e1210':'#f6f6f3'));
// Acento: en familias no-classic manda su CSS (salvo override); en clásico se aplica inline.
if(fam!=='classic'){
st.removeProperty('--accent');st.removeProperty('--accent-soft');
var lum=fam==='modern'?(dark?'dark':'light'):(fam==='brutalist'?'light':'dark');
var ov=ls.getItem('easyzfs-accent-'+fam);
if(ov&&ACC[ov]){var ar=ACC[ov][lum];st.setProperty('--accent',ar[0]);st.setProperty('--accent-soft',ar[1]);}
}else{
var a=ACC[getk('easyzfs-accent','zfc-accent')]||ACC.emerald;
var v=dark?a.d:a.l;
st.setProperty('--accent',v[0]);st.setProperty('--accent-soft',v[1]);
}
if(getk('easyzfs-density','zfc-density')==='compact'){
document.documentElement.dataset.density='compact';st.fontSize='13.5px';
}
Expand All @@ -49,8 +76,6 @@
var L=getk('easyzfs-lang','zfc-lang');
document.documentElement.lang=(L==='es'||L==='en')?L:
(((navigator.language||'es').toLowerCase().indexOf('en')===0)?'en':'es');
var m=document.querySelector('meta[name="theme-color"]');
if(m)m.setAttribute('content',dark?'#0e1210':'#f6f6f3');
}catch(e){}})();
</script>
</head>
Expand Down
38 changes: 21 additions & 17 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ function AlertsPanel({ onClose }: { onClose: () => void }) {
}

function Shell() {
const { t, route, navigate, demo, exitDemo, user, isAdmin, ready, themeMode, themeEff, setTheme } = useApp();
const { t, route, navigate, demo, exitDemo, user, isAdmin, ready, family, mode, themeEff, setMode } = useApp();
const { openModal } = useModal();
const [showAlerts, setShowAlerts] = useState(false);
const [hasPending, setHasPending] = useState(false);
Expand Down Expand Up @@ -301,23 +301,27 @@ function Shell() {
<IconBell />
{hasPending && <span className="ping" />}
</button>
<button type="button" className="iconbtn theme-toggle-mobile"
aria-label={themeEff === 'dark' ? t('s_theme_toLight') : t('s_theme_toDark')}
title={themeEff === 'dark' ? t('s_theme_toLight') : t('s_theme_toDark')}
onClick={() => setTheme(themeEff === 'dark' ? 'light' : 'dark')}>
{themeEff === 'dark' ? <IconSun size={16} /> : <IconMoon size={16} />}
</button>
<div className="theme-pill" role="radiogroup" aria-label={t('s_theme')}>
{([['auto', IconMonitor], ['light', IconSun], ['dark', IconMoon]] as const).map(([m, Icon]) => (
<button key={m} type="button" role="radio" aria-checked={themeMode === m}
className={themeMode === m ? 'active' : ''}
title={t(`s_theme_${m}` as never)}
onClick={() => setTheme(m)}>
<Icon size={14} />
<span className="plbl">{t(`s_theme_${m}` as never)}</span>
{(family === 'classic' || family === 'modern') && (
<>
<button type="button" className="iconbtn theme-toggle-mobile"
aria-label={themeEff === 'dark' ? t('s_theme_toLight') : t('s_theme_toDark')}
title={themeEff === 'dark' ? t('s_theme_toLight') : t('s_theme_toDark')}
onClick={() => setMode(themeEff === 'dark' ? 'light' : 'dark')}>
{themeEff === 'dark' ? <IconSun size={16} /> : <IconMoon size={16} />}
</button>
))}
</div>
<div className="theme-pill" role="radiogroup" aria-label={t('s_theme')}>
{([['system', IconMonitor], ['light', IconSun], ['dark', IconMoon]] as const).map(([m, Icon]) => (
<button key={m} type="button" role="radio" aria-checked={mode === m}
className={mode === m ? 'active' : ''}
title={t(m === 'system' ? 's_theme_auto' : (`s_theme_${m}` as never))}
onClick={() => setMode(m)}>
<Icon size={14} />
<span className="plbl">{t(m === 'system' ? 's_theme_auto' : (`s_theme_${m}` as never))}</span>
</button>
))}
</div>
</>
)}
<button type="button" className={`topuser${active === 'settings' ? ' active' : ''}`}
onClick={() => navigate('settings')}
aria-label={user.display_name || user.user}
Expand Down
6 changes: 3 additions & 3 deletions web/src/components/Donut.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ export function Donut({ parts, size = 132, centerValue, centerLabel }: {
return el;
})
)}
<text x="21" y="20.5" textAnchor="middle" style={{ fontSize: 9, fontWeight: 700, fill: 'var(--text)' }}>{centerValue}</text>
<text x="21" y="27.5" textAnchor="middle" style={{ fontSize: 3.6, fill: 'var(--text2)' }}>{centerLabel}</text>
<text x="21" y="20.5" textAnchor="middle" style={{ fontSize: 10, fontWeight: 700, fill: 'var(--text)' }}>{centerValue}</text>
<text x="21" y="27.5" textAnchor="middle" style={{ fontSize: 3.9, fill: 'var(--text2)' }}>{centerLabel}</text>
</svg>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 12.5 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 13.5 }}>
{parts.map((p) => (
<span key={p.label} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, color: 'var(--text2)' }}>
<i style={{ width: 9, height: 9, borderRadius: '50%', background: p.color, display: 'inline-block' }} />
Expand Down
1 change: 1 addition & 0 deletions web/src/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const IconCheck = (p: P) => base({ size: 11, strokeWidth: 3.2, ...p }, <p
export const IconMonitor = (p: P) => base(p, <><rect x="2" y="4" width="20" height="13" rx="2" /><path d="M8 21h8M12 17v4" /></>);
export const IconUpload = (p: P) => base(p, <><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><path d="m17 8-5-5-5 5" /><path d="M12 3v12" /></>);
export const IconCode = (p: P) => base(p, <><path d="m16 18 6-6-6-6" /><path d="m8 6-6 6 6 6" /></>);
export const IconBlock = (p: P) => base(p, <><rect x="3" y="5" width="18" height="14" /><path d="M3 10h18" /></>);
export const IconShield = (p: P) => base(p, <path d="M12 22s8-3.6 8-10V5l-8-3-8 3v7c0 6.4 8 10 8 10z" />);
export const IconDownload = (p: P) => base(p, <><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><path d="m7 10 5 5 5-5" /><path d="M12 15V3" /></>);
export const IconList = (p: P) => base(p, <><path d="M8 6h13M8 12h13M8 18h13" /><path d="M3 6h.01M3 12h.01M3 18h.01" /></>);
Expand Down
Loading
Loading