Skip to content

Commit cebac54

Browse files
committed
feat: repo público — ribbon de actualización semanal, releases semver y CSP
El repo pasa a público (gh repo rename panelsolar → helios, historia saneada en commits previos). Con el repo público, el check de actualizaciones deja de ser humo (antes repo privado → 404 → siempre 'al día'). - UpdateRibbon: check semanal vía localStorage contra releases de GitHub (solo admin), enlace 'Ver novedades'. Patrón app-auto-update. - Ajustes: REPO_URL → gnacho/helios (el rewrite con filter-repo lo dejó corrupto como gdemo/panelsolar). - CSP server: connect-src https://api.github.com (Deltos ya lo tenía). - release.yml: releases estables semver (tests + build + tarballs por arch con node_modules pre-built + checksums.txt). - bump 0.5.0 → 0.6.0 (app + server + locks).
1 parent fdcd52e commit cebac54

9 files changed

Lines changed: 189 additions & 8 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: release
2+
3+
# Release estable por tag v*: empaqueta la webapp completa (frontend build +
4+
# server + node_modules de producción PRE-BUILT por arch) + checksums.txt.
5+
# El 'Comprobar actualizaciones' del front y el ribbon semanal consumen estas
6+
# releases (releases/latest, semver).
7+
8+
on:
9+
push:
10+
tags: ['v*']
11+
workflow_dispatch: # sin tag = empaqueta y sube como artifact de prueba
12+
13+
permissions:
14+
contents: write
15+
16+
jobs:
17+
build:
18+
strategy:
19+
fail-fast: false
20+
matrix:
21+
include:
22+
- arch: amd64
23+
runs-on: ubuntu-latest
24+
- arch: arm64
25+
runs-on: ubuntu-24.04-arm
26+
runs-on: ${{ matrix.runs-on }}
27+
steps:
28+
- uses: actions/checkout@v4
29+
30+
- uses: actions/setup-node@v4
31+
with:
32+
node-version: 22
33+
cache: npm
34+
cache-dependency-path: |
35+
app/package-lock.json
36+
server/package-lock.json
37+
38+
- name: Tests del server (vitest)
39+
working-directory: server
40+
run: npm ci --no-audit --no-fund && npm test
41+
42+
- name: Build frontend
43+
working-directory: app
44+
run: npm ci --no-audit --no-fund && npm run build
45+
46+
- name: node_modules de producción (sin dev, compila prebuilds del arch)
47+
working-directory: server
48+
run: rm -rf node_modules && npm ci --omit=dev --no-audit --no-fund
49+
50+
- name: Empaqueta release
51+
env:
52+
REF: ${{ github.ref_name }}
53+
run: |
54+
VERSION="${REF#v}"
55+
[ "${{ github.ref_type }}" = "tag" ] || VERSION="0.0.0-SNAPSHOT-${GITHUB_SHA::7}"
56+
mkdir -p stage/server
57+
cp -r app/dist stage/dist
58+
cp -r server/src server/package.json stage/server/
59+
cp -r shared stage/shared
60+
cp -r server/node_modules stage/server/node_modules
61+
tar -czf "helios_${VERSION}_linux_${{ matrix.arch }}.tar.gz" -C stage .
62+
sha256sum helios_*.tar.gz > checksums.txt
63+
cat checksums.txt
64+
65+
- name: Publica en la release (solo tags)
66+
if: github.ref_type == 'tag'
67+
uses: softprops/action-gh-release@v2
68+
with:
69+
files: |
70+
helios_*.tar.gz
71+
checksums.txt
72+
73+
- name: Sube artifact de prueba (solo workflow_dispatch)
74+
if: github.ref_type != 'tag'
75+
uses: actions/upload-artifact@v4
76+
with:
77+
name: helios-snapshot-linux-${{ matrix.arch }}
78+
path: |
79+
helios_*.tar.gz
80+
checksums.txt
81+
retention-days: 7

‎app/package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎app/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "helios",
33
"private": true,
4-
"version": "0.5.0",
4+
"version": "0.6.0",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { useEffect, useState } from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import { RefreshCw } from 'lucide-react';
4+
import { apiFetch } from '@/data/api-client';
5+
import pkg from '../../package.json';
6+
7+
const CHECK_KEY = 'helios-last-update-check';
8+
const CHECK_INTERVAL = 7 * 24 * 60 * 60 * 1000; // 1 vez por semana (regla app-auto-update)
9+
const REPO_URL = 'https://github.com/gnacho/helios';
10+
11+
/** a vs b semver ('1.10.0' > '1.9.0'); prefijos 'v' ignorados. */
12+
function compareSemver(a: string, b: string): number {
13+
const pa = a.replace(/^v/, '').split('.').map((n) => parseInt(n, 10) || 0);
14+
const pb = b.replace(/^v/, '').split('.').map((n) => parseInt(n, 10) || 0);
15+
for (let i = 0; i < 3; i++) {
16+
if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0);
17+
}
18+
return 0;
19+
}
20+
21+
/**
22+
* Ribbon de actualización (patrón app-auto-update: check semanal + aviso si hay
23+
* versión nueva en el repo público). Solo admin. GET anónima a GitHub releases
24+
* (el repo es público), 1 vez por semana vía localStorage; aplicar = redesplegar
25+
* en el servidor, así que el ribbon enlaza a la release.
26+
*/
27+
export default function UpdateRibbon() {
28+
const { t } = useTranslation();
29+
const [state, setState] = useState<'idle' | 'checking' | 'uptodate' | 'available' | 'error'>('idle');
30+
const [latestVersion, setLatestVersion] = useState('');
31+
32+
useEffect(() => {
33+
let stale = false;
34+
const run = async () => {
35+
try {
36+
// Solo admin ve el ribbon (como el resto de la zona de actualizaciones).
37+
const me = await apiFetch<{ user?: { role?: string } }>('/api/auth/me').catch(() => null);
38+
if (me?.user?.role !== 'admin') return;
39+
40+
const last = Number(window.localStorage.getItem(CHECK_KEY) || 0);
41+
if (Date.now() - last < CHECK_INTERVAL) return; // ya se comprobó esta semana
42+
window.localStorage.setItem(CHECK_KEY, String(Date.now()));
43+
44+
const repo = REPO_URL.match(/github\.com\/([^/]+\/[^/.]+)/)?.[1];
45+
if (!repo) return;
46+
setState('checking');
47+
const res = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {
48+
headers: { Accept: 'application/vnd.github+json' },
49+
});
50+
let version = '';
51+
if (res.ok) {
52+
const data = (await res.json()) as { tag_name?: string };
53+
version = data.tag_name || '';
54+
} else if (res.status === 404) {
55+
const tagRes = await fetch(`https://api.github.com/repos/${repo}/tags?per_page=1`, {
56+
headers: { Accept: 'application/vnd.github+json' },
57+
});
58+
if (tagRes.ok) {
59+
const tags = (await tagRes.json()) as { name?: string }[];
60+
version = tags[0]?.name || '';
61+
}
62+
}
63+
if (stale) return;
64+
if (!version || compareSemver(version, pkg.version) <= 0) setState('uptodate');
65+
else {
66+
setLatestVersion(version.replace(/^v/, ''));
67+
setState('available');
68+
}
69+
} catch {
70+
if (!stale) setState('error');
71+
}
72+
};
73+
void run();
74+
return () => {
75+
stale = true;
76+
};
77+
}, []);
78+
79+
if (state !== 'available') return null;
80+
81+
return (
82+
<div
83+
role="status"
84+
className="mb-4 flex items-center gap-2.5 rounded-xl border border-amber-500/35 bg-amber-500/10 px-3.5 py-2.5 text-[13px] font-semibold text-amber-500"
85+
>
86+
<RefreshCw className="h-4 w-4 shrink-0" />
87+
<span>{t('ajustes.about.updateAvailable', { version: latestVersion })}</span>
88+
<a
89+
href={`${REPO_URL}/releases`}
90+
target="_blank"
91+
rel="noreferrer"
92+
className="ml-auto flex h-8 shrink-0 items-center rounded-lg border border-amber-500/40 px-3 text-xs font-medium text-amber-500 transition-colors hover:bg-amber-500/15"
93+
>
94+
{t('ajustes.about.viewRelease')}
95+
</a>
96+
</div>
97+
);
98+
}

‎app/src/components/shell/AppLayout.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { fmtWeekdayDate } from '@/i18n';
2525
import BrandLogo from '@/components/BrandLogo';
2626
import ConnectionStatus from '@/components/ConnectionStatus';
2727
import AlertsBell from '@/components/AlertsBell';
28+
import UpdateRibbon from '@/components/UpdateRibbon';
2829
import ThemeToggle from '@/components/ThemeToggle';
2930
import Footer from '@/components/Footer';
3031
import HeliosToaster from '@/components/HeliosToaster';
@@ -422,6 +423,7 @@ export default function AppLayout({ children }: { children: ReactNode }) {
422423
</header>
423424

424425
<main className={cn('mx-auto max-w-[1440px] px-4 pb-24 pt-4 md:ml-16 md:px-6 md:pb-6 md:pt-6', lgMargin)}>
426+
<UpdateRibbon />
425427
{isDemo && <DemoBanner onExit={exitDemo} />}
426428
{children}
427429
<div className="hidden lg:block">

‎app/src/pages/Ajustes.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1642,7 +1642,7 @@ function InstallSection({ state, install }: { state: InstallState; install: () =
16421642

16431643
// ── § Acerca de ──────────────────────────────────────────────────────────────
16441644

1645-
const REPO_URL = 'https://github.com/gdemo/panelsolar';
1645+
const REPO_URL = 'https://github.com/gnacho/helios';
16461646

16471647
function compareSemver(a: string, b: string): number {
16481648
const pa = a.replace(/^v/, '').split('.').map((n) => parseInt(n, 10) || 0);

‎server/package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎server/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "helios-server",
3-
"version": "0.5.0",
3+
"version": "0.6.0",
44
"private": true,
55
"type": "module",
66
"engines": {

‎server/src/index.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ app.use('*', async (c, next) => {
123123
c.header('Referrer-Policy', 'strict-origin-when-cross-origin')
124124
c.header('Permissions-Policy', 'geolocation=(), microphone=(), camera=()')
125125
c.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
126-
c.header('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'")
126+
c.header('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' https://api.github.com")
127127
await next()
128128
})
129129
const sseClients = new Set()

0 commit comments

Comments
 (0)