Files
scalesApp/frontend/src/components/License/LicenseManager.jsx
T

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('/')}>&larr; 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 &mdash; 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>
);
}