@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
< script >
import { createEventDispatcher , onMount } from "svelte";
import QRCode from "qrcode";
import { getWireGuardConfig , regenerateWireGuard , getBunkerURL , fetchWireGuardStatus , getWireGuardAudit } from "./api.js";
import { getBunkerInfo } from "./api.js";
export let isLoggedIn = false;
export let userPubkey = "";
@ -11,14 +11,13 @@
@@ -11,14 +11,13 @@
const dispatch = createEventDispatcher();
// State
let wgConfig = null;
let bunkerInfo = null;
let wgStatus = null;
let auditData = null;
let isLoading = false;
let error = "";
let wgQrDataUrl = "";
let bunkerQrDataUrl = "";
let clientQrDataUrl = "";
let signerQrDataUrl = "";
let copiedItem = "";
let bunkerSecret = "";
$: canAccess = isLoggedIn & & userPubkey & & (
currentEffectiveRole === "write" ||
@ -26,116 +25,79 @@
@@ -26,116 +25,79 @@
currentEffectiveRole === "owner"
);
let hasLoadedOnce = false;
// Generate bunker URLs when bunkerInfo and userPubkey are available
$: clientBunkerURL = bunkerInfo & & userPubkey ?
`bunker://${ userPubkey } ?relay=${ encodeURIComponent ( bunkerInfo . relay_url )} ${ bunkerSecret ? `&secret=$ { bunkerSecret } ` : ''}` : "" ;
$: signerBunkerURL = bunkerInfo ?
`nostr+connect://${ bunkerInfo . relay_url } ` : "";
onMount(async () => {
// Always check status first
await checkStatus();
if (canAccess && wgStatus?.available && !hasLoadedOnce) {
hasLoadedOnce = true;
await loadConfig();
}
await loadBunkerInfo();
});
$: if (canAccess && wgStatus?.available && !hasLoadedOnce && !isLoading) {
hasLoadedOnce = true;
loadConfig();
}
async function checkStatus() {
try {
wgStatus = await fetchWireGuardStatus();
} catch (err) {
console.error("Error checking WireGuard status:", err);
wgStatus = { available : false } ;
}
}
async function loadConfig() {
if (!userSigner || !userPubkey) return;
async function loadBunkerInfo() {
isLoading = true;
error = "";
try {
// Load WireGuard config, bunker URL, and audit data in parallel
const [wgResult, bunkerResult, auditResult] = await Promise.all([
getWireGuardConfig(userSigner, userPubkey),
getBunkerURL(userSigner, userPubkey),
getWireGuardAudit(userSigner, userPubkey).catch(() => null)
]);
bunkerInfo = await getBunkerInfo();
wgConfig = wgResult;
bunkerInfo = bunkerResult;
auditData = auditResult;
// Generate QR codes
if (wgConfig?.config_text) {
wgQrDataUrl = await QRCode.toDataURL(wgConfig.config_text, {
width: 256,
margin: 2,
color: { dark : "#000000" , light : "#ffffff" }
});
// Generate a random secret for secure connection
if (!bunkerSecret) {
bunkerSecret = generateSecret();
}
if (bunkerInfo?.url) {
bunkerQrDataUrl = await QRCode.toDataURL(bunkerInfo.url, {
width: 256,
margin: 2,
color: { dark : "#000000" , light : "#ffffff" }
});
}
// Generate QR codes
await generateQRCodes();
} catch (err) {
console.error("Error loading bunker config :", err);
error = err.message || "Failed to load configur ation";
console.error("Error loading bunker info:", err);
error = err.message || "Failed to load bunker information";
} finally {
isLoading = false;
}
}
function formatDate(timestamp) {
if (!timestamp) return "Never";
return new Date(timestamp * 1000).toLocaleString();
function generateSecret() {
const array = new Uint8Array(16);
crypto.getRandomValues(array);
return Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
}
async function handleRegenerate () {
if (!confirm("Regenerate your WireGuard keys? Your current keys will stop working.")) {
return ;
}
async function regenerateSecret () {
bunkerSecret = generateSecret();
await generateQRCodes() ;
}
isLoading = true;
error = "";
async function generateQRCodes() {
if (clientBunkerURL) {
clientQrDataUrl = await QRCode.toDataURL(clientBunkerURL, {
width: 280,
margin: 2,
color: { dark : "#000000" , light : "#ffffff" }
});
}
try {
await regenerateWireGuard(userSigner, userPubkey);
// Reload config after regeneration
hasLoadedOnce = false;
await loadConfig();
} catch (err) {
console.error("Error regenerating keys:", err);
error = err.message || "Failed to regenerate keys";
} finally {
isLoading = false;
if (signerBunkerURL) {
signerQrDataUrl = await QRCode.toDataURL(signerBunkerURL, {
width: 280,
margin: 2,
color: { dark : "#000000" , light : "#ffffff" }
});
}
}
function copyToClipboard(text, label) {
navigator.clipboard.writeText(text);
alert(`${ label } copied to clipboard!` );
// Regenerate QR codes when URLs change
$: if (clientBunkerURL || signerBunkerURL) {
generateQRCodes( );
}
function downloadConfig() {
if (!wgConfig?.config_text) return;
const blob = new Blob([wgConfig.config_text], { type : "text/plain" } );
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "wg-orly.conf";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
function copyToClipboard(text, label) {
navigator.clipboard.writeText(text);
copiedItem = label;
setTimeout(() => {
copiedItem = "";
}, 2000);
}
function openLoginModal() {
@ -143,19 +105,19 @@
@@ -143,19 +105,19 @@
}
< / script >
{ #if ! wgStatus ? . available }
{ #if ! bunkerInfo ? . available }
< div class = "bunker-view" >
< div class = "unavailable-message" >
< h3 > Remote Signing Not Available< / h3 >
< p > This relay does not have WireGuard/Bunker enabled, or ACL mode is set to "none".< / p >
< p class = "hint" > Remote signing requires the relay operator to enable WireGuard VPN and use ACL mode "follows" or "managed".< / p >
< p > This relay does not have bunker mode enabled, or ACL mode is set to "none".< / p >
< p class = "hint" > Remote signing requires the relay operator to enable ACL mode "follows" or "managed".< / p >
< / div >
< / div >
{ :else if canAccess }
< div class = "bunker-view" >
< div class = "header-section" >
< h3 > Remote Signing (Bunker)< / h3 >
< button class = "refresh-btn" on:click = { loadConfig } disabled= { isLoading } >
< h3 > Remote Signing (NIP-46 Bunker)< / h3 >
< button class = "refresh-btn" on:click = { loadBunkerInfo } disabled= { isLoading } >
{ isLoading ? "Loading..." : "Refresh" }
< / button >
< / div >
@ -164,189 +126,118 @@
@@ -164,189 +126,118 @@
< div class = "error-message" > { error } </ div >
{ /if }
{ #if isLoading && ! wgConfig }
< div class = "loading" > Loading configuration...< / div >
{ :else if wgConfig }
< div class = "instructions" >
< p > < strong > How it works:< / strong > Connect to the relay's private VPN, then use Amber to sign events remotely.< / p >
{ #if bunkerInfo ? . cashu_enabled && bunkerInfo ? . acl_mode !== "none" }
< div class = "cat-warning" >
< strong > CAT Required:< / strong > This relay requires Cashu Access Tokens (CAT) for bunker connections.
Your client must support CAT authentication or connections will be rejected.
< / div >
{ /if }
< div class = "config-sections" >
<!-- Step 1: WireGuard -->
< section class = "config-section" >
< h4 > Step 1: Install WireGuard< / h4 >
< p class = "section-desc" > Download the WireGuard app for your device:< / p >
< div class = "client-links" >
< a href = "https://play.google.com/store/apps/details?id=com.wireguard.android" target = "_blank" rel = "noopener noreferrer" class = "client-link" >
< span class = "client-icon" > Android< / span >
< span class = "client-store" > Google Play< / span >
< / a >
< a href = "https://f-droid.org/packages/com.wireguard.android/" target = "_blank" rel = "noopener noreferrer" class = "client-link" >
< span class = "client-icon" > Android< / span >
< span class = "client-store" > F-Droid< / span >
< / a >
< a href = "https://apps.apple.com/app/wireguard/id1441195209" target = "_blank" rel = "noopener noreferrer" class = "client-link" >
< span class = "client-icon" > iOS< / span >
< span class = "client-store" > App Store< / span >
< / a >
< a href = "https://www.wireguard.com/install/" target = "_blank" rel = "noopener noreferrer" class = "client-link" >
< span class = "client-icon" > Desktop< / span >
< span class = "client-store" > Windows/Mac/Linux< / span >
< / a >
< / div >
< / section >
<!-- Step 2: WireGuard Config -->
< section class = "config-section" >
< h4 > Step 2: Add VPN Configuration< / h4 >
< p class = "section-desc" > Scan this QR code with the WireGuard app:< / p >
{ #if isLoading && ! bunkerInfo }
< div class = "loading" > Loading bunker information...< / div >
{ :else if bunkerInfo }
< div class = "instructions" >
< p > < strong > How it works:< / strong > Both your signing app (Amber) and your client app connect to this relay.
The relay acts as a secure middleman for NIP-46 remote signing.< / p >
< / div >
< div class = "qr-container" >
{ #if wgQrDataUrl }
< img src = { wgQrDataUrl } alt="WireGuard Configuration QR Code " class = "qr-code" />
< div class = "qr-sections" >
<!-- Client QR Code -->
< section class = "qr-section" >
< h4 > For Client App< / h4 >
< p class = "section-desc" > Scan with your Nostr client to request signatures from Amber:< / p >
< div
class="qr-container clickable"
on:click={() => copyToClipboard ( clientBunkerURL , "client" )}
on:keypress={( e ) => e . key === 'Enter' && copyToClipboard ( clientBunkerURL , "client" )}
role="button"
tabindex="0"
title="Click to copy bunker URL"
>
{ #if clientQrDataUrl }
< img src = { clientQrDataUrl } alt="Client Bunker QR Code " class = "qr-code" />
< div class = "qr-overlay" class:visible = { copiedItem === "client" } >
Copied!
< / div >
{ : else }
< div class = "qr-placeholder" > Generating QR...< / div >
{ /if }
< / div >
< div class = "config-actions" >
< button on:click = {() => copyToClipboard ( wgConfig . config_text , "Config" )} > Copy Config </ button >
< button on:click = { downloadConfig } > Download . conf </ button >
< div class = "url-display" >
< code class = "bunker-url" > { clientBunkerURL } </ code >
< / div >
< details class = "config-text-details" >
< summary > Show raw config< / summary >
< pre class = "config-text" > { wgConfig . config_text } </ pre >
< / details >
< div class = "copy-hint" > Click QR code to copy< / div >
< / section >
<!-- Step 3: Connect VPN -->
< section class = "config-section" >
< h4 > Step 3: Connect to VPN< / h4 >
< p class = "section-desc" > After importing the config, toggle the VPN connection ON in the WireGuard app.< / p >
< div class = "ip-info" >
< span class = "label" > Your VPN IP:< / span >
< code > { wgConfig . interface . address } </ code >
<!-- Signer QR Code (Amber) -->
< section class = "qr-section" >
< h4 > For Signer (Amber)< / h4 >
< p class = "section-desc" > Scan with < a href = "https://github.com/greenart7c3/Amber" target = "_blank" rel = "noopener noreferrer" > Amber< / a > to connect as a signer:< / p >
< div
class="qr-container clickable"
on:click={() => copyToClipboard ( signerBunkerURL , "signer" )}
on:keypress={( e ) => e . key === 'Enter' && copyToClipboard ( signerBunkerURL , "signer" )}
role="button"
tabindex="0"
title="Click to copy connection URL"
>
{ #if signerQrDataUrl }
< img src = { signerQrDataUrl } alt="Signer Connection QR Code " class = "qr-code" />
< div class = "qr-overlay" class:visible = { copiedItem === "signer" } >
Copied!
< / div >
{ : else }
< div class = "qr-placeholder" > Generating QR...< / div >
{ /if }
< / div >
< / section >
<!-- Step 4: Bunker URL -->
{ #if bunkerInfo }
< section class = "config-section" >
< h4 > Step 4: Add Bunker to Amber< / h4 >
< p class = "section-desc" > With VPN connected, scan this QR code in < a href = "https://github.com/greenart7c3/Amber" target = "_blank" rel = "noopener noreferrer" > Amber< / a > :< / p >
< div class = "qr-container" >
{ #if bunkerQrDataUrl }
< img src = { bunkerQrDataUrl } alt="Bunker URL QR Code " class = "qr-code" />
{ : else }
< div class = "qr-placeholder" > Generating QR...< / div >
{ /if }
< / div >
< div class = "bunker-url-container" >
< code class = "bunker-url" > { bunkerInfo . url } </ code >
< button on:click = {() => copyToClipboard ( bunkerInfo . url , "Bunker URL" )} > Copy</button >
< / div >
< div class = "relay-info" >
< span class = "label" > Relay npub:< / span >
< code class = "npub" > { bunkerInfo . relay_npub } </ code >
< / div >
< / section >
{ /if }
<!-- Amber links -->
< section class = "config-section" >
< h4 > Get Amber (NIP-46 Signer)< / h4 >
< p class = "section-desc" > Amber is an Android app for secure remote signing:< / p >
< div class = "client-links" >
< a href = "https://play.google.com/store/apps/details?id=com.greenart7c3.nostrsigner" target = "_blank" rel = "noopener noreferrer" class = "client-link" >
< span class = "client-icon" > Amber< / span >
< span class = "client-store" > Google Play< / span >
< / a >
< a href = "https://github.com/greenart7c3/Amber/releases" target = "_blank" rel = "noopener noreferrer" class = "client-link" >
< span class = "client-icon" > Amber< / span >
< span class = "client-store" > GitHub APK< / span >
< / a >
< div class = "url-display" >
< code class = "bunker-url" > { signerBunkerURL } </ code >
< / div >
< div class = "copy-hint" > Click QR code to copy< / div >
< / section >
< / div >
<!-- Danger zone -->
< div class = "danger-zone" >
< h4 > Danger Zone< / h4 >
< p > Regenerate your WireGuard keys if you believe they've been compromised.< / p >
< button class = "danger-btn" on:click = { handleRegenerate } disabled= { isLoading } >
Regenerate Keys
< / button >
<!-- Connection Info -->
< div class = "connection-info" >
< h4 > Connection Details< / h4 >
< div class = "info-row" >
< span class = "label" > Relay:< / span >
< code > { bunkerInfo . relay_url } </ code >
< button class = "copy-btn" on:click = {() => copyToClipboard ( bunkerInfo . relay_url , "relay" )} >
{ copiedItem === "relay" ? "Copied!" : "Copy" }
< / button >
< / div >
< div class = "info-row" >
< span class = "label" > Your npub:< / span >
< code class = "npub" > { userPubkey } </ code >
< / div >
< div class = "info-row" >
< span class = "label" > Secret:< / span >
< code class = "secret" > { bunkerSecret } </ code >
< button class = "copy-btn" on:click = { regenerateSecret } > Regenerate</button >
< / div >
< / div >
<!-- Audit Log Section -->
{ #if auditData && ( auditData . revoked_keys ? . length > 0 || auditData . access_logs ? . length > 0 )}
< div class = "audit-section" >
< h4 > Key History & Access Log< / h4 >
< p class = "audit-desc" > Monitor activity on your old WireGuard keys. High access counts might indicate you left something connected or someone copied your credentials.< / p >
{ #if auditData . revoked_keys ? . length > 0 }
< div class = "audit-subsection" >
< h5 > Revoked Keys< / h5 >
< div class = "audit-table-container" >
< table class = "audit-table" >
< thead >
< tr >
< th > Client IP< / th >
< th > Created< / th >
< th > Revoked< / th >
< th > Access Count< / th >
< th > Last Access< / th >
< / tr >
< / thead >
< tbody >
{ #each auditData . revoked_keys as key }
< tr class:warning = { key . access_count > 0 } >
< td >< code > { key . client_ip } </ code ></ td >
< td > { formatDate ( key . created_at )} </ td >
< td > { formatDate ( key . revoked_at )} </ td >
< td class:highlight = { key . access_count > 0 } > { key . access_count } </td >
< td > { formatDate ( key . last_access_at )} </ td >
< / tr >
{ /each }
< / tbody >
< / table >
< / div >
< / div >
{ /if }
{ #if auditData . access_logs ? . length > 0 }
< div class = "audit-subsection" >
< h5 > Recent Access Attempts (Obsolete Addresses)< / h5 >
< div class = "audit-table-container" >
< table class = "audit-table" >
< thead >
< tr >
< th > Client IP< / th >
< th > Time< / th >
< th > Remote Address< / th >
< / tr >
< / thead >
< tbody >
{ #each auditData . access_logs as log }
< tr >
< td >< code > { log . client_ip } </ code ></ td >
< td > { formatDate ( log . timestamp )} </ td >
< td >< code > { log . remote_addr } </ code ></ td >
< / tr >
{ /each }
< / tbody >
< / table >
< / div >
< / div >
{ /if }
<!-- Amber links -->
< section class = "amber-section" >
< h4 > Get Amber (NIP-46 Signer)< / h4 >
< p class = "section-desc" > Amber is an Android app for secure remote signing:< / p >
< div class = "client-links" >
< a href = "https://play.google.com/store/apps/details?id=com.greenart7c3.nostrsigner" target = "_blank" rel = "noopener noreferrer" class = "client-link" >
< span class = "client-icon" > Amber< / span >
< span class = "client-store" > Google Play< / span >
< / a >
< a href = "https://github.com/greenart7c3/Amber/releases" target = "_blank" rel = "noopener noreferrer" class = "client-link" >
< span class = "client-icon" > Amber< / span >
< span class = "client-store" > GitHub APK< / span >
< / a >
< / div >
{ /if }
< / section >
{ /if }
< / div >
{ :else if isLoggedIn }
@ -408,6 +299,16 @@
@@ -408,6 +299,16 @@
margin-bottom: 1em;
}
.cat-warning {
background-color: rgba(255, 193, 7, 0.15);
border: 1px solid rgba(255, 193, 7, 0.5);
color: var(--text-color);
padding: 0.75em 1em;
border-radius: 4px;
margin-bottom: 1em;
font-size: 0.95em;
}
.loading {
text-align: center;
padding: 2em;
@ -427,19 +328,20 @@
@@ -427,19 +328,20 @@
color: var(--text-color);
}
.config -sections {
display: flex ;
flex-direction: column ;
.qr -sections {
display: grid ;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)) ;
gap: 1.5em;
margin-bottom: 1.5em;
}
.config -section {
.qr -section {
background-color: var(--card-bg);
padding: 1.25em;
border-radius: 8px;
}
.config -section h4 {
.qr -section h4 {
margin: 0 0 0.5em 0;
color: var(--text-color);
}
@ -455,45 +357,24 @@
@@ -455,45 +357,24 @@
color: var(--primary);
}
.client-links {
display: flex;
flex-wrap: wrap;
gap: 0.75em;
}
.client-link {
.qr-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.75em 1em;
background-color: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: 6px;
text-decoration: none;
color: var(--text-color);
transition: border-color 0.2s, background-color 0.2s;
min-width: 100px;
}
.client-link:hover {
border-color: var(--primary);
background-color: var(--sidebar-bg);
justify-content: center;
margin: 1em 0;
position: relative;
}
.client-icon {
font-weight: 500 ;
margin-bottom: 0.25em ;
.qr-container.clickable {
cursor: pointer;
transition: transform 0.1s;
}
.client-store {
font-size: 0.8em;
opacity: 0.7;
.qr-container.clickable:hover {
transform: scale(1.02);
}
.qr-container {
display: flex;
justify-content: center;
margin: 1em 0;
.qr-container.clickable:active {
transform: scale(0.98);
}
.qr-code {
@ -502,9 +383,29 @@
@@ -502,9 +383,29 @@
padding: 8px;
}
.qr-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.85);
color: #4ade80;
padding: 0.75em 1.5em;
border-radius: 8px;
font-weight: 600;
font-size: 1.1em;
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
}
.qr-overlay.visible {
opacity: 1;
}
.qr-placeholder {
width: 256px;
height: 256px;
width: 280 px;
height: 280 px;
display: flex;
align-items: center;
justify-content: center;
@ -514,59 +415,59 @@
@@ -514,59 +415,59 @@
opacity: 0.5;
}
.config-actions {
display: flex;
justify-content: center;
gap: 0.75em;
margin-top: 1em;
.url-display {
text-align: center;
margin-top: 0.5em;
}
.config-actions button {
padding: 0.5em 1em;
background-color: var(--primary);
color: var(--text-color);
border: none;
.bunker-url {
font-family: monospace;
font-size: 0.75em;
word-break: break-all;
padding: 0.5em;
background-color: var(--bg-color);
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
}
.config-actions button:hover {
background-color: var(--accent-hover-color);
display: inline-block;
max-width: 100%;
color: var(--text-color);
}
.config-text-details {
margin-top: 1em;
.copy-hint {
text-align: center;
font-size: 0.8em;
color: var(--text-color);
opacity: 0.6;
margin-top: 0.5em;
}
.config-text-details summary {
cursor: pointer;
color: var(--text-color) ;
opacity: 0.8 ;
font-size: 0.9 em;
.connection-info {
background-color: var(--card-bg) ;
padding: 1.25em ;
border-radius: 8px ;
margin-bottom: 1.5 em;
}
.config-text {
margin-top: 0.5em;
padding: 1em;
background-color: var(--bg-color);
border-radius: 4px;
font-size: 0.85em;
overflow-x: auto;
white-space: pre;
.connection-info h4 {
margin: 0 0 1em 0;
color: var(--text-color);
}
.ip-info, .relay-info {
.info-row {
display: flex;
align-items: center;
gap: 0.5em;
margin-top: 0.5em;
margin-bottom: 0.75em;
flex-wrap: wrap;
}
.info-row:last-child {
margin-bottom: 0;
}
.label {
color: var(--text-color);
opacity: 0.7;
min-width: 80px;
}
code {
@ -575,78 +476,71 @@
@@ -575,78 +476,71 @@
background-color: var(--bg-color);
border-radius: 4px;
color: var(--text-color);
word-break: break-all;
}
.bunker-url-container {
display: flex;
align-items: center;
gap: 0.5em;
justify-content: center;
flex-wrap: wrap;
}
.bunker-url {
word-break: break-all;
max-width: 400px;
.npub, .secret {
font-size: 0.85em;
}
.bunker-url-container butto n {
padding: 0.4em 0.8 em;
.copy-btn {
padding: 0.3em 0.6em;
background-color: var(--primary);
color: var(--text-color);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85 em;
font-size: 0.8em;
}
.bunker-url-container butto n:hover {
.copy-bt n:hover {
background-color: var(--accent-hover-color);
}
.npub {
word-break: break-all;
font-size: 0.85em;
}
.danger-zone {
margin-top: 2em;
.amber-section {
background-color: var(--card-bg);
padding: 1.25em;
border: 1px solid var(--warning);
border-radius: 8px;
background-color: rgba(255, 100, 100, 0.05);
}
.danger-zone h4 {
.amber-section h4 {
margin: 0 0 0.5em 0;
color: var(--warning );
color: var(--text-color );
}
.danger-zone p {
margin: 0 0 1em 0;
.client-links {
display: flex;
flex-wrap: wrap;
gap: 0.75em;
}
.client-link {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.75em 1em;
background-color: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: 6px;
text-decoration: none;
color: var(--text-color);
opacity: 0.8;
font-size: 0.95em;
transition: border-color 0.2s, background-color 0.2s ;
min-width: 100px ;
}
.danger-btn {
background-color: transparent;
border: 1px solid var(--warning);
color: var(--warning);
padding: 0.5em 1em;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
.client-link:hover {
border-color: var(--primary);
background-color: var(--sidebar-bg);
}
.danger-btn:hover:not(:disabled) {
background-color: var(--warning) ;
color: var(--text-color) ;
.client-icon {
font-weight: 500;
margin-bottom: 0.25em;
}
.danger-btn:disabled {
opacity: 0.5 ;
cursor: not-allowed ;
.client-store {
font-size: 0.8em ;
opacity: 0.7 ;
}
.unavailable-message, .access-denied {
@ -703,83 +597,11 @@
@@ -703,83 +597,11 @@
background-color: var(--accent-hover-color);
}
/* Audit section styles */
.audit-section {
margin-top: 2em;
padding: 1.25em;
border: 1px solid var(--border-color);
border-radius: 8px;
background-color: var(--card-bg);
}
.audit-section h4 {
margin: 0 0 0.5em 0;
color: var(--text-color);
}
.audit-desc {
margin: 0 0 1em 0;
color: var(--text-color);
opacity: 0.8;
font-size: 0.9em;
}
.audit-subsection {
margin-bottom: 1.5em;
}
.audit-subsection:last-child {
margin-bottom: 0;
}
.audit-subsection h5 {
margin: 0 0 0.5em 0;
color: var(--text-color);
font-size: 0.95em;
}
.audit-table-container {
overflow-x: auto;
}
.audit-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85em;
}
.audit-table th,
.audit-table td {
padding: 0.5em 0.75em;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.audit-table th {
background-color: var(--bg-color);
color: var(--text-color);
font-weight: 500;
}
.audit-table td {
color: var(--text-color);
}
.audit-table td code {
font-size: 0.9em;
padding: 0.15em 0.3em;
}
.audit-table tr.warning {
background-color: rgba(255, 100, 100, 0.1);
}
.audit-table td.highlight {
color: var(--warning);
font-weight: 600;
}
@media (max-width: 600px) {
.qr-sections {
grid-template-columns: 1fr;
}
.client-links {
flex-direction: column;
}
@ -789,16 +611,12 @@
@@ -789,16 +611,12 @@
}
.bunker-url {
font-size: 0.7 5em;
font-size: 0.6 5em;
}
.audit-table {
font-size: 0.75em;
}
.audit-table th,
.audit-table td {
padding: 0.4em 0.5em;
.info-row {
flex-direction: column;
align-items: flex-start;
}
}
< / style >