fancy dash
This commit is contained in:
1 parent
3b317a4f0c
commit
ccccdc2031
16 files changed
+1530
-34
No files matched your search
@@ -0,0 +1,2 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -0,0 +1,13 @@
|
||||
FROM python:3.12-alpine
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
REPOSITORY_ROOT=/srv/repository \
|
||||
DASHBOARD_PORT=8000
|
||||
|
||||
WORKDIR /app
|
||||
COPY --chown=10001:10001 app.py /app/app.py
|
||||
COPY --chown=10001:10001 web/ /app/web/
|
||||
USER 10001:10001
|
||||
EXPOSE 8000
|
||||
CMD ["python", "/app/app.py"]
|
||||
@@ -0,0 +1,314 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read-only, live RustProxy package registry UI.
|
||||
|
||||
The existing Nginx continues to serve APT, DNF and pkg artifacts unchanged.
|
||||
This service only generates the dashboard and a public JSON catalog.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from http import HTTPStatus
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote, urlsplit
|
||||
|
||||
LOG = logging.getLogger("rustproxy.repository")
|
||||
REPOSITORY = Path(os.environ.get("REPOSITORY_ROOT", "/srv/repository"))
|
||||
WEB = Path(__file__).resolve().parent / "web"
|
||||
PORT = int(os.environ.get("DASHBOARD_PORT", "8000"))
|
||||
CACHE_SECONDS = max(1, int(os.environ.get("DASHBOARD_CACHE_SECONDS", "15")))
|
||||
MAX_PACKAGES = 5000
|
||||
|
||||
SITES = ("linux", "opnsense", "freebsd")
|
||||
META_PATHS = {
|
||||
"linux": Path("LINUX-BUILD-INFO.txt"),
|
||||
"opnsense": Path("opnsense/BUILD-INFO.txt"),
|
||||
"freebsd": Path("freebsd/BUILD-INFO.txt"),
|
||||
}
|
||||
REPO_URLS = {
|
||||
"linux": os.environ.get("REPOSITORY_URL", "http://localhost:18765").rstrip("/"),
|
||||
"opnsense": os.environ.get("OPNSENSE_REPOSITORY_URL", "http://localhost:18766").rstrip("/"),
|
||||
}
|
||||
REPO_URLS["freebsd"] = REPO_URLS["opnsense"] + "/freebsd"
|
||||
|
||||
DEB_FILENAME = re.compile(r"^(?P<name>.+?)_(?P<version>[^_]+)_(?P<arch>[^_]+)\.deb$")
|
||||
RPM_FILENAME = re.compile(
|
||||
r"^(?P<name>.+)-(?P<version>\d[^-]*)-(?P<release>[^/]+)\."
|
||||
r"(?P<arch>x86_64|amd64|aarch64|noarch|arm64)\.rpm$"
|
||||
)
|
||||
PKG_FILENAME = re.compile(r"^(?P<name>.+)-(?P<version>\d[^/]*)\.pkg$")
|
||||
|
||||
|
||||
def iso_time(stamp: float) -> str:
|
||||
return datetime.fromtimestamp(stamp, tz=timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
|
||||
|
||||
|
||||
def parse_metadata(path: Path) -> dict[str, str]:
|
||||
try:
|
||||
# Metadata is emitted by our builder, not submitted through the dashboard.
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except (FileNotFoundError, PermissionError, OSError):
|
||||
return {}
|
||||
info: dict[str, str] = {}
|
||||
for line in text.splitlines():
|
||||
key, separator, value = line.partition("=")
|
||||
if separator and re.fullmatch(r"[a-z][a-z0-9_]*", key):
|
||||
info[key] = value.strip()[:1024]
|
||||
return info
|
||||
|
||||
|
||||
def parse_filename(filename: str, kind: str) -> tuple[str, str, str] | None:
|
||||
if kind == "deb":
|
||||
m = DEB_FILENAME.fullmatch(filename)
|
||||
return (m["name"], m["version"], m["arch"]) if m else None
|
||||
if kind == "rpm":
|
||||
m = RPM_FILENAME.fullmatch(filename)
|
||||
if m:
|
||||
return (m["name"], f"{m['version']}-{m['release']}", m["arch"])
|
||||
return None
|
||||
if filename in ("packagesite.pkg", "data.pkg") or filename.endswith("-latest.pkg"):
|
||||
return None
|
||||
m = PKG_FILENAME.fullmatch(filename)
|
||||
return (m["name"], m["version"], "amd64") if m else None
|
||||
|
||||
|
||||
def publication_info(current: Path, channel: str) -> dict[str, object]:
|
||||
location = current / META_PATHS[channel]
|
||||
raw = parse_metadata(location)
|
||||
info: dict[str, object] = {
|
||||
"channel": channel,
|
||||
"published": bool(raw),
|
||||
"app_version": raw.get("app_version", ""),
|
||||
"package_version": raw.get("package_version", ""),
|
||||
"package_release": raw.get("package_release", ""),
|
||||
"source_commit": raw.get("source_commit", ""),
|
||||
"source_ref": raw.get("source_ref", ""),
|
||||
"source_url": raw.get("source_url", ""),
|
||||
"build_reason": raw.get("build_reason", ""),
|
||||
"packaging_revision": raw.get("packaging_revision", ""),
|
||||
"opnsense_series": raw.get("opnsense_series", ""),
|
||||
"freebsd_abi": raw.get("freebsd_abi", ""),
|
||||
"built_at": None,
|
||||
"date_source": "unknown",
|
||||
}
|
||||
if channel == "linux" and info["app_version"]:
|
||||
info["package_version"] = f"{info['app_version']}-{info['package_release']}" if info["package_release"] else info["app_version"]
|
||||
|
||||
stamp = raw.get("built_at", "")
|
||||
if stamp:
|
||||
try:
|
||||
candidate = datetime.fromisoformat(stamp.replace("Z", "+00:00"))
|
||||
if candidate.tzinfo is not None:
|
||||
info["built_at"] = candidate.astimezone(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
|
||||
info["date_source"] = "build"
|
||||
except ValueError:
|
||||
pass
|
||||
if info["built_at"] is None and raw:
|
||||
# Backward compatibility with builds made before 'built_at' was saved.
|
||||
try:
|
||||
info["built_at"] = iso_time(location.stat().st_mtime)
|
||||
info["date_source"] = "file"
|
||||
except OSError:
|
||||
pass
|
||||
return info
|
||||
|
||||
|
||||
def get_current(repository: Path) -> Path | None:
|
||||
try:
|
||||
# Never follow a manipulated symlink outside the repository tree.
|
||||
root = repository.resolve(strict=True)
|
||||
current = (root / "current").resolve(strict=True)
|
||||
if current.is_dir() and current.is_relative_to(root):
|
||||
return current
|
||||
except (OSError, RuntimeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def package_entry(path: Path, current: Path, site: str, kind: str, channel: str,
|
||||
builds: dict[str, dict[str, object]]) -> dict[str, object] | None:
|
||||
parsed = parse_filename(path.name, kind)
|
||||
if not parsed or not path.is_file():
|
||||
return None
|
||||
name, version, arch = parsed
|
||||
try:
|
||||
stat = path.stat()
|
||||
except OSError:
|
||||
return None
|
||||
relative = path.relative_to(current)
|
||||
if site == "linux":
|
||||
public_path = relative
|
||||
else:
|
||||
public_path = relative.relative_to(site)
|
||||
# Each URL segment is encoded independently; separators remain separators.
|
||||
encoded_path = "/".join(quote(p, safe="") for p in public_path.parts)
|
||||
base = REPO_URLS[site]
|
||||
built_at = iso_time(stat.st_mtime)
|
||||
source = "file"
|
||||
build = builds[channel]
|
||||
if build.get("date_source") == "build" and version == build.get("package_version"):
|
||||
built_at = str(build["built_at"])
|
||||
source = "build"
|
||||
return {
|
||||
"name": name,
|
||||
"version": version,
|
||||
"architecture": arch,
|
||||
"format": kind.upper() if kind != "pkg" else "PKG",
|
||||
"channel": channel,
|
||||
"size_bytes": stat.st_size,
|
||||
"built_at": built_at,
|
||||
"date_source": source,
|
||||
"download_url": f"{base}/{encoded_path}",
|
||||
"relative_path": relative.as_posix(),
|
||||
}
|
||||
|
||||
|
||||
def generate_catalog(repository: Path = REPOSITORY) -> dict[str, object]:
|
||||
current = get_current(repository)
|
||||
builds: dict[str, dict[str, object]] = {}
|
||||
packages: list[dict[str, object]] = []
|
||||
for channel in SITES:
|
||||
builds[channel] = publication_info(current, channel) if current else {
|
||||
"channel": channel, "published": False, "app_version": "", "package_version": "",
|
||||
"package_release": "", "source_commit": "", "source_ref": "",
|
||||
"source_url": "", "build_reason": "", "packaging_revision": "",
|
||||
"opnsense_series": "", "freebsd_abi": "", "built_at": None, "date_source": "unknown",
|
||||
}
|
||||
|
||||
if current is not None:
|
||||
folders = (
|
||||
("linux", "deb", "linux", current / "apt" / "pool", "*.deb"),
|
||||
("linux", "rpm", "linux", current / "rpm" / "x86_64", "*.rpm"),
|
||||
("opnsense", "pkg", "opnsense", current / "opnsense", "*.pkg"),
|
||||
("freebsd", "pkg", "freebsd", current / "freebsd", "*.pkg"),
|
||||
)
|
||||
for site, kind, channel, directory, pattern in folders:
|
||||
if not directory.is_dir():
|
||||
continue
|
||||
try:
|
||||
for file in directory.rglob(pattern):
|
||||
result = package_entry(file, current, site, kind, channel, builds)
|
||||
if result:
|
||||
packages.append(result)
|
||||
except OSError as e:
|
||||
LOG.warning("Cannot scan %s: %s", directory, e)
|
||||
|
||||
packages.sort(key=lambda item: (str(item["built_at"]), str(item["version"])), reverse=True)
|
||||
total_packages = len(packages)
|
||||
packages = packages[:MAX_PACKAGES]
|
||||
channel_counts = {site: sum(p["channel"] == site for p in packages) for site in SITES}
|
||||
build_times = [str(b["built_at"]) for b in builds.values() if b["built_at"]]
|
||||
return {
|
||||
"generated_at": iso_time(time.time()),
|
||||
"ready": any(b["published"] for b in builds.values()) or bool(packages),
|
||||
"repository_urls": REPO_URLS,
|
||||
"apt_distribution": os.environ.get("REPO_DISTRIBUTION", "stable"),
|
||||
"apt_component": os.environ.get("REPO_COMPONENT", "main"),
|
||||
"builds": builds,
|
||||
"packages": packages,
|
||||
"summary": {
|
||||
"packages_count": total_packages,
|
||||
"packages_visible": len(packages),
|
||||
"channel_counts": channel_counts,
|
||||
"channels_published": sum(bool(b["published"]) for b in builds.values()),
|
||||
"latest_build_at": max(build_times, default=None),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class CatalogCache:
|
||||
def __init__(self, repository: Path = REPOSITORY, ttl: int = CACHE_SECONDS):
|
||||
self.repository = repository
|
||||
self.ttl = ttl
|
||||
self._lock = threading.Lock()
|
||||
self._expires = 0.0
|
||||
self._snapshot: dict[str, object] | None = None
|
||||
|
||||
def get(self) -> dict[str, object]:
|
||||
with self._lock:
|
||||
if self._snapshot is None or time.monotonic() >= self._expires:
|
||||
self._snapshot = generate_catalog(self.repository)
|
||||
self._expires = time.monotonic() + self.ttl
|
||||
return self._snapshot
|
||||
|
||||
|
||||
CACHE = CatalogCache()
|
||||
|
||||
|
||||
class DashboardHandler(BaseHTTPRequestHandler):
|
||||
server_version = "RustProxyRepository/1.0"
|
||||
sys_version = ""
|
||||
|
||||
def do_GET(self) -> None:
|
||||
path = urlsplit(self.path).path
|
||||
if path == "/healthz":
|
||||
self.send_bytes(b"ok\n", "text/plain; charset=utf-8", cache="no-store")
|
||||
elif path == "/api/v1/packages":
|
||||
try:
|
||||
payload = json.dumps(CACHE.get(), ensure_ascii=False, separators=(",", ":"))
|
||||
except Exception:
|
||||
LOG.exception("Failed to build package catalog")
|
||||
self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||
return
|
||||
self.send_bytes(payload.encode("utf-8"), "application/json; charset=utf-8", cache="no-store")
|
||||
elif path in ("/", "/index.html", "/freebsd/", "/freebsd/index.html"):
|
||||
site = self.headers.get("X-Repo-Site", "linux")
|
||||
if path.startswith("/freebsd/"):
|
||||
site = "freebsd"
|
||||
if site not in SITES:
|
||||
site = "linux"
|
||||
page = (WEB / "index.html").read_text(encoding="utf-8")
|
||||
page = page.replace("__REPOSITORY_SITE__", site)
|
||||
self.send_bytes(page.encode("utf-8"), "text/html; charset=utf-8", cache="no-store", html=True)
|
||||
elif path in ("/ui/style.css", "/ui/app.js", "/ui/favicon.svg"):
|
||||
name = path.rsplit("/", 1)[-1]
|
||||
mime = {"style.css": "text/css", "app.js": "application/javascript", "favicon.svg": "image/svg+xml"}[name]
|
||||
self.send_bytes((WEB / name).read_bytes(), f"{mime}; charset=utf-8", cache="public, max-age=600")
|
||||
else:
|
||||
self.send_error(HTTPStatus.NOT_FOUND, "Unknown dashboard route")
|
||||
|
||||
def do_HEAD(self) -> None:
|
||||
# Nginx allows HEAD and GET; suppress response content for HEAD.
|
||||
self.do_GET()
|
||||
|
||||
def send_bytes(self, payload: bytes, mime: str, *, cache: str, html: bool = False) -> None:
|
||||
self.send_response(HTTPStatus.OK)
|
||||
self.send_header("Content-Type", mime)
|
||||
self.send_header("Content-Length", str(len(payload)))
|
||||
self.send_header("Cache-Control", cache)
|
||||
self.send_header("X-Content-Type-Options", "nosniff")
|
||||
self.send_header("X-Frame-Options", "DENY")
|
||||
if html:
|
||||
self.send_header("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self'; "
|
||||
"connect-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'none'; "
|
||||
"frame-ancestors 'none'; form-action 'none'")
|
||||
self.end_headers()
|
||||
if self.command != "HEAD":
|
||||
self.wfile.write(payload)
|
||||
|
||||
def log_message(self, fmt: str, *args: object) -> None:
|
||||
LOG.info("%s - %s", self.address_string(), fmt % args)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
||||
httpd = ThreadingHTTPServer(("0.0.0.0", PORT), DashboardHandler)
|
||||
httpd.daemon_threads = True
|
||||
LOG.info("Dashboard ready on port %s (repository: %s)", PORT, REPOSITORY)
|
||||
try:
|
||||
httpd.serve_forever(poll_interval=0.5)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
httpd.server_close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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) => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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();
|
||||
})();
|
||||
@@ -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 |
@@ -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>
|
||||
@@ -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}}
|
||||
Reference in new issue
Block a user