added qr code

This commit is contained in:
2026-04-02 16:10:44 -05:00
parent 13f38f6254
commit d391905e92
3 changed files with 59 additions and 135 deletions

View File

@@ -3,7 +3,9 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import base64
import hashlib import hashlib
import io
import json import json
import os import os
import re import re
@@ -31,6 +33,7 @@ UPDATE_STATUS = "/var/log/sovran-hub-update.status"
UPDATE_UNIT = "sovran-hub-update.service" UPDATE_UNIT = "sovran-hub-update.service"
INTERNAL_IP_FILE = "/var/lib/secrets/internal-ip" INTERNAL_IP_FILE = "/var/lib/secrets/internal-ip"
ZEUS_CONNECT_FILE = "/var/lib/secrets/zeus-connect-url"
REBOOT_COMMAND = ["reboot"] REBOOT_COMMAND = ["reboot"]
@@ -185,6 +188,24 @@ def _get_external_ip() -> str:
return "unavailable" return "unavailable"
# ── QR code helper ────────────────────────────────────────────────
def _generate_qr_base64(data: str) -> str | None:
"""Generate a QR code PNG and return it as a base64-encoded data URI.
Uses qrencode CLI (available on the system via credentials-pdf.nix)."""
try:
result = subprocess.run(
["qrencode", "-o", "-", "-t", "PNG", "-s", "6", "-m", "2", "-l", "H", data],
capture_output=True, timeout=10,
)
if result.returncode == 0 and result.stdout:
b64 = base64.b64encode(result.stdout).decode("ascii")
return f"data:image/png;base64,{b64}"
except Exception:
pass
return None
# ── Update helpers (file-based, no systemctl) ──────────────────── # ── Update helpers (file-based, no systemctl) ────────────────────
def _read_update_status() -> str: def _read_update_status() -> str:
@@ -224,16 +245,22 @@ def _read_log(offset: int = 0) -> tuple[str, int]:
# ── Credentials helpers ────────────────────────────────────────── # ── Credentials helpers ──────────────────────────────────────────
def _resolve_credential(cred: dict) -> dict | None: def _resolve_credential(cred: dict) -> dict | None:
"""Resolve a single credential entry to {label, value}.""" """Resolve a single credential entry to {label, value, ...}."""
label = cred.get("label", "") label = cred.get("label", "")
prefix = cred.get("prefix", "") prefix = cred.get("prefix", "")
suffix = cred.get("suffix", "") suffix = cred.get("suffix", "")
extract = cred.get("extract", "") extract = cred.get("extract", "")
multiline = cred.get("multiline", False) multiline = cred.get("multiline", False)
qrcode = cred.get("qrcode", False)
# Static value # Static value
if "value" in cred: if "value" in cred:
return {"label": label, "value": prefix + cred["value"] + suffix, "multiline": multiline} result = {"label": label, "value": prefix + cred["value"] + suffix, "multiline": multiline}
if qrcode:
qr_data = _generate_qr_base64(result["value"])
if qr_data:
result["qrcode"] = qr_data
return result
# File-based value # File-based value
filepath = cred.get("file", "") filepath = cred.get("file", "")
@@ -255,7 +282,14 @@ def _resolve_credential(cred: dict) -> dict | None:
return None return None
value = prefix + raw + suffix value = prefix + raw + suffix
return {"label": label, "value": value, "multiline": multiline} result = {"label": label, "value": value, "multiline": multiline}
if qrcode:
qr_data = _generate_qr_base64(value)
if qr_data:
result["qrcode"] = qr_data
return result
# ── Routes ─────────────────────────────────────────────────────── # ── Routes ───────────────────────────────────────────────────────

View File

@@ -269,7 +269,7 @@ async function refreshServices() {
} }
} }
// ── Network IPs ────────────────────────────────────────────────── // ── Network IPs ──────────────────────────────────<EFBFBD><EFBFBD>────────────────
async function loadNetwork() { async function loadNetwork() {
try { try {
@@ -319,9 +319,22 @@ async function openCredsModal(unit, name) {
for (const cred of data.credentials) { for (const cred of data.credentials) {
const id = "cred-" + Math.random().toString(36).substring(2, 8); const id = "cred-" + Math.random().toString(36).substring(2, 8);
const displayValue = linkify(cred.value); const displayValue = linkify(cred.value);
// QR code block (if present)
let qrBlock = "";
if (cred.qrcode) {
qrBlock = `
<div class="creds-qr-wrap">
<img class="creds-qr-img" src="${cred.qrcode}" alt="QR Code for ${escHtml(cred.label)}">
<div class="creds-qr-hint">Scan with Zeus app on your phone</div>
</div>
`;
}
html += ` html += `
<div class="creds-row"> <div class="creds-row">
<div class="creds-label">${escHtml(cred.label)}</div> <div class="creds-label">${escHtml(cred.label)}</div>
${qrBlock}
<div class="creds-value-wrap"> <div class="creds-value-wrap">
<div class="creds-value" id="${id}">${displayValue}</div> <div class="creds-value" id="${id}">${displayValue}</div>
<button class="creds-copy-btn" data-target="${id}">Copy</button> <button class="creds-copy-btn" data-target="${id}">Copy</button>

View File

@@ -1,6 +1,6 @@
/* Sovran_SystemsOS Hub — Web UI Stylesheet /* Sovran_SystemsOS Hub — Web UI Stylesheet
Dark theme matching the Adwaita dark aesthetic Dark theme matching the Adwaita dark aesthetic
v4 — credentials info modal */ v5 QR code support in credentials modal */
*, *::before, *::after { *, *::before, *::after {
box-sizing: border-box; box-sizing: border-box;
@@ -530,7 +530,7 @@ button.btn-reboot:hover:not(:disabled) {
background-color: #5a5c72; background-color: #5a5c72;
} }
/* ── Credentials info modal ─────────────────────────────────────── */ /* ── Credentials info modal ──────────────────────────────────────<EFBFBD><EFBFBD>─ */
.creds-dialog { .creds-dialog {
background-color: var(--surface-color); background-color: var(--surface-color);
@@ -670,137 +670,14 @@ button.btn-reboot:hover:not(:disabled) {
color: #defce6; color: #defce6;
} }
/* ── Reboot overlay ─────────────────────────────────────────────── */ /* ── QR code in credentials modal ────────────────────────────────── */
.reboot-overlay { .creds-qr-wrap {
display: none;
position: fixed;
inset: 0;
background-color: rgba(15, 15, 25, 0.92);
z-index: 999;
align-items: center;
justify-content: center;
}
.reboot-overlay.visible {
display: flex;
}
.reboot-card {
background-color: var(--surface-color);
border: 1px solid var(--border-color);
border-radius: 20px;
padding: 48px 56px;
text-align: center;
max-width: 480px;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.8);
animation: reboot-fade-in 0.4s ease-out;
}
@keyframes reboot-fade-in {
from { opacity: 0; transform: scale(0.92) translateY(12px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.reboot-icon {
font-size: 3rem;
color: var(--accent-color);
margin-bottom: 16px;
animation: reboot-spin 2s linear infinite;
display: inline-block;
}
@keyframes reboot-spin {
to { transform: rotate(360deg); }
}
.reboot-title {
font-size: 1.35rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 12px;
}
.reboot-message {
font-size: 0.92rem;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 24px;
}
.reboot-dots {
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; padding: 20px 0;
gap: 8px; margin-bottom: 10px;
margin-bottom: 16px;
} }
.reboot-dot { .
width: 10px;
height: 10px;
border-radius: 50%;
background-color: var(--accent-color);
animation: reboot-bounce 1.4s ease-in-out infinite;
}
.reboot-dot:nth-child(2) { animation-delay: 0.2s; }
.reboot-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes reboot-bounce {
0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
40% { opacity: 1; transform: scale(1.2); }
}
.reboot-submessage {
font-size: 0.82rem;
color: var(--text-dim);
font-style: italic;
}
/* ── Empty state ────────────────────────────────────────────────── */
.empty-state {
text-align: center;
padding: 64px 24px;
color: var(--text-dim);
}
.empty-state p {
font-size: 1rem;
margin-bottom: 8px;
}
/* ── Responsive ─────────────────────────────────────────────────── */
@media (max-width: 600px) {
.header-bar {
padding: 10px 14px;
gap: 10px;
}
.header-bar .title {
font-size: 0.95rem;
}
.ip-bar {
gap: 16px;
flex-wrap: wrap;
padding: 8px 14px;
}
.main-content {
padding: 16px 12px 40px;
}
.tiles-grid {
justify-content: center;
}
.service-tile {
width: 160px;
min-height: 200px;
}
.reboot-card {
padding: 36px 28px;
margin: 0 16px;
}
.creds-dialog {
margin: 0 12px;
}
}