165 lines
7.2 KiB
React
165 lines
7.2 KiB
React
import { useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useLicense } from '../../contexts/LicenseContext';
|
|
import api from '../../services/api';
|
|
import './LicenseManager.css';
|
|
|
|
export default function LicenseManager() {
|
|
const navigate = useNavigate();
|
|
const { status, refreshStatus } = useLicense();
|
|
|
|
const [blob, setBlob] = useState('');
|
|
const [message, setMessage] = useState('');
|
|
const [error, setError] = useState('');
|
|
const [saving, setSaving] = useState(false);
|
|
const [copied, setCopied] = useState('');
|
|
|
|
const copy = (text, label) => {
|
|
navigator.clipboard?.writeText(text).then(
|
|
() => { setCopied(label); setTimeout(() => setCopied(''), 2000); },
|
|
() => setError('Could not copy - select and copy manually')
|
|
);
|
|
};
|
|
|
|
const handleInstall = async () => {
|
|
if (!blob.trim()) return;
|
|
setSaving(true); setError(''); setMessage('');
|
|
try {
|
|
await api.post('/api/license/', { key_blob: blob.trim() });
|
|
await refreshStatus();
|
|
setBlob('');
|
|
setMessage('Licence installed.');
|
|
} catch (err) {
|
|
setError(err.response?.data?.detail || 'Could not install licence');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
if (!status) {
|
|
return (
|
|
<div className="license-manager">
|
|
<div className="lm-body"><div className="lm-loading">Loading licence...</div></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const componentsJson = JSON.stringify(status.components || {});
|
|
|
|
return (
|
|
<div className="license-manager">
|
|
<div className="lm-body">
|
|
<div className="lm-toolbar">
|
|
<button className="lm-back" onClick={() => navigate('/')}>← Back</button>
|
|
</div>
|
|
|
|
{message && <div className="lm-ok">{message}</div>}
|
|
{error && <div className="lm-error">{error}</div>}
|
|
|
|
{/* Current state */}
|
|
<div className="lm-panel">
|
|
<h3>Licence</h3>
|
|
{status.valid ? (
|
|
<div className="lm-state lm-state--ok">
|
|
<div className="lm-badge lm-badge--ok">Licensed</div>
|
|
<dl className="lm-facts">
|
|
<dt>Customer</dt><dd>{status.customer || '—'}</dd>
|
|
<dt>Stations</dt>
|
|
<dd>{status.stations_used} of {status.stations} in use</dd>
|
|
</dl>
|
|
</div>
|
|
) : (
|
|
<div className="lm-state lm-state--warn">
|
|
<div className="lm-badge lm-badge--warn">
|
|
{status.in_grace ? 'Not licensed' : 'Licence expired'}
|
|
</div>
|
|
<p className="lm-reason">{status.reason}</p>
|
|
{status.in_grace ? (
|
|
<p className="lm-grace">
|
|
Everything keeps working for <strong>{status.grace_days_left} more days</strong>.
|
|
After that only {1} station can measure. Send the fingerprint below to
|
|
your supplier to get a licence key.
|
|
</p>
|
|
) : (
|
|
<p className="lm-grace">
|
|
The grace period has ended, so only {status.stations} station can measure.
|
|
Send the fingerprint below to your supplier.
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Fingerprint to send */}
|
|
<div className="lm-panel">
|
|
<h3>This machine</h3>
|
|
<p className="lm-hint">
|
|
Send this to your supplier to obtain a licence key. It identifies this
|
|
server only — the values are one-way hashes, not hardware serial numbers.
|
|
</p>
|
|
|
|
<div className="lm-fingerprint">
|
|
<code>{status.fingerprint}</code>
|
|
<button onClick={() => copy(status.fingerprint, 'fingerprint')}>
|
|
{copied === 'fingerprint' ? 'Copied' : 'Copy'}
|
|
</button>
|
|
</div>
|
|
|
|
<details className="lm-details">
|
|
<summary>Full details (needed to issue the key)</summary>
|
|
<div className="lm-fingerprint">
|
|
<code className="lm-components">{componentsJson}</code>
|
|
<button onClick={() => copy(componentsJson, 'components')}>
|
|
{copied === 'components' ? 'Copied' : 'Copy'}
|
|
</button>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
|
|
{/* Install */}
|
|
<div className="lm-panel">
|
|
<h3>Install a licence key</h3>
|
|
<textarea
|
|
className="lm-blob"
|
|
value={blob}
|
|
onChange={e => setBlob(e.target.value)}
|
|
placeholder="Paste the licence key from your supplier"
|
|
rows={5}
|
|
/>
|
|
<button className="lm-install" onClick={handleInstall} disabled={saving || !blob.trim()}>
|
|
{saving ? 'Checking...' : 'Install licence'}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Connected stations */}
|
|
{status.slots && (
|
|
<div className="lm-panel">
|
|
<h3>Connected stations ({status.slots.length})</h3>
|
|
{status.slots.length === 0 ? (
|
|
<div className="lm-empty">No stations have reported in.</div>
|
|
) : (
|
|
<table className="lm-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Station</th><th>COM port</th><th>Bridge version</th><th>Last seen</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{status.slots.map(s => (
|
|
<tr key={s.station_key}>
|
|
<td>{s.station_key}</td>
|
|
<td>{s.com_port || '—'}</td>
|
|
<td>{s.bridge_version || '—'}</td>
|
|
<td>{new Date(s.last_seen).toLocaleTimeString()}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|