added qr code
This commit is contained in:
@@ -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 ───────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user