fancy dash

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-22 11:23:32 +02:00
1 parent 3b317a4f0c
commit ccccdc2031
16 files changed
+1530 -34

No files matched your search

+383
View File
@@ -0,0 +1,383 @@
/* RustProxy Package Registry — dependency-free interactive client */
(() => {
'use strict';
const el = (id) => document.getElementById(id);
const CHANNEL_LABELS = {linux:'Linux',opnsense:'OPNsense',freebsd:'FreeBSD'};
const INSTALL_NAMES = {deb:'Debian / Ubuntu',rpm:'RHEL / Rocky / Alma / Fedora',opnsense:'OPNsense',freebsd:'FreeBSD native'};
const SECTION_LABELS = {overview:'Przegląd',packages:'Paczki i wersje',install:'Instalacja',repository:'Repozytoria'};
const esc = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[char]));
const collator = new Intl.Collator('pl', {numeric:true,sensitivity:'base'});
const formatNumber = new Intl.NumberFormat('pl-PL');
const site = ['linux','opnsense','freebsd'].includes(document.body.dataset.site) ? document.body.dataset.site : 'linux';
const state = {
site, catalog:null, section:'overview', installing:site === 'linux' ? 'deb' : site,
channel:site === 'linux' ? 'all' : site,
format:'all', search:'', sort:'date', rows:25, refreshing:false,
};
const formatDate = (value) => {
if (!value) return '—';
const time = new Date(value);
if (Number.isNaN(time.valueOf())) return '—';
return new Intl.DateTimeFormat('pl-PL', {year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}).format(time);
};
const formatShortDate = (value) => {
if (!value) return '—';
const time = new Date(value);
if (Number.isNaN(time.valueOf())) return '—';
return new Intl.DateTimeFormat('pl-PL', {day:'2-digit',month:'short',year:'numeric'}).format(time);
};
const formatSize = (value) => {
const size = Number(value);
if (!Number.isFinite(size) || size < 0) return '—';
if (size < 1024) return `${formatNumber.format(size)} B`;
if (size < 1024 ** 2) return `${(size / 1024).toFixed(1)} KB`;
return `${(size / 1024 ** 2).toFixed(1)} MB`;
};
const safeLink = (url) => {
try {
const resolved = new URL(url, window.location.href);
if (['http:','https:'].includes(resolved.protocol)) return resolved.href;
} catch (_) { /* invalid URL */ }
return '#';
};
const origin = (url) => typeof url === 'string' ? url.replace(/\/$/, '') : '';
const dateLabel = (kind) => kind === 'build' ? 'Data zapisana przez builder' : 'Data pliku (starsze buildy bez znacznika czasu)';
const setText = (id, value) => {const node = el(id);if (node) node.textContent = value;};
const versionText = (b) => b?.app_version ? `v${b.app_version}` : '—';
let toastTimer;
function toast(message) {
const node = el('toast');
node.textContent = message;
node.hidden = false;
window.clearTimeout(toastTimer);
toastTimer = window.setTimeout(() => {node.hidden = true;}, 2700);
}
function setTheme(theme) {
const target = ['dark','light','cream'].includes(theme) ? theme : 'dark';
document.documentElement.dataset.theme = target;
setText('theme-name', {dark:'Dark',light:'Light',cream:'Cream'}[target]);
try {localStorage.setItem('rustproxy-repo-theme', target);} catch (_) { /* optional */ }
}
function navigate(section) {
const next = SECTION_LABELS[section] ? section : 'overview';
state.section = next;
document.querySelectorAll('.view').forEach((view) => {view.hidden = view.id !== `view-${next}`;});
document.querySelectorAll('[data-section]').forEach((btn) => {
const active = btn.dataset.section === next;
btn.classList.toggle('active', active);
if (active) btn.setAttribute('aria-current','page'); else btn.removeAttribute('aria-current');
});
setText('crumb-section', SECTION_LABELS[next]);
if (window.location.hash !== `#${next}`) history.replaceState(null, '', `${location.pathname}${location.search}#${next}`);
window.scrollTo({top:0,behavior:'instant'});
}
function buildCard(build, channel) {
const live = !!build?.published;
const commit = build?.source_commit ? build.source_commit.slice(0,9) : '—';
const subtitle = channel === 'linux' ? 'APT / DNF · amd64 + x86_64' : channel === 'opnsense'
? `PKG · ${build?.opnsense_series || 'FreeBSD'}` : `PKG · ${build?.freebsd_abi || 'Native'}`;
const version = build?.package_version || (live ? build?.app_version : '') || '—';
return `<article class="build-card${live ? '' : ' off'}">
<div class="build-card-top"><span>${esc(CHANNEL_LABELS[channel].toUpperCase())} / ${channel === 'linux' ? 'SIGNED' : 'PKG'}</span><span class="pill ${live ? 'live' : 'missing'}">${live ? 'PUBLISHED' : 'BRAK BUILDU'}</span></div>
<div class="build-card-version">${esc(version)}</div><div class="build-card-sub">${esc(subtitle)}</div>
<div class="build-card-details"><div><small>BUILD DATE</small><strong title="${esc(dateLabel(build?.date_source))}">${esc(formatDate(build?.built_at))}</strong></div><div><small>COMMIT</small><strong class="commit">${esc(commit)}</strong></div></div>
</article>`;
}
function formatBadge(pkg) {
return `<span class="format-label${pkg.format === 'PKG' ? ' pkg' : ''}">${esc(pkg.format)}</span>`;
}
function tableRow(pkg, compact = false) {
const download = `<a class="download-link" href="${esc(safeLink(pkg.download_url))}" target="_blank" rel="noopener noreferrer" download title="Pobierz ${esc(pkg.name)} ${esc(pkg.version)}">${compact ? 'Pobierz' : 'Pobierz plik'} <span>↗</span></a>`;
const first = `<span class="artifact-name" title="${esc(pkg.name)}">${esc(pkg.name)}</span><span class="artifact-arch">${esc(pkg.architecture)}</span>`;
const format = `${formatBadge(pkg)}<span class="channel-tag ${pkg.format === 'PKG' ? 'pkg' : ''}">${esc(CHANNEL_LABELS[pkg.channel] || pkg.channel)}</span>`;
const date = `<span class="date-cell" title="${esc(dateLabel(pkg.date_source))}">${esc(formatDate(pkg.built_at))}</span>`;
if (compact) return `<tr><td>${first}</td><td>${format}</td><td><span class="version-code">${esc(pkg.version)}</span></td><td>${date}</td><td>${download}</td></tr>`;
return `<tr><td>${first}</td><td>${format}</td><td><span class="version-code">${esc(pkg.version)}</span></td><td>${date}</td><td><span class="date-cell">${esc(formatSize(pkg.size_bytes))}</span></td><td>${download}</td></tr>`;
}
function renderOverview(data) {
const main = data.builds?.[state.site];
const heroVersion = versionText(main);
setText('hero-version', heroVersion);
setText('hero-channel', state.site.toUpperCase());
setText('hero-date', formatDate(main?.built_at));
setText('hero-commit', main?.source_commit ? main.source_commit.slice(0,12) : '—');
el('hero-date').title = dateLabel(main?.date_source);
setText('stat-packages', formatNumber.format(data.summary?.packages_count || 0));
setText('stat-channels', `${data.summary?.channels_published || 0} / 3`);
setText('stat-date', formatShortDate(data.summary?.latest_build_at));
setText('stat-version', heroVersion);
setText('stat-release', main?.package_version ? `Pakiet: ${main.package_version}` : 'Brak opublikowanej wersji');
setText('stat-date-sub', 'Data ostatniej publikacji');
setText('pipeline-updated', formatDate(data.generated_at));
const ready = !!data.ready;
el('hero-status').innerHTML = `<span class="pulse-dot"></span> ${ready ? 'REPOZYTORIUM AKTYWNE' : 'OCZEKIWANIE NA BUILD'}`;
el('hero-status').classList.toggle('waiting', !ready);
el('sidebar-status').classList.toggle('waiting', !ready);
el('sidebar-status').querySelector('span').textContent = ready ? 'REPOSITORY ONLINE' : 'OCZEKIWANIE NA DANE';
const side = el('side-channels');
side.innerHTML = ['linux','opnsense','freebsd'].map((channel) => `<span class="side-channel${data.builds[channel]?.published ? '' : ' disabled'}"><i class="tiny-dot"></i>${esc(CHANNEL_LABELS[channel])}<small>${channel === 'linux' ? 'APT / DNF' : 'PKG'}</small></span>`).join('');
el('build-grid').innerHTML = ['linux','opnsense','freebsd'].map((channel) => buildCard(data.builds[channel],channel)).join('');
const recent = data.packages.slice(0,6);
el('recent-rows').innerHTML = recent.length ? recent.map((pkg) => tableRow(pkg,true)).join('') : '<tr><td colspan="5" class="empty-table">Nie opublikowano jeszcze żadnych paczek. Katalog zapełni się po pierwszym buildzie.</td></tr>';
}
function filteredPackages() {
const arr = state.catalog?.packages || [];
const search = state.search.trim().toLocaleLowerCase('pl-PL');
const filtered = arr.filter((pkg) => {
if (state.channel !== 'all' && pkg.channel !== state.channel) return false;
if (state.format !== 'all' && pkg.format !== state.format) return false;
if (!search) return true;
return [pkg.name,pkg.version,pkg.channel,pkg.format,pkg.architecture].some((field) => String(field ?? '').toLocaleLowerCase('pl-PL').includes(search));
});
if (state.sort === 'name') filtered.sort((a,b) => collator.compare(a.name,b.name) || collator.compare(b.version,a.version));
if (state.sort === 'version') filtered.sort((a,b) => collator.compare(b.version,a.version) || collator.compare(a.name,b.name));
if (state.sort === 'size') filtered.sort((a,b) => b.size_bytes - a.size_bytes);
if (state.sort === 'date') filtered.sort((a,b) => String(b.built_at).localeCompare(String(a.built_at)) || collator.compare(b.version,a.version));
return filtered;
}
function renderPackages() {
const data = state.catalog;
if (!data) return;
const all = filteredPackages();
const shown = all.slice(0,state.rows);
setText('packages-total', `${formatNumber.format(data.summary.packages_count)} paczek`);
setText('filter-count', `${formatNumber.format(all.length)} ${all.length === 1 ? 'wynik' : 'wyników'}`);
setText('result-range', all.length ? `Wyświetlono ${shown.length} z ${all.length}` : 'Brak pasujących paczek');
el('catalog-rows').innerHTML = shown.length ? shown.map((pkg) => tableRow(pkg)).join('') : '<tr><td colspan="6" class="empty-table">Brak paczek spełniających kryteria. Zmień filtry lub poczekaj na publikację.</td></tr>';
el('show-more').hidden = shown.length >= all.length;
}
function instructions(kind, data) {
const urls = data.repository_urls || {};
const linux = origin(urls.linux);
const opnsense = origin(urls.opnsense);
const freebsd = origin(urls.freebsd);
const distribution = data.apt_distribution || 'stable';
const component = data.apt_component || 'main';
if (kind === 'deb') {
return {
description:'Dodaj podpisane repozytorium APT, odśwież listę pakietów i zainstaluj RustProxy.',
signature:'GPG SIGNED',
note:'Obsługiwane: Debian / Ubuntu · architektura amd64.',
reference:`${linux}/apt/dists/${encodeURIComponent(distribution)}/InRelease`,
agent:'sudo apt install rustproxy-docker-agent',
code:[
`curl -fsSL ${linux}/repo-key.gpg \\`,
' | sudo gpg --dearmor -o /usr/share/keyrings/rustproxy.gpg',
'',
`echo "deb [signed-by=/usr/share/keyrings/rustproxy.gpg] ${linux}/apt ${distribution} ${component}" \\`,
' | sudo tee /etc/apt/sources.list.d/rustproxy.list',
'',
'sudo apt update',
'sudo apt install rustproxy',
].join('\n'),
};
}
if (kind === 'rpm') {
return {
description:'Dodaj podpisane repozytorium DNF/YUM i zainstaluj serwer RustProxy.',
signature:'GPG SIGNED',
note:'Obsługiwane: RHEL / Rocky / Alma / Fedora · architektura x86_64.',
reference:`${linux}/rpm/x86_64/repodata/repomd.xml`,
agent:'sudo dnf install rustproxy-docker-agent',
code:[
`sudo rpm --import ${linux}/repo-key.gpg`,
'',
"sudo tee /etc/yum.repos.d/rustproxy.repo > /dev/null <<'REPO'",
'[rustproxy]',
'name=RustProxy Repository',
`baseurl=${linux}/rpm/$basearch`,
'enabled=1',
'gpgcheck=1',
'repo_gpgcheck=1',
`gpgkey=${linux}/repo-key.gpg`,
'REPO',
'',
'sudo dnf install rustproxy',
].join('\n'),
};
}
if (kind === 'opnsense') {
return {
description:'Pobierz konfigurację repozytorium pkg i zainstaluj plugin os-rustproxy.',
signature:'UNSIGNED PKG',
note:'Repozytorium pkg nie ma podpisanego katalogu. Korzystaj tylko z zaufanego hosta HTTPS.',
reference:`${opnsense}/RustProxy.conf`,
agent:'sudo apt install rustproxy-docker-agent',
code:[
'mkdir -p /usr/local/etc/pkg/repos',
`fetch -o /usr/local/etc/pkg/repos/RustProxy.conf ${opnsense}/RustProxy.conf`,
'pkg update -f',
'pkg install os-rustproxy',
].join('\n'),
};
}
return {
description:'Zainstaluj natywny pakiet rustproxy i włącz usługę rc.d.',
signature:'UNSIGNED PKG',
note:'Dostępne po ustawieniu FREEBSD_NATIVE_ENABLED=1. Katalog pkg jest niesygnowany.',
reference:`${freebsd}/RustProxy-FreeBSD.conf`,
agent:'sudo dnf install rustproxy-docker-agent',
code:[
'mkdir -p /usr/local/etc/pkg/repos',
`fetch -o /usr/local/etc/pkg/repos/RustProxy-FreeBSD.conf ${freebsd}/RustProxy-FreeBSD.conf`,
'pkg update -f',
'pkg install rustproxy',
'sysrc rustproxy_enable=YES',
'service rustproxy start',
].join('\n'),
};
}
function renderInstall() {
if (!state.catalog) return;
const kind = state.installing;
const instructionsData = instructions(kind,state.catalog);
document.querySelectorAll('[data-install]').forEach((node) => {
const active = node.dataset.install === kind;
node.classList.toggle('active',active);
node.setAttribute('aria-selected', String(active));
});
setText('install-tab-title', INSTALL_NAMES[kind]);
setText('install-description', instructionsData.description);
setText('install-signature', instructionsData.signature);
el('install-signature').classList.toggle('live', instructionsData.signature === 'GPG SIGNED');
setText('install-command', instructionsData.code);
setText('install-note', instructionsData.note);
setText('agent-command', instructionsData.agent);
el('install-reference').href = safeLink(instructionsData.reference);
}
function endpointsCard(name, label, url, links, signed) {
return `<div class="endpoint-card">
<div class="endpoint-card-top"><h2>${esc(name)}</h2><span class="pill${signed ? ' live' : ''}">${esc(label)}</span></div>
<a href="${esc(safeLink(url))}" target="_blank" rel="noopener noreferrer">${esc(url)} ↗</a>
<ul>${links.map((link) => `<li><a href="${esc(safeLink(link.url))}" target="_blank" rel="noopener noreferrer"><span>${esc(link.name)}</span><span>↗</span></a></li>`).join('')}</ul>
</div>`;
}
function renderRepositories() {
const data = state.catalog;
if (!data) return;
const url = data.repository_urls;
const distribution = encodeURIComponent(data.apt_distribution || 'stable');
el('endpoints-grid').innerHTML = [
endpointsCard('Linux','GPG SIGNED',url.linux,[
{name:'Klucz publiczny GPG',url:`${url.linux}/repo-key.gpg`},
{name:'APT InRelease',url:`${url.linux}/apt/dists/${distribution}/InRelease`},
{name:'RPM repomd.xml',url:`${url.linux}/rpm/x86_64/repodata/repomd.xml`},
{name:'Build info',url:`${url.linux}/LINUX-BUILD-INFO.txt`},
],true),
endpointsCard('OPNsense','UNSIGNED PKG',url.opnsense,[
{name:'RustProxy.conf',url:`${url.opnsense}/RustProxy.conf`},
{name:'meta.conf',url:`${url.opnsense}/meta.conf`},
{name:'packagesite.pkg',url:`${url.opnsense}/packagesite.pkg`},
{name:'Build info',url:`${url.opnsense}/BUILD-INFO.txt`},
],false),
endpointsCard('FreeBSD','UNSIGNED PKG',url.freebsd,[
{name:'RustProxy-FreeBSD.conf',url:`${url.freebsd}/RustProxy-FreeBSD.conf`},
{name:'meta.conf',url:`${url.freebsd}/meta.conf`},
{name:'packagesite.pkg',url:`${url.freebsd}/packagesite.pkg`},
{name:'Build info',url:`${url.freebsd}/BUILD-INFO.txt`},
],false),
].join('');
el('metadata-table').innerHTML = ['linux','opnsense','freebsd'].map((channel) => {
const b = data.builds[channel] || {};
const rows = [
['WERSJA APLIKACJI',b.app_version || '—','latest-value'],
['WERSJA PACZKI',b.package_version || '—',''],
['DATA BUILDU',formatDate(b.built_at),''],
['SOURCE COMMIT',b.source_commit || '—',''],
['REWIZJA PAKOWANIA',b.packaging_revision || '—',''],
];
return `<div class="meta-block"><h3>${esc(CHANNEL_LABELS[channel])}</h3>${rows.map(([label,value,klass]) => `<div class="meta-row"><small>${esc(label)}</small><strong class="${klass}"${label === 'DATA BUILDU' ? ` title="${esc(dateLabel(b.date_source))}"` : ''}>${esc(value)}</strong></div>`).join('')}</div>`;
}).join('');
}
function renderAll(data) {
state.catalog = data;
renderOverview(data);
renderPackages();
renderInstall();
renderRepositories();
setText('last-sync', formatDate(data.generated_at));
setText('footer-time', formatDate(data.generated_at));
el('last-sync').innerHTML = `<i></i> ${esc(formatDate(data.generated_at))}`;
}
async function refresh() {
if (state.refreshing) return;
state.refreshing = true;
el('refresh').classList.add('busy');
el('refresh').disabled = true;
try {
const response = await fetch('/api/v1/packages', {cache:'no-store',headers:{'Accept':'application/json'}});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
if (!Array.isArray(data.packages) || !data.builds || !data.summary) throw new Error('Invalid catalog');
renderAll(data);
el('error-banner').hidden = true;
} catch (err) {
el('error-banner').hidden = false;
setText('last-sync','Błąd odczytu');
console.error('RustProxy repository:',err);
} finally {
state.refreshing = false;
el('refresh').classList.remove('busy');
el('refresh').disabled = false;
}
}
async function copyText(text) {
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
} else {
const input = document.createElement('textarea');
input.value = text;
input.setAttribute('readonly','');
input.className = 'clipboard-helper';
document.body.appendChild(input);
input.focus();input.select();
const copied = document.execCommand('copy');
input.remove();
if (!copied) throw new Error('Clipboard unsupported');
}
toast('Skopiowano do schowka');
} catch (_) { toast('Nie udało się skopiować — zaznacz polecenia ręcznie'); }
}
function init() {
setText('year',new Date().getFullYear());
try {setTheme(localStorage.getItem('rustproxy-repo-theme') || 'dark');} catch (_) {setTheme('dark');}
document.querySelectorAll('[data-section]').forEach((button) => button.addEventListener('click',() => navigate(button.dataset.section)));
document.querySelectorAll('[data-go-section]').forEach((button) => button.addEventListener('click',() => navigate(button.dataset.goSection)));
document.querySelectorAll('[data-install]').forEach((button) => button.addEventListener('click',() => {state.installing = button.dataset.install;renderInstall();}));
document.querySelectorAll('[data-copy-target]').forEach((button) => button.addEventListener('click',() => copyText(el(button.dataset.copyTarget)?.textContent || '')));
el('theme-toggle').addEventListener('click',() => {
const themes = ['dark','light','cream'];
const i = themes.indexOf(document.documentElement.dataset.theme);
setTheme(themes[(i+1)%themes.length]);
});
el('refresh').addEventListener('click',() => {refresh();});
el('retry-inline').addEventListener('click',() => {refresh();});
el('search-input').addEventListener('input',(event) => {state.search = event.target.value;state.rows = 25;renderPackages();});
el('filter-channel').value = state.channel;
el('filter-channel').addEventListener('change',(event) => {state.channel = event.target.value;state.rows = 25;renderPackages();});
el('filter-format').addEventListener('change',(event) => {state.format = event.target.value;state.rows = 25;renderPackages();});
el('filter-sort').addEventListener('change',(event) => {state.sort = event.target.value;state.rows = 25;renderPackages();});
el('show-more').addEventListener('click',() => {state.rows += 25;renderPackages();});
window.addEventListener('hashchange',() => navigate(window.location.hash.slice(1)));
navigate(window.location.hash.slice(1) || 'overview');
refresh();
window.setInterval(() => {if (!document.hidden) refresh();}, 60000);
document.addEventListener('visibilitychange',() => {if (!document.hidden) refresh();});
}
init();
})();
+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<rect width="64" height="64" rx="11" fill="#131615"/>
<rect x="5" y="5" width="54" height="54" rx="8" fill="none" stroke="#f69b74" stroke-width="2"/>
<text x="11" y="42" fill="#f69b74" font-size="27" font-family="monospace" font-weight="bold">r</text>
<text x="26" y="42" fill="#bdd293" font-size="26" font-family="monospace" font-weight="bold">/</text>
<text x="39" y="42" fill="#f69b74" font-size="27" font-family="monospace" font-weight="bold">p</text>
</svg>

After

Width:  |  Height:  |  Size: 547 B

+136
View File
@@ -0,0 +1,136 @@
<!doctype html>
<html lang="pl" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="referrer" content="no-referrer">
<meta name="theme-color" content="#131615">
<title>RustProxy · Package Registry</title>
<link rel="icon" type="image/svg+xml" href="/ui/favicon.svg">
<link rel="stylesheet" href="/ui/style.css">
<script defer src="/ui/app.js"></script>
</head>
<body data-site="__REPOSITORY_SITE__">
<div class="shell">
<aside class="sidebar" id="sidebar">
<a href="#overview" class="brand" aria-label="RustProxy Package Registry">
<span class="brand-mark" aria-hidden="true">r<span>/</span>p</span>
<span class="brand-text">RustProxy <small>PACKAGE REGISTRY</small></span>
</a>
<div class="edition">INDEPENDENT BY DESIGN <span>BUILD DISTRIBUTION</span></div>
<div class="sidebar-label">WORKSPACE / 01</div>
<nav class="main-navigation" aria-label="Nawigacja panelu">
<button class="nav active" data-section="overview" aria-current="page"><span>01</span><b>Przegląd</b><span class="nav-arrow">↗</span></button>
<button class="nav" data-section="packages"><span>02</span><b>Paczki i wersje</b><span class="nav-arrow">↗</span></button>
<button class="nav" data-section="install"><span>03</span><b>Instalacja</b><span class="nav-arrow">↗</span></button>
<button class="nav" data-section="repository"><span>04</span><b>Repozytoria</b><span class="nav-arrow">↗</span></button>
</nav>
<div class="sidebar-label sidebar-label--second">DOSTĘPNE KANAŁY</div>
<div class="side-channels" id="side-channels">
<span class="side-channel"><i class="tiny-dot"></i> Linux <small>APT / DNF</small></span>
<span class="side-channel"><i class="tiny-dot"></i> OPNsense <small>PKG</small></span>
<span class="side-channel"><i class="tiny-dot"></i> FreeBSD <small>PKG</small></span>
</div>
<div class="sidebar-bottom">
<div class="small-label">REPOSITORY ENGINE</div>
<p>Python / HTTP API<br>Nginx / Signed APT + RPM<br>FreeBSD pkg catalog</p>
<div class="local-tag" id="sidebar-status"><i></i><span>OCZEKIWANIE NA DANE</span></div>
<div class="copyright">© RustProxy <span id="year"></span></div>
</div>
</aside>
<div class="workspace">
<header class="topbar">
<div class="crumb"><span>RUSTPROXY</span><b>/</b><span>REPOSITORY</span><b>/</b><strong id="crumb-section">Przegląd</strong></div>
<div class="top-actions">
<span class="live-sync" id="last-sync"><i></i> Odczyt danych…</span>
<button class="quiet icon-button" id="refresh" title="Odśwież listę paczek" aria-label="Odśwież dane">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 7v5h-5M4 17v-5h5"/><path d="M5 9a8 8 0 0 1 13.4-2.2L20 12M4 12l1.6 5.2A8 8 0 0 0 19 15"/></svg>
</button>
<button class="quiet theme-switch" id="theme-toggle" title="Zmień motyw" aria-label="Zmień motyw"><span class="theme-glyph">◐</span><span id="theme-name">Dark</span></button>
</div>
</header>
<main class="main-content">
<div class="error-banner" id="error-banner" hidden role="status">Nie można pobrać katalogu. Automatyczna próba ponowienia za minutę. <button id="retry-inline">Spróbuj teraz</button></div>
<section class="view" id="view-overview" aria-labelledby="overview-title">
<div class="page-head">
<div>
<div class="eyebrow"><span class="eyebrow-line"></span> DISTRIBUTION CONTROL ROOM <span class="eyebrow-separator">/</span> LIVE CATALOG</div>
<h1 id="overview-title">Pakiety. Wersje.<br><em>Pełna kontrola.</em></h1>
<p class="lead">Jedno miejsce dla wydań RustProxy na Linux, OPNsense i FreeBSD. Aktualne buildy, daty publikacji i gotowe instrukcje instalacji.</p>
</div>
<div class="page-head-side"><div class="subtle-label">STATUS / LIVE DATA</div><span class="active-status" id="hero-status"><span class="pulse-dot"></span> ODCZYTYWANIE</span><div class="muted-detail">Odświeżanie automatyczne co 60 sekund</div></div>
</div>
<div class="hero-grid">
<div class="hero-release">
<div class="hero-header"><span><span class="dot-amber"></span> NAJNOWSZE WYDANIE <span id="hero-channel">LINUX</span></span><span class="hero-corner">R / P — 01</span></div>
<div class="hero-version-label">RUSTPROXY / APPLICATION VERSION</div>
<div class="hero-version" id="hero-version">—</div>
<div class="hero-foot">
<div><small>ZBUDOWANO / OPUBLIKOWANO</small><strong id="hero-date">Brak informacji</strong></div>
<div><small>SOURCE COMMIT</small><strong class="mono" id="hero-commit">—</strong></div>
<button class="hero-link" data-go-section="packages">Pokaż wydania <span aria-hidden="true">↗</span></button>
</div>
<div class="hero-background-rings" aria-hidden="true"></div>
</div>
<div class="pipeline-panel">
<div class="pipeline-head"><span class="small-label">BUILD PIPELINE</span><span class="pipeline-live"><i class="tiny-dot"></i> READ ONLY</span></div>
<p>Od źródeł do gotowych paczek.</p>
<div class="pipeline-list">
<div class="pipeline-step"><span class="step-index">01</span><span class="step-info"><b>Git / source</b><small>RustProxy upstream</small></span><span class="step-right">GIT <span>↗</span></span></div>
<div class="pipeline-connector"></div>
<div class="pipeline-step"><span class="step-index">02</span><span class="step-info"><b>Build / package</b><small>Rust + Go + FreeBSD</small></span><span class="step-right">CI <span>↗</span></span></div>
<div class="pipeline-connector"></div>
<div class="pipeline-step"><span class="step-index">03</span><span class="step-info"><b>Signed repository</b><small>APT / RPM / PKG</small></span><span class="step-right step-success">LIVE <span>●</span></span></div>
</div>
<div class="pipeline-footer">Ostatni odczyt <span id="pipeline-updated">—</span></div>
</div>
</div>
<div class="stats-grid" aria-label="Statystyki repozytorium">
<div class="stat"><span>01 / PACZEK W KATALOGU</span><strong id="stat-packages">—</strong><small>Wszystkie opublikowane artefakty</small></div>
<div class="stat"><span>02 / AKTYWNE KANAŁY</span><strong id="stat-channels">—</strong><small>Linux · OPNsense · FreeBSD</small></div>
<div class="stat"><span>03 / OSTATNI BUILD</span><strong class="stat-date" id="stat-date">—</strong><small id="stat-date-sub">UTC / czas publikacji</small></div>
<div class="stat"><span>04 / AKTUALNA WERSJA</span><strong id="stat-version">—</strong><small id="stat-release">Wersja aplikacji</small></div>
</div>
<div class="section-heading"><div><span class="section-index">01 / RELEASE CHANNELS</span><h2>Ostatnie buildy</h2></div><button class="text-link" data-go-section="packages">Cały katalog <span>↗</span></button></div>
<div class="build-grid" id="build-grid"><div class="skeleton-card">Ładowanie kanałów…</div></div>
<div class="section-heading section-heading--second"><div><span class="section-index">02 / RECENT ARTIFACTS</span><h2>Najnowsze paczki</h2></div><button class="text-link" data-go-section="packages">Przeglądaj paczki <span>↗</span></button></div>
<div class="card recent-card">
<div class="table-scroller"><table class="data-table"><thead><tr><th>PAKIET</th><th>FORMAT / KANAŁ</th><th>WERSJA PACZKI</th><th>DATA BUILDU</th><th class="table-action-col">PLIK</th></tr></thead><tbody id="recent-rows"><tr><td colspan="5" class="empty-table">Ładowanie danych…</td></tr></tbody></table></div>
</div>
</section>
<section class="view" id="view-packages" aria-labelledby="packages-title" hidden>
<div class="page-head compact-head"><div><div class="eyebrow"><span class="eyebrow-line"></span> ARTIFACT EXPLORER <span class="eyebrow-separator">/</span> VERSIONS</div><h1 id="packages-title">Katalog <em>paczek.</em></h1><p class="lead">Wyszukuj, filtruj, sortuj i pobieraj opublikowane pliki. Wszystkie dane są odczytywane bezpośrednio z aktualnego repozytorium.</p></div><div class="count-pill" id="packages-total">0 paczek</div></div>
<div class="filter-panel"><div class="search-field"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m16.5 16.5 4 4"/></svg><input id="search-input" type="search" placeholder="Szukaj nazwy, wersji, architektury..." aria-label="Szukaj paczek" autocomplete="off"></div><label class="filter-input">KANAŁ<select id="filter-channel"><option value="all">Wszystkie</option><option value="linux">Linux</option><option value="opnsense">OPNsense</option><option value="freebsd">FreeBSD</option></select></label><label class="filter-input">FORMAT<select id="filter-format"><option value="all">Wszystkie</option><option value="DEB">DEB</option><option value="RPM">RPM</option><option value="PKG">PKG</option></select></label><label class="filter-input">SORTOWANIE<select id="filter-sort"><option value="date">Najnowsze</option><option value="version">Wersja ↓</option><option value="name">Nazwa A–Z</option><option value="size">Rozmiar ↓</option></select></label></div>
<div class="result-topline"><span id="filter-count">Ładowanie…</span><span>Wersje i daty pochodzą z paczek i metadanych buildu</span></div>
<div class="card catalog-card"><div class="table-scroller"><table class="data-table data-table--catalog"><thead><tr><th>PAKIET</th><th>KANAŁ</th><th>WERSJA</th><th>DATA BUILDU</th><th>ROZMIAR</th><th>POBIERZ</th></tr></thead><tbody id="catalog-rows"><tr><td colspan="6" class="empty-table">Ładowanie danych…</td></tr></tbody></table></div><div class="catalog-bottom"><span id="result-range">—</span><button class="quiet show-more" id="show-more" hidden>Załaduj kolejne 25 ↓</button></div></div>
</section>
<section class="view" id="view-install" aria-labelledby="install-title" hidden>
<div class="page-head compact-head"><div><div class="eyebrow"><span class="eyebrow-line"></span> GET STARTED <span class="eyebrow-separator">/</span> INSTALLATION</div><h1 id="install-title">Zainstaluj <em>RustProxy.</em></h1><p class="lead">Gotowe polecenia dodania repozytorium i instalacji. Wybierz platformę, skopiuj komendy do terminala.</p></div></div>
<div class="install-tabs" role="tablist" aria-label="Wybór systemu"><button role="tab" aria-selected="true" data-install="deb" class="install-tab active">Debian / Ubuntu <small>APT · amd64</small></button><button role="tab" aria-selected="false" data-install="rpm" class="install-tab">RHEL / Rocky <small>DNF · x86_64</small></button><button role="tab" aria-selected="false" data-install="opnsense" class="install-tab">OPNsense <small>FreeBSD pkg</small></button><button role="tab" aria-selected="false" data-install="freebsd" class="install-tab">FreeBSD <small>Native pkg</small></button></div>
<div class="install-card card"><div class="install-card-head"><div><span class="section-index">TERMINAL / INSTALL</span><h2 id="install-tab-title">Debian / Ubuntu</h2><p id="install-description">Dodanie podpisanego repo APT i instalacja serwera.</p></div><span class="pill" id="install-signature">GPG SIGNED</span></div><div class="code-window"><div class="code-topbar"><span><i class="tiny-dot"></i><i class="tiny-dot"></i><i class="tiny-dot"></i> <b>install.sh</b></span><button class="copy-code" id="copy-install" data-copy-target="install-command">Kopiuj <span>↗</span></button></div><pre><code id="install-command">Przygotowywanie poleceń…</code></pre></div><div class="install-foot"><span id="install-note">Wymagane uprawnienia sudo.</span><a id="install-reference" class="text-link" href="#">Pliki repozytorium ↗</a></div></div>
<div class="install-extra-grid"><div class="card hint-card"><span class="hint-icon">01</span><h3>Docker discovery agent</h3><p>Linux ma również osobną paczkę <code>rustproxy-docker-agent</code>. Instalujesz ją niezależnie od głównego serwera.</p><div class="inline-command"><code id="agent-command">sudo apt install rustproxy-docker-agent</code><button data-copy-target="agent-command" class="quiet">Kopiuj</button></div></div><div class="card hint-card"><span class="hint-icon">02</span><h3>Podpisy i zaufanie</h3><p>Repozytoria APT i DNF w builderze są podpisane GPG. Repozytoria OPNsense i natywnego FreeBSD używają obecnie katalogów <code>signature_type: none</code>; korzystaj z zaufanego źródła HTTPS.</p></div></div>
</section>
<section class="view" id="view-repository" aria-labelledby="repository-title" hidden>
<div class="page-head compact-head"><div><div class="eyebrow"><span class="eyebrow-line"></span> REPOSITORY ENDPOINTS <span class="eyebrow-separator">/</span> METADATA</div><h1 id="repository-title">Źródła <em>pakietów.</em></h1><p class="lead">Adresy publicznych repozytoriów, podpisów i katalogów paczek. Aktualne metadane pochodzą z opublikowanych buildów.</p></div></div>
<div class="endpoints-grid" id="endpoints-grid"></div>
<div class="section-heading section-heading--second"><div><span class="section-index">SOURCE TRACE</span><h2>Informacje o wydaniach</h2></div></div>
<div class="card metadata-card"><div class="meta-table" id="metadata-table"><span>Wczytywanie…</span></div></div>
</section>
<footer class="page-footer"><span>RUSTPROXY <b>/</b> PACKAGE REGISTRY</span><span>Live catalog · Python / Nginx <b>·</b> <span id="footer-time">—</span></span></footer>
</main>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
</body>
</html>
+390
View File
@@ -0,0 +1,390 @@
/* RustProxy Package Registry · palette adapted from RustProxy 0.34.6 */
:root {
color-scheme: dark;
--bg:#131615; --panel:#1b1f1d; --panel2:#202622; --line:#323a34;
--muted:#a0aaa0; --text:#edf0e9; --accent:#f69b74; --green:#bdd293;
--red:#ffaaa2; --sidebar-bg:#101310; --brand-secondary:#cfe0ac;
--nav-active-bg:#242b23; --nav-hover-bg:#20251f; --text-subtle:#879082;
--separator:#596454; --border-hover:#89917c; --field-bg:#131813;
--field-line:#47523f; --placeholder:#778070; --row-line:#2b332c;
--outline-line:#717b67; --code-bg:#101810; --notice-text:#c9b99c;
--notice-line:#554833; --notice-bg:#29251c; --pill-line:#6d5a45;
--pill-live-line:#576847; --shadow:rgba(0,0,0,.22); --accent-ink:#221810;
--ring1:rgba(246,155,116,.08); --ring2:rgba(189,210,147,.04);
--mono:ui-monospace,SFMono-Regular,Consolas,"Liberation Mono",monospace;
--sans:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
}
html[data-theme="light"] {
color-scheme: light;
--bg:#f4f6f2; --panel:#fff; --panel2:#edf1eb; --line:#cbd3c7;
--muted:#5e685f; --text:#1d241f; --accent:#b85b38; --green:#5f783f;
--red:#b64a43; --sidebar-bg:#eef2ec; --brand-secondary:#6b8249;
--nav-active-bg:#e1e8dc; --nav-hover-bg:#e8ede6; --text-subtle:#687369;
--separator:#9ba69b; --border-hover:#839083; --field-bg:#fff;
--field-line:#b7c1b5; --placeholder:#7a867b; --row-line:#e0e5dd;
--outline-line:#98a497; --code-bg:#edf3e9; --notice-text:#685b45;
--notice-line:#d4c2a5; --notice-bg:#fff9ef; --pill-line:#cbb28f;
--pill-live-line:#a8ba8d; --shadow:rgba(31,39,33,.1); --accent-ink:#fff;
--ring1:rgba(184,91,56,.055); --ring2:rgba(95,120,63,.045);
}
html[data-theme="cream"] {
color-scheme: light;
--bg:#f2ede3; --panel:#fcf9f2; --panel2:#eae3d6; --line:#d5cabb;
--muted:#6b6257; --text:#302c27; --accent:#8c5942; --green:#5e7655;
--red:#a54645; --sidebar-bg:#eae2d5; --brand-secondary:#668367;
--nav-active-bg:#e0d3c0; --nav-hover-bg:#e6dccc; --text-subtle:#786e61;
--separator:#afa494; --border-hover:#968776; --field-bg:#fffcf7;
--field-line:#baad9a; --placeholder:#807466; --row-line:#e2d9cb;
--outline-line:#9c8d7c; --code-bg:#f0e8da; --notice-text:#765e40;
--notice-line:#d9c3a1; --notice-bg:#f8efdf; --pill-line:#c2a78d;
--pill-live-line:#b0bd9d; --shadow:rgba(64,49,34,.08); --accent-ink:#fff;
--ring1:rgba(140,89,66,.075); --ring2:rgba(94,118,85,.045);
}
*,*::before,*::after{box-sizing:border-box}
html{scroll-behavior:smooth}
body{margin:0;color:var(--text);background:var(--bg);font:13px/1.6 var(--sans);min-width:320px}
button,input,select{font:inherit}
button{cursor:pointer}
button:disabled{opacity:.5;cursor:wait}
a{color:inherit;text-decoration:none}
a:hover{text-decoration:none}
h1,h2,h3,p{margin:0}
h1,h2,h3{line-height:1.18}
[hidden]{display:none!important}
button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
::selection{background:var(--accent);color:var(--accent-ink)}
.shell{display:grid;grid-template-columns:250px minmax(0,1fr);min-height:100vh}
.sidebar{position:sticky;top:0;height:100vh;min-height:600px;display:flex;flex-direction:column;padding:30px 19px 24px;background:var(--sidebar-bg);border-right:1px solid var(--line)}
.brand{display:flex;align-items:center;gap:11px;padding:1px 3px;font-size:19px;font-weight:770;letter-spacing:-.8px;line-height:1.12}
.brand-mark{width:49px;height:49px;flex:0 0 49px;display:flex;align-items:center;justify-content:center;border:1px solid var(--accent);border-radius:3px;color:var(--accent);font:700 29px var(--mono);letter-spacing:-3px;padding-right:4px}
.brand-mark span{color:var(--brand-secondary);font-size:25px}
.brand-text{display:block}
.brand-text small{display:block;font:9px/1.4 var(--mono);letter-spacing:1.6px;color:var(--green);margin-top:5px}
.edition{padding:19px 4px 28px;color:var(--muted);font:9px/1.6 var(--mono);letter-spacing:1.15px;border-bottom:1px solid var(--line)}
.edition span{display:block;color:var(--accent);margin-top:5px}
.sidebar-label{margin:29px 0 11px 11px;color:var(--text-subtle);font:10px var(--mono);letter-spacing:1px}
.main-navigation{display:flex;flex-direction:column;gap:5px}
.nav{display:flex;gap:12px;align-items:center;padding:12px 12px 12px 14px;min-height:43px;border:0;border-left:2px solid transparent;color:var(--muted);background:transparent;text-align:left;transition:background .14s ease,color .14s ease;border-radius:2px}
.nav>span:first-child{font:10px var(--mono);opacity:.75}
.nav b{font-size:12.5px;font-weight:540;flex:1}
.nav-arrow{font-size:12px;opacity:0;transition:opacity .14s}
.nav:hover{background:var(--nav-hover-bg);color:var(--text)}
.nav:hover .nav-arrow{opacity:.9}
.nav.active{background:var(--nav-active-bg);color:var(--brand-secondary);border-left-color:var(--brand-secondary)}
.nav.active .nav-arrow{opacity:.9}
.sidebar-label--second{margin-top:36px}
.side-channels{display:flex;flex-direction:column;gap:5px;padding:0 7px}
.side-channel{display:flex;align-items:center;gap:11px;color:var(--muted);font-size:12px;padding:7px 8px}
.side-channel.disabled .tiny-dot{opacity:.25}
.side-channel small{font:10px var(--mono);color:var(--text-subtle);margin-left:auto}
.tiny-dot{width:6px;height:6px;display:inline-block;flex-shrink:0;background:var(--green);border-radius:50%}
.sidebar-bottom{margin-top:auto;padding:24px 10px 0;border-top:1px solid var(--line)}
.small-label{color:var(--muted);font:10px/1.3 var(--mono);letter-spacing:.7px}
.sidebar-bottom p{color:var(--text-subtle);font-size:11px;line-height:1.85;margin:12px 0 23px}
.local-tag{display:flex;gap:9px;align-items:center;color:var(--green);font:10px var(--mono);letter-spacing:.4px}
.local-tag i,.live-sync i{width:6px;height:6px;border-radius:50%;background:var(--green);display:inline-block;flex:none}
.local-tag.waiting{color:var(--muted)}
.local-tag.waiting i{background:var(--muted)}
.copyright{margin-top:30px;color:var(--text-subtle);font:10px var(--mono)}
.workspace{min-width:0}
.topbar{height:70px;padding:0 clamp(25px,4.3vw,72px);border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;gap:14px;background:var(--bg);position:sticky;top:0;z-index:10}
.crumb{display:flex;gap:13px;align-items:center;color:var(--muted);font:10px var(--mono);letter-spacing:1px;white-space:nowrap}
.crumb b{color:var(--separator);font-weight:400}
.crumb strong{color:var(--text);font-weight:500}
.top-actions{display:flex;gap:9px;align-items:center}
.live-sync{margin-right:11px;color:var(--muted);display:flex;align-items:center;gap:10px;font:10px var(--mono);white-space:nowrap}
.quiet{color:var(--muted);background:transparent;border:1px solid var(--line);border-radius:3px;padding:8px 12px;white-space:nowrap;transition:color .14s,border-color .14s}
.quiet:hover{color:var(--text);border-color:var(--border-hover)}
.icon-button{display:inline-flex;align-items:center;justify-content:center;width:36px;height:35px;padding:0}
.icon-button.busy svg{animation:spin .9s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.theme-switch{height:35px;display:flex;align-items:center;gap:8px;padding:0 12px}
.theme-glyph{font-size:18px;line-height:1;color:var(--accent)}
.main-content{padding:47px clamp(25px,4.3vw,72px) 25px;max-width:1660px;margin:0 auto}
.view{min-height:55vh}
.page-head{display:flex;justify-content:space-between;align-items:flex-end;gap:24px;margin-bottom:29px}
.page-head h1{font-size:clamp(34px,3.8vw,54px);font-weight:550;letter-spacing:-2.3px;line-height:1.1}
.page-head h1 em{font-style:normal;color:var(--accent)}
.eyebrow{display:flex;align-items:center;gap:9px;font:10px var(--mono);color:var(--green);letter-spacing:1.9px;margin-bottom:21px}
.eyebrow-line{height:1px;width:20px;background:var(--green)}
.eyebrow-separator{color:var(--separator);margin:0 1px}
.lead{max-width:680px;color:var(--muted);font-size:13px;line-height:1.82;margin-top:17px}
.page-head-side{align-self:flex-end;text-align:right;padding-bottom:4px;flex-shrink:0}
.subtle-label{color:var(--text-subtle);font:9px var(--mono);letter-spacing:1px;margin-bottom:8px}
.active-status{display:inline-flex;gap:9px;align-items:center;font:10px var(--mono);letter-spacing:.8px;color:var(--green);border:1px solid var(--pill-live-line);padding:7px 10px}
.pulse-dot{width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 0 4px color-mix(in srgb,var(--green) 12%,transparent)}
.active-status.waiting{color:var(--muted);border-color:var(--line)}
.active-status.waiting .pulse-dot{background:var(--muted);box-shadow:none}
.muted-detail{font:10px var(--mono);color:var(--text-subtle);margin-top:10px}
.hero-grid{display:grid;grid-template-columns:minmax(0,1.62fr) minmax(270px,.85fr);gap:16px;align-items:stretch}
.hero-release{position:relative;isolation:isolate;overflow:hidden;min-height:316px;padding:26px 29px 22px;border:1px solid var(--line);border-top:2px solid var(--accent);background:radial-gradient(ellipse at 93% 30%,var(--ring1),transparent 50%),radial-gradient(ellipse at 30% 110%,var(--ring2),transparent 60%),var(--panel)}
.hero-header{display:flex;justify-content:space-between;align-items:center;gap:10px;color:var(--green);font:10px var(--mono);letter-spacing:1px;position:relative;z-index:1}
.hero-header>span:first-child{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.dot-amber{width:6px;height:6px;border-radius:50%;background:var(--accent)}
.hero-header #hero-channel{color:var(--accent)}
.hero-corner{color:var(--text-subtle)}
.hero-version-label{color:var(--text-subtle);font:10px var(--mono);letter-spacing:1.6px;margin-top:47px;position:relative;z-index:1}
.hero-version{font:600 clamp(64px,7vw,108px)/1.1 var(--mono);letter-spacing:-6px;color:var(--text);margin-top:3px;position:relative;z-index:1;overflow-wrap:anywhere}
.hero-foot{border-top:1px solid var(--line);padding-top:20px;margin-top:21px;display:flex;align-items:flex-end;justify-content:space-between;gap:12px;position:relative;z-index:1}
.hero-foot>div{display:flex;flex-direction:column;gap:5px;min-width:0}
.hero-foot small{font:9px var(--mono);letter-spacing:1px;color:var(--text-subtle)}
.hero-foot strong{font:12px var(--sans);font-weight:550;color:var(--text);white-space:nowrap}
.hero-foot strong.mono{font:12px var(--mono);color:var(--green)}
.hero-link{border:1px solid var(--accent);background:var(--accent);color:var(--accent-ink);font-weight:650;padding:11px 15px;white-space:nowrap;border-radius:3px;transition:filter .15s}
.hero-link:hover{filter:brightness(1.09)}
.hero-link span{margin-left:8px}
.hero-background-rings{pointer-events:none;position:absolute;right:-150px;top:22px;width:420px;height:420px;border:1px solid color-mix(in srgb,var(--green) 10%,transparent);border-radius:50%;z-index:0}
.hero-background-rings::after{content:"";position:absolute;inset:44px;border:1px solid color-mix(in srgb,var(--accent) 10%,transparent);border-radius:50%}
.pipeline-panel{background:var(--panel);border:1px solid var(--line);padding:24px 24px 15px;min-height:316px;display:flex;flex-direction:column}
.pipeline-head{display:flex;justify-content:space-between;align-items:center;gap:10px}
.pipeline-live{font:9px var(--mono);color:var(--green);display:flex;align-items:center;gap:8px}
.pipeline-panel>p{font-size:12px;color:var(--muted);margin:17px 0 20px}
.pipeline-list{display:flex;flex-direction:column}
.pipeline-step{display:flex;align-items:center;gap:14px;min-width:0}
.step-index{width:35px;height:35px;flex-shrink:0;background:var(--panel2);border:1px solid var(--line);display:grid;place-items:center;color:var(--accent);font:11px var(--mono)}
.step-info{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}
.step-info b{font-weight:560;font-size:12px}
.step-info small{color:var(--text-subtle);font:10px var(--mono)}
.step-right{font:10px var(--mono);color:var(--muted)}
.step-right span{color:var(--separator);margin-left:3px}
.step-right.step-success{color:var(--green)}
.step-right.step-success span{font-size:8px;color:var(--green)}
.pipeline-connector{width:1px;height:14px;background:var(--line);margin:3px 0 3px 17px}
.pipeline-footer{display:flex;justify-content:space-between;gap:10px;border-top:1px solid var(--line);padding-top:13px;margin-top:auto;color:var(--text-subtle);font:9px var(--mono)}
.pipeline-footer span{color:var(--muted)}
.stats-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:0;margin-top:16px;border:1px solid var(--line);background:var(--panel)}
.stat{padding:22px 22px 21px;min-width:0}
.stat+.stat{border-left:1px solid var(--line)}
.stat>span{font:10px var(--mono);letter-spacing:.6px;color:var(--muted)}
.stat strong{display:block;font:500 clamp(21px,2.5vw,35px)/1.35 var(--mono);letter-spacing:-1px;margin:9px 0 3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.stat strong.stat-date{font-size:clamp(15px,2vw,24px)}
.stat:first-child strong{color:var(--accent)}
.stat small{color:var(--text-subtle);font-size:10px}
.section-heading{display:flex;align-items:end;justify-content:space-between;gap:15px;margin:42px 0 16px}
.section-heading--second{margin-top:39px}
.section-index{font:10px var(--mono);letter-spacing:1.2px;color:var(--green)}
h2{font-size:22px;letter-spacing:-.75px;font-weight:565;margin-top:7px}
h3{font-size:16px;font-weight:575;letter-spacing:-.3px}
.text-link{background:none;border:0;color:var(--accent);font:11px var(--mono);padding:4px 0;white-space:nowrap}
.text-link:hover{text-decoration:underline}
.text-link span{margin-left:5px}
.build-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px}
.build-card,.skeleton-card{background:var(--panel);border:1px solid var(--line);padding:19px 22px 20px;min-width:0;position:relative}
.build-card:before{content:"";position:absolute;inset:0 auto 0 0;width:2px;background:var(--green);opacity:.75}
.build-card.off:before{background:var(--separator)}
.build-card-top{display:flex;justify-content:space-between;gap:8px;align-items:center;color:var(--muted)}
.build-card-top>span:first-child{font:10px var(--mono);letter-spacing:.7px}
.pill{display:inline-block;border:1px solid var(--pill-line);color:var(--accent);padding:3px 8px;font:9px var(--mono);letter-spacing:.4px;white-space:nowrap}
.pill.live{border-color:var(--pill-live-line);color:var(--green)}
.pill.missing{border-color:var(--line);color:var(--muted)}
.build-card-version{font:550 clamp(22px,2.3vw,31px)/1.4 var(--mono);letter-spacing:-1.5px;margin:15px 0 2px;overflow-wrap:anywhere}
.build-card-sub{color:var(--muted);font:11px var(--mono);margin-bottom:16px}
.build-card-details{border-top:1px solid var(--row-line);padding-top:14px;display:flex;justify-content:space-between;align-items:center;gap:6px}
.build-card-details>div{display:flex;flex-direction:column;gap:4px;min-width:0}
.build-card-details small{font:9px var(--mono);color:var(--text-subtle)}
.build-card-details strong{font:11px var(--mono);color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.build-card-details .commit{color:var(--green)}
.skeleton-card{grid-column:1/-1;color:var(--muted)}
.card{border:1px solid var(--line);background:var(--panel);min-width:0}
.recent-card{overflow:hidden}
.table-scroller{overflow-x:auto;scrollbar-color:var(--field-line) transparent;scrollbar-width:thin}
.data-table{border-collapse:collapse;width:100%;min-width:680px;text-align:left}
.data-table thead{background:var(--panel2)}
.data-table th{font:10px var(--mono);letter-spacing:.7px;color:var(--muted);padding:16px 19px;white-space:nowrap;border-bottom:1px solid var(--line)}
.data-table td{padding:15px 19px;border-bottom:1px solid var(--row-line);font-size:12px;color:var(--text);white-space:nowrap}
.data-table tbody tr:last-child td{border-bottom:0}
.data-table tbody tr:hover{background:color-mix(in srgb,var(--nav-hover-bg) 70%,transparent)}
.artifact-name{font-weight:570;display:block;max-width:270px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.artifact-arch{display:block;margin-top:3px;color:var(--text-subtle);font:10px var(--mono)}
.version-code{font:12px var(--mono);color:var(--green)}
.date-cell{font:11px var(--mono);color:var(--text)}
.date-cell small{display:block;color:var(--text-subtle);margin-top:2px}
.channel-tag{display:inline-flex;align-items:center;gap:7px;font:10px var(--mono);color:var(--muted)}
.channel-tag:before{content:"";width:5px;height:5px;background:var(--accent);border-radius:50%}
.channel-tag.pkg:before{background:var(--green)}
.format-label{display:inline-block;font:9px var(--mono);color:var(--accent);background:color-mix(in srgb,var(--accent) 7%,transparent);border:1px solid color-mix(in srgb,var(--accent) 28%,transparent);padding:3px 6px;letter-spacing:.5px;margin-right:7px}
.format-label.pkg{color:var(--green);border-color:var(--pill-live-line);background:color-mix(in srgb,var(--green) 7%,transparent)}
.download-link{font:11px var(--mono);color:var(--accent);border:1px solid var(--line);padding:7px 10px;display:inline-flex;gap:8px;align-items:center;white-space:nowrap}
.download-link:hover{border-color:var(--accent);background:var(--nav-hover-bg)}
.download-link span{font-size:13px}
.empty-table{color:var(--muted)!important;text-align:center;font-size:13px!important;padding:34px!important}
.compact-head{margin-bottom:34px}
.compact-head .lead{max-width:710px}
.count-pill{color:var(--green);border:1px solid var(--pill-live-line);font:10px var(--mono);padding:9px 14px;white-space:nowrap}
.filter-panel{border:1px solid var(--line);background:var(--panel);padding:15px;display:grid;grid-template-columns:minmax(220px,2.3fr) repeat(3,minmax(120px,1fr));gap:11px;align-items:end}
.search-field{position:relative;display:flex;align-items:center;height:41px;background:var(--field-bg);border:1px solid var(--field-line)}
.search-field svg{margin:0 12px;color:var(--muted);flex-shrink:0}
.search-field input{background:none;border:0;outline:none;width:100%;height:100%;color:var(--text);padding:0 11px 0 0;min-width:0;font-size:12px}
.search-field input::placeholder{color:var(--placeholder)}
.search-field:focus-within{border-color:var(--accent)}
.filter-input{display:flex;flex-direction:column;gap:8px;font:9px var(--mono);color:var(--muted);letter-spacing:.7px}
.filter-input select{appearance:none;width:100%;height:41px;padding:0 31px 0 11px;border:1px solid var(--field-line);background:var(--field-bg);color:var(--text);border-radius:0;font:12px var(--sans);background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),linear-gradient(135deg,var(--muted) 50%,transparent 50%);background-position:calc(100% - 16px) 18px,calc(100% - 11px) 18px;background-size:5px 5px;background-repeat:no-repeat}
.filter-input select option{background:var(--panel);color:var(--text)}
.result-topline{display:flex;gap:12px;justify-content:space-between;margin:19px 0 13px;color:var(--muted);font:10px var(--mono)}
.result-topline span:first-child{color:var(--green)}
.catalog-bottom{display:flex;gap:15px;align-items:center;justify-content:space-between;padding:14px 19px;border-top:1px solid var(--line);color:var(--text-subtle);font:10px var(--mono)}
.show-more{font-size:11px;padding:8px 13px}
.install-tabs{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-bottom:16px}
.install-tab{display:flex;flex-direction:column;align-items:flex-start;gap:6px;padding:16px 18px;border:1px solid var(--line);border-bottom:2px solid var(--line);background:var(--panel);color:var(--muted);font-size:13px;font-weight:600;text-align:left;transition:border-color .14s,color .14s}
.install-tab small{font:10px var(--mono);font-weight:400;color:var(--text-subtle)}
.install-tab:hover{border-color:var(--border-hover);color:var(--text)}
.install-tab.active{color:var(--text);border-bottom-color:var(--accent);background:var(--panel2)}
.install-tab.active small{color:var(--green)}
.install-card{padding:27px}
.install-card-head{display:flex;justify-content:space-between;align-items:flex-start;gap:15px;margin-bottom:22px}
.install-card-head h2{font-size:24px;margin:9px 0}
.install-card-head p{color:var(--muted);font-size:12px}
.code-window{border:1px solid var(--line);background:var(--code-bg);overflow:hidden}
.code-topbar{display:flex;justify-content:space-between;align-items:center;padding:10px 17px;border-bottom:1px solid var(--line);color:var(--text-subtle);font:11px var(--mono)}
.code-topbar>span{display:flex;align-items:center;gap:7px}
.code-topbar .tiny-dot:nth-child(1){background:var(--accent)}
.code-topbar .tiny-dot:nth-child(2){background:var(--notice-text)}
.code-topbar .tiny-dot:nth-child(3){background:var(--green)}
.code-topbar b{font-weight:400;margin-left:13px}
.copy-code{color:var(--accent);background:transparent;border:0;font:11px var(--mono);padding:4px 2px}
.copy-code:hover{text-decoration:underline}
.code-window pre{color:var(--text);font:12px/1.9 var(--mono);overflow:auto;margin:0;padding:21px 24px;min-height:170px;tab-size:2;white-space:pre}
.install-foot{display:flex;justify-content:space-between;gap:15px;margin-top:17px;color:var(--muted);font-size:11px}
.install-extra-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px}
.hint-card{padding:24px 26px}
.hint-icon{display:grid;place-items:center;width:34px;height:34px;border:1px solid var(--pill-live-line);color:var(--green);font:11px var(--mono);margin-bottom:18px}
.hint-card h3{margin-bottom:10px}
.hint-card p{color:var(--muted);font-size:12px;line-height:1.8;min-height:67px}
.hint-card code{font:11px var(--mono);color:var(--green)}
.inline-command{display:flex;align-items:center;justify-content:space-between;gap:8px;border:1px solid var(--line);background:var(--code-bg);padding:9px 12px;margin-top:15px}
.inline-command code{font-size:10px;overflow:auto;white-space:nowrap}
.inline-command button{font-size:10px;padding:5px 8px}
.endpoints-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px}
.endpoint-card{border:1px solid var(--line);background:var(--panel);padding:23px 22px}
.endpoint-card-top{display:flex;align-items:center;justify-content:space-between;gap:9px;margin-bottom:19px}
.endpoint-card h2{font-size:20px;letter-spacing:-.4px;margin:0}
.endpoint-card>a{color:var(--green);font:11px var(--mono);word-break:break-all;display:block;min-height:54px;line-height:1.7}
.endpoint-card>a:hover{text-decoration:underline}
.endpoint-card ul{list-style:none;padding:15px 0 0;border-top:1px solid var(--line);margin:12px 0 0}
.endpoint-card li{margin:12px 0 0;font-size:11px}
.endpoint-card li a{display:flex;justify-content:space-between;gap:7px;align-items:center}
.endpoint-card li a span:first-child{color:var(--muted)}
.endpoint-card li a span:last-child{color:var(--accent)}
.endpoint-card li a:hover span:first-child{color:var(--text)}
.metadata-card{padding:3px 25px}
.meta-table{display:grid;grid-template-columns:repeat(3,minmax(0,1fr))}
.meta-block{padding:19px 0}
.meta-block+.meta-block{padding-left:23px;border-left:1px solid var(--line)}
.meta-block h3{font-size:13px;margin-bottom:12px}
.meta-row{display:flex;flex-direction:column;gap:4px;margin-top:11px}
.meta-row small{font:9px var(--mono);letter-spacing:.6px;color:var(--text-subtle)}
.meta-row strong{font:11px var(--mono);font-weight:400;color:var(--text);overflow-wrap:anywhere}
.meta-row strong.latest-value{color:var(--green)}
.page-footer{border-top:1px solid var(--line);color:var(--text-subtle);font:10px var(--mono);display:flex;justify-content:space-between;gap:14px;margin-top:54px;padding:19px 0}
.page-footer b{color:var(--accent);font-weight:400}
.error-banner{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 0 21px;padding:14px 16px;color:var(--notice-text);border:1px solid var(--notice-line);background:var(--notice-bg);font-size:12px}
.error-banner button{color:var(--accent);background:none;border:0;font-weight:600;white-space:nowrap}
.clipboard-helper{position:fixed;opacity:0;pointer-events:none}
.toast{position:fixed;right:24px;bottom:25px;background:var(--brand-secondary);color:var(--sidebar-bg);border:1px solid var(--pill-live-line);box-shadow:0 10px 35px var(--shadow);padding:13px 20px;font-size:12px;font-weight:600;z-index:100;max-width:min(400px,calc(100vw - 32px))}
@media(max-width:1390px){
.shell{grid-template-columns:226px minmax(0,1fr)}
.sidebar{padding-left:13px;padding-right:13px}
.topbar{padding-left:30px;padding-right:30px}
.main-content{padding-left:30px;padding-right:30px}
.hero-grid{grid-template-columns:minmax(0,1.35fr) minmax(245px,.85fr)}
.hero-foot{flex-wrap:wrap}
.hero-link{margin-left:auto}
.stat{padding:20px 15px}
.filter-panel{grid-template-columns:minmax(200px,1.8fr) repeat(3,minmax(110px,1fr))}
}
@media(max-width:1070px){
.hero-grid{grid-template-columns:minmax(0,1fr)}
.hero-release{min-height:295px}
.pipeline-panel{min-height:auto}
.pipeline-list{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}
.pipeline-step{gap:10px}
.pipeline-connector{display:none}
.step-right{display:none}
.pipeline-footer{margin-top:17px}
.build-grid{gap:10px}
.build-card{padding:16px}
.build-card-details{flex-wrap:wrap}
.build-card-details>div{width:100%}
.filter-panel{grid-template-columns:repeat(3,minmax(0,1fr))}
.search-field{grid-column:1/-1}
.endpoints-grid{grid-template-columns:1fr 1fr}
}
@media(max-width:780px){
.shell{display:block}
.sidebar{position:relative;width:auto;height:auto;min-height:auto;padding:13px 21px 10px;border-right:0;border-bottom:1px solid var(--line)}
.brand{font-size:17px}
.brand-mark{width:38px;height:38px;flex-basis:38px;font-size:23px}
.brand-mark span{font-size:19px}
.edition,.sidebar-label,.side-channels,.sidebar-bottom{display:none}
.main-navigation{flex-direction:row;overflow-x:auto;gap:3px;margin-top:13px;padding-bottom:2px}
.nav{flex:1;min-width:max-content;gap:8px;padding:10px 12px;font-size:12px;min-height:auto}
.nav-arrow{display:none}
.nav b{font-size:11px}
.topbar{position:relative;padding:0 20px;height:57px}
.main-content{padding:35px 20px 22px}
.crumb{font-size:9px;gap:7px;letter-spacing:.5px}
.page-head{align-items:flex-start}
.page-head-side{display:none}
.page-head h1{font-size:39px}
.live-sync{margin-right:1px;font-size:9px}
.stats-grid{grid-template-columns:1fr 1fr}
.stat:nth-child(3){border-left:0;border-top:1px solid var(--line)}
.stat:nth-child(4){border-top:1px solid var(--line)}
.build-grid{grid-template-columns:1fr 1fr}
.install-tabs{grid-template-columns:1fr 1fr}
.install-extra-grid{grid-template-columns:1fr}
.hint-card p{min-height:auto}
.meta-table{grid-template-columns:1fr}
.meta-block+.meta-block{border-left:0;border-top:1px solid var(--line);padding-left:0}
.endpoints-grid{grid-template-columns:1fr}
}
@media(max-width:540px){
.sidebar{padding:12px 13px 9px}
.main-navigation{gap:2px}
.nav{gap:5px;padding:9px 7px;flex:1;justify-content:center}
.nav>span:first-child{font-size:9px}
.nav b{font-size:10px}
.topbar{padding:0 15px}
.top-actions{gap:5px}
.live-sync{display:none}
.theme-switch{padding:0 9px}
.main-content{padding:28px 15px 18px}
.page-head h1{font-size:35px;letter-spacing:-1.5px}
.eyebrow{font-size:8px;letter-spacing:1.1px;gap:6px}
.eyebrow-line{width:13px}
.lead{font-size:12px}
.hero-release{padding:19px 17px 20px;min-height:270px}
.hero-version-label{margin-top:39px;font-size:9px}
.hero-version{font-size:clamp(46px,11vw,72px);letter-spacing:-3px}
.hero-corner{display:none}
.hero-foot{gap:10px}
.hero-foot>div{min-width:calc(50% - 10px)}
.hero-foot strong{font-size:11px}
.hero-link{margin-top:6px;margin-left:0;width:100%}
.pipeline-panel{padding:19px}
.pipeline-list{display:flex;flex-direction:column;gap:13px}
.step-right{display:block}
.stats-grid .stat{padding:17px 14px}
.stat>span{font-size:9px}
.stat strong{font-size:23px}
.stat strong.stat-date{font-size:16px}
.stat small{font-size:9px}
.build-grid{grid-template-columns:1fr}
.build-card-details{flex-wrap:nowrap}
.build-card-details>div{width:auto}
.section-heading h2{font-size:21px}
.filter-panel{grid-template-columns:1fr 1fr}
.search-field{grid-column:1/-1}
.filter-input:last-child{grid-column:1/-1}
.result-topline{flex-direction:column}
.install-tabs{gap:7px}
.install-tab{padding:11px 9px;font-size:12px}
.install-tab small{font-size:9px}
.install-card{padding:17px}
.install-card-head{flex-wrap:wrap}
.code-window pre{font-size:11px;padding:15px}
.install-foot{flex-direction:column;gap:8px}
.hint-card{padding:19px}
.page-footer{flex-direction:column;gap:7px}
}
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*,*::before,*::after{animation-duration:.01ms!important;transition-duration:.01ms!important}}