v0.9.26 added a admin tools

This commit is contained in:
2026-03-15 16:36:01 -04:00
parent 53d665cc6f
commit 7bc0d26cdd
17 changed files with 872 additions and 96 deletions

View File

@@ -2,16 +2,27 @@ import { useState, useEffect } from 'react';
import { api } from '../utils/api.js';
import { useToast } from '../contexts/ToastContext.jsx';
export default function SettingsModal({ onClose }) {
export default function SettingsModal({ onClose, onFeaturesChanged }) {
const toast = useToast();
const [vapidPublic, setVapidPublic] = useState('');
const [loading, setLoading] = useState(true);
const [generating, setGenerating] = useState(false);
const [showRegenWarning, setShowRegenWarning] = useState(false);
// Registration
const [regCode, setRegCode] = useState('');
const [activeCode, setActiveCode] = useState('');
const [features, setFeatures] = useState({ branding: false, groupManager: false });
const [regLoading, setRegLoading] = useState(false);
useEffect(() => {
api.getSettings().then(({ settings }) => {
setVapidPublic(settings.vapid_public || '');
setActiveCode(settings.registration_code || '');
setFeatures({
branding: settings.feature_branding === 'true',
groupManager: settings.feature_group_manager === 'true',
});
setLoading(false);
}).catch(() => setLoading(false));
}, []);
@@ -31,17 +42,35 @@ export default function SettingsModal({ onClose }) {
};
const handleGenerateClick = () => {
if (vapidPublic) {
setShowRegenWarning(true);
} else {
doGenerate();
if (vapidPublic) setShowRegenWarning(true);
else doGenerate();
};
const handleRegister = async () => {
setRegLoading(true);
try {
const { features: f } = await api.registerCode(regCode.trim());
setFeatures(f);
setActiveCode(regCode.trim());
setRegCode('');
toast(regCode.trim() ? 'Registration code applied.' : 'Registration cleared.', 'success');
window.dispatchEvent(new Event('jama:settings-changed'));
onFeaturesChanged && onFeaturesChanged(f);
} catch (e) {
toast(e.message || 'Invalid code', 'error');
} finally {
setRegLoading(false);
}
};
const featureList = [
features.branding && 'Branding',
features.groupManager && 'Group Manager',
].filter(Boolean);
return (
<div className="modal-overlay" onClick={e => e.target === e.currentTarget && onClose()}>
<div className="modal" style={{ maxWidth: 480 }}>
<div className="flex items-center justify-between" style={{ marginBottom: 20 }}>
<h2 className="modal-title" style={{ margin: 0 }}>Settings</h2>
<button className="btn-icon" onClick={onClose}>
@@ -49,84 +78,78 @@ export default function SettingsModal({ onClose }) {
</button>
</div>
<div>
<div className="settings-section-label">Web Push Notifications (VAPID)</div>
{/* Registration Code */}
<div style={{ marginBottom: 28 }}>
<div className="settings-section-label">Feature Registration</div>
{activeCode && featureList.length > 0 && (
<div style={{ marginBottom: 10, display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, color: 'var(--success)' }}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="20 6 9 17 4 12"/></svg>
Active unlocks: {featureList.join(', ')}
</div>
)}
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
<input
className="input flex-1"
placeholder="Enter registration code"
value={regCode}
onChange={e => setRegCode(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleRegister()}
/>
<button className="btn btn-primary btn-sm" onClick={handleRegister} disabled={regLoading}>
{regLoading ? '…' : 'Apply'}
</button>
</div>
{activeCode && (
<button className="btn btn-secondary btn-sm" onClick={() => { setRegCode(''); api.registerCode('').then(() => { setFeatures({ branding: false, groupManager: false }); setActiveCode(''); window.dispatchEvent(new Event('jama:settings-changed')); onFeaturesChanged && onFeaturesChanged({ branding: false, groupManager: false }); }); }}>
Clear Registration
</button>
)}
<p style={{ fontSize: 12, color: 'var(--text-tertiary)', marginTop: 8 }}>
A registration code unlocks Branding and Group Manager features. Contact your jama provider for a code.
</p>
</div>
<div style={{ borderTop: '1px solid var(--border)', paddingTop: 20 }}>
<div className="settings-section-label">Web Push Notifications (VAPID)</div>
{loading ? (
<p style={{ fontSize: 13, color: "var(--text-secondary)" }}>Loading</p>
<p style={{ fontSize: 13, color: 'var(--text-secondary)' }}>Loading</p>
) : (
<>
{vapidPublic ? (
<div style={{ marginBottom: 16 }}>
<div style={{
background: "var(--surface-variant)",
border: "1px solid var(--border)",
borderRadius: "var(--radius)",
padding: "10px 12px",
marginBottom: 10,
}}>
<div style={{ fontSize: 11, color: "var(--text-tertiary)", marginBottom: 4, textTransform: "uppercase", letterSpacing: "0.5px" }}>
Public Key
</div>
<code style={{
fontSize: 11,
color: "var(--text-primary)",
wordBreak: "break-all",
lineHeight: 1.5,
display: "block",
}}>
{vapidPublic}
</code>
<div style={{ background: 'var(--surface-variant)', border: '1px solid var(--border)', borderRadius: 'var(--radius)', padding: '10px 12px', marginBottom: 10 }}>
<div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginBottom: 4, textTransform: 'uppercase', letterSpacing: '0.5px' }}>Public Key</div>
<code style={{ fontSize: 11, color: 'var(--text-primary)', wordBreak: 'break-all', lineHeight: 1.5, display: 'block' }}>{vapidPublic}</code>
</div>
<span style={{ fontSize: 13, color: "var(--success)", display: "flex", alignItems: "center", gap: 5 }}>
<span style={{ fontSize: 13, color: 'var(--success)', display: 'flex', alignItems: 'center', gap: 5 }}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="20 6 9 17 4 12"/></svg>
Push notifications active
</span>
</div>
) : (
<p style={{ fontSize: 13, color: "var(--text-secondary)", marginBottom: 12 }}>
No VAPID keys found. Generate keys to enable Web Push notifications users can then receive alerts when the app is closed or in the background.
<p style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 12 }}>
No VAPID keys found. Generate keys to enable Web Push notifications.
</p>
)}
{showRegenWarning && (
<div style={{
background: "#fce8e6",
border: "1px solid #f5c6c2",
borderRadius: "var(--radius)",
padding: "14px 16px",
marginBottom: 16,
}}>
<p style={{ fontSize: 13, fontWeight: 600, color: "var(--error)", marginBottom: 8 }}>
Regenerate VAPID keys?
<div style={{ background: '#fce8e6', border: '1px solid #f5c6c2', borderRadius: 'var(--radius)', padding: '14px 16px', marginBottom: 16 }}>
<p style={{ fontSize: 13, fontWeight: 600, color: 'var(--error)', marginBottom: 8 }}> Regenerate VAPID keys?</p>
<p style={{ fontSize: 13, color: '#5c2c28', marginBottom: 12, lineHeight: 1.5 }}>
Generating new keys will <strong>invalidate all existing push subscriptions</strong>. Users will need to re-enable notifications.
</p>
<p style={{ fontSize: 13, color: "#5c2c28", marginBottom: 12, lineHeight: 1.5 }}>
Generating new keys will <strong>invalidate all existing push subscriptions</strong>. Every user will stop receiving push notifications immediately and will need to re-enable them by opening the app. This cannot be undone.
</p>
<div style={{ display: "flex", gap: 8 }}>
<button
className="btn btn-sm"
style={{ background: "var(--error)", color: "white" }}
onClick={doGenerate}
disabled={generating}
>
{generating ? "Generating…" : "Yes, regenerate keys"}
</button>
<button className="btn btn-secondary btn-sm" onClick={() => setShowRegenWarning(false)}>
Cancel
</button>
<div style={{ display: 'flex', gap: 8 }}>
<button className="btn btn-sm" style={{ background: 'var(--error)', color: 'white' }} onClick={doGenerate} disabled={generating}>{generating ? 'Generating…' : 'Yes, regenerate keys'}</button>
<button className="btn btn-secondary btn-sm" onClick={() => setShowRegenWarning(false)}>Cancel</button>
</div>
</div>
)}
{!showRegenWarning && (
<button className="btn btn-primary btn-sm" onClick={handleGenerateClick} disabled={generating}>
{generating ? "Generating…" : vapidPublic ? "Regenerate Keys" : "Generate Keys"}
{generating ? 'Generating…' : vapidPublic ? 'Regenerate Keys' : 'Generate Keys'}
</button>
)}
<p style={{ fontSize: 12, color: "var(--text-tertiary)", marginTop: 12, lineHeight: 1.5 }}>
Requires HTTPS. After generating, users will be prompted to enable notifications on their next visit. On iOS, the app must be installed to the home screen first.
<p style={{ fontSize: 12, color: 'var(--text-tertiary)', marginTop: 12, lineHeight: 1.5 }}>
Requires HTTPS. On iOS, the app must be installed to the home screen first.
</p>
</>
)}