Clipboard fix: fallback fuer HTTP (non-secure) context, SSH copy-to-clipboard

This commit is contained in:
cynfo3000
2026-03-03 23:54:56 +01:00
parent 025840e9ba
commit 983ecf390c
6 changed files with 99 additions and 43 deletions
+26 -33
View File
@@ -267,23 +267,23 @@ export default function Agents() {
<div className="text-gray-500">Laden...</div>
) : (
<div className="bg-gray-900 rounded-lg border border-gray-800 overflow-x-auto">
<table className="w-full text-sm whitespace-nowrap">
<table className="w-full text-xs whitespace-nowrap">
<thead>
<tr className="border-b border-gray-800 text-left text-gray-500 text-xs">
<th className="px-3 py-2 font-medium w-8"></th>
<th className="px-2 py-1.5 font-medium w-8"></th>
{isColVisible('customer') && <SortHeader label="KUNDE" k="customer" />}
{isColVisible('name') && <SortHeader label="NAME" k="name" />}
{isColVisible('hostname') && <th className="px-3 py-2 font-medium">HOSTNAME</th>}
{isColVisible('hostname') && <th className="px-2 py-1.5 font-medium">HOSTNAME</th>}
{isColVisible('version') && <SortHeader label="VERSION" k="version" />}
{isColVisible('wanip') && <th className="px-3 py-2 font-medium">WAN IP</th>}
{isColVisible('lanip') && <th className="px-3 py-2 font-medium">LAN IP</th>}
{isColVisible('wanip') && <th className="px-2 py-1.5 font-medium">WAN IP</th>}
{isColVisible('lanip') && <th className="px-2 py-1.5 font-medium">LAN IP</th>}
{isColVisible('uptime') && <SortHeader label="UPTIME" k="uptime" />}
{isColVisible('cpu') && <SortHeader label="CPU" k="cpu" />}
{isColVisible('ram') && <SortHeader label="RAM" k="ram" />}
{isColVisible('disk') && <SortHeader label="DISK" k="disk" />}
{isColVisible('lastresponse') && <th className="px-3 py-2 font-medium">LETZTER KONTAKT</th>}
{isColVisible('gateways') && <th className="px-3 py-2 font-medium">GATEWAYS</th>}
{isColVisible('agentversion') && <th className="px-3 py-2 font-medium">AGENT</th>}
{isColVisible('lastresponse') && <th className="px-2 py-1.5 font-medium">LETZTER KONTAKT</th>}
{isColVisible('gateways') && <th className="px-2 py-1.5 font-medium">GATEWAYS</th>}
{isColVisible('agentversion') && <th className="px-2 py-1.5 font-medium">AGENT</th>}
</tr>
</thead>
<tbody className="divide-y divide-gray-800">
@@ -293,37 +293,30 @@ export default function Agents() {
onClick={() => setSelectedId(a.id)}
className={`hover:bg-gray-800/50 transition-colors cursor-pointer ${selectedId === a.id ? 'bg-gray-800/70' : ''}`}
>
<td className="px-3 py-2"><StatusBadge status={a.status} size="dot" /></td>
<td className="px-2 py-1.5"><StatusBadge status={a.status} size="dot" /></td>
{isColVisible('customer') && (
<td className="px-3 py-2 text-gray-400">
<td className="px-2 py-1.5 text-gray-400">
{a.customer ? <span className="text-orange-400">{a.customerNumber}</span> : <span className="text-gray-600">—</span>}
</td>
)}
{isColVisible('name') && (
<td className="px-3 py-2 text-white font-medium">
<span className="inline-flex items-center gap-2">
<span className={`text-[10px] font-mono px-1 rounded ${a.platform === 'linux' ? 'bg-blue-900/50 text-blue-400' : 'bg-orange-900/50 text-orange-400'}`}>
{a.platform === 'linux' ? 'LNX' : 'OPN'}
</span>
{a.name}
</span>
</td>
<td className="px-2 py-1.5 text-white font-medium">{a.name}</td>
)}
{isColVisible('hostname') && <td className="px-3 py-2 text-gray-400 text-xs">{a.hostname}</td>}
{isColVisible('version') && <td className="px-3 py-2 text-gray-400">{a.version || '—'}</td>}
{isColVisible('wanip') && <td className="px-3 py-2 text-gray-400">{a.wanIp || '—'}</td>}
{isColVisible('lanip') && <td className="px-3 py-2 text-gray-400">{a.lanIp || '—'}</td>}
{isColVisible('uptime') && <td className="px-3 py-2 text-gray-400">{a.uptime ? formatUptime(a.uptime) : '—'}</td>}
{isColVisible('cpu') && <td className="px-3 py-2"><MiniBar value={a.cpuPct} /></td>}
{isColVisible('ram') && <td className="px-3 py-2"><MiniBar value={a.ramPct} /></td>}
{isColVisible('disk') && <td className="px-3 py-2"><MiniBar value={a.diskPct} /></td>}
{isColVisible('hostname') && <td className="px-2 py-1.5 text-gray-400 text-xs">{a.hostname}</td>}
{isColVisible('version') && <td className="px-2 py-1.5 text-gray-400">{a.version || '—'}</td>}
{isColVisible('wanip') && <td className="px-2 py-1.5 text-gray-400">{a.wanIp || '—'}</td>}
{isColVisible('lanip') && <td className="px-2 py-1.5 text-gray-400">{a.lanIp || '—'}</td>}
{isColVisible('uptime') && <td className="px-2 py-1.5 text-gray-400">{a.uptime ? formatUptime(a.uptime) : '—'}</td>}
{isColVisible('cpu') && <td className="px-2 py-1.5"><MiniBar value={a.cpuPct} /></td>}
{isColVisible('ram') && <td className="px-2 py-1.5"><MiniBar value={a.ramPct} /></td>}
{isColVisible('disk') && <td className="px-2 py-1.5"><MiniBar value={a.diskPct} /></td>}
{isColVisible('lastresponse') && (
<td className="px-3 py-2 text-gray-500 text-xs">
<td className="px-2 py-1.5 text-gray-500 text-xs">
{a.last_heartbeat ? new Date(a.last_heartbeat).toLocaleString('de-DE') : '—'}
</td>
)}
{isColVisible('gateways') && (
<td className="px-3 py-2">
<td className="px-2 py-1.5">
{a.gateways.length > 0 ? (
<div className="flex flex-col gap-0.5">
{a.gateways.map((gw) => (
@@ -337,7 +330,7 @@ export default function Agents() {
</td>
)}
{isColVisible('agentversion') && (
<td className="px-3 py-2 text-gray-500 text-xs">{a.agent_version || '—'}</td>
<td className="px-2 py-1.5 text-gray-500 text-xs">{a.agent_version || '—'}</td>
)}
</tr>
))}
@@ -463,14 +456,14 @@ function gwDotColor(status) {
}
function MiniBar({ value }) {
if (value === null || value === undefined) return <span className="text-gray-600 text-xs">—</span>
if (value === null || value === undefined) return <span className="text-gray-600 text-[10px]">—</span>
const color = value > 90 ? 'bg-red-500' : value > 70 ? 'bg-yellow-500' : value > 50 ? 'bg-blue-500' : 'bg-green-500'
return (
<div className="flex items-center gap-1.5">
<div className="w-12 h-1.5 bg-gray-800 rounded overflow-hidden">
<div className="flex items-center gap-1">
<div className="w-10 h-1 bg-gray-800 rounded overflow-hidden">
<div className={`h-full rounded ${color}`} style={{ width: `${Math.min(value, 100)}%` }} />
</div>
<span className="text-xs text-gray-400">{value.toFixed(0)}%</span>
<span className="text-[10px] text-gray-400">{value.toFixed(0)}%</span>
</div>
)
}
+2 -1
View File
@@ -1,6 +1,7 @@
import { useEffect, useState, useRef } from 'react'
import api from '../api/client'
import { Upload, RefreshCw, CheckCircle, AlertTriangle, ArrowUpCircle, X, Monitor, Server, Cpu, Package, Copy, Check } from 'lucide-react'
import { copyToClipboard } from '../utils/clipboard'
const PLATFORMS = [
{ id: 'freebsd', label: 'FreeBSD / OPNsense', icon: Server },
@@ -245,7 +246,7 @@ export default function Firmware() {
<pre className="text-xs font-mono text-orange-300 whitespace-pre-wrap">{`fetch --no-verify-peer --no-verify-hostname -o /tmp/installer.zip "${BACKEND_URL}/api/v1/installer/download?platform=freebsd&api_key=01532e5a7c9e70bf2757df77a2f5b9b9" && cd /tmp && unzip -o installer.zip && sh /tmp/install.sh`}</pre>
<button
onClick={() => {
navigator.clipboard.writeText(`fetch --no-verify-peer --no-verify-hostname -o /tmp/installer.zip "${BACKEND_URL}/api/v1/installer/download?platform=freebsd&api_key=01532e5a7c9e70bf2757df77a2f5b9b9" && cd /tmp && unzip -o installer.zip && sh /tmp/install.sh`)
copyToClipboard(`fetch --no-verify-peer --no-verify-hostname -o /tmp/installer.zip "${BACKEND_URL}/api/v1/installer/download?platform=freebsd&api_key=01532e5a7c9e70bf2757df77a2f5b9b9" && cd /tmp && unzip -o installer.zip && sh /tmp/install.sh`)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}}
+5 -4
View File
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'
import api from '../api/client'
import { useAuthStore } from '../stores/auth'
import { copyToClipboard } from '../utils/clipboard'
import { Plus, Trash2, KeyRound, ShieldAlert, Copy, Check, Key } from 'lucide-react'
export default function SettingsPage() {
@@ -256,8 +257,8 @@ function APIKeysSection() {
reload()
}
const copyToClipboard = (text, id) => {
navigator.clipboard.writeText(text)
const copyText = (text, id) => {
copyToClipboard(text)
setCopied(id)
setTimeout(() => setCopied(null), 2000)
}
@@ -312,7 +313,7 @@ function APIKeysSection() {
{newKey.key}
</code>
<button
onClick={() => copyToClipboard(newKey.key, 'new')}
onClick={() => copyText(newKey.key, 'new')}
className="px-3 py-2 bg-gray-800 hover:bg-gray-700 rounded text-gray-300 transition-colors"
>
{copied === 'new' ? <Check className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4" />}
@@ -343,7 +344,7 @@ function APIKeysSection() {
<div className="flex items-center gap-3 mt-0.5">
<span className="text-xs text-gray-600 font-mono">{k.key.substring(0, 8)}...{k.key.substring(k.key.length - 4)}</span>
<button
onClick={() => copyToClipboard(k.key, k.id)}
onClick={() => copyText(k.key, k.id)}
className="text-gray-600 hover:text-orange-400 transition-colors"
title="Key kopieren"
>