added to remote repo

This commit is contained in:
Philipp
2026-05-21 08:18:29 +02:00
parent 8697c9f405
commit cc599b6e18
8049 changed files with 1096323 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
import { Eye, EyeOff, Save } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { api, errorMessage, setApiToken } from '../api.js';
export function Settings({ onChanged }) {
const [fields, setFields] = useState([]);
const [values, setValues] = useState({});
const [visibleSecrets, setVisibleSecrets] = useState({});
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState('');
const [error, setError] = useState('');
const requiredMissing = useMemo(
() => fields.filter((field) => field.required && !String(values[field.key] || '').trim()),
[fields, values],
);
async function loadSettings() {
setError('');
try {
const result = await api.get('/settings');
setFields(result.data.fields || []);
setValues(Object.fromEntries((result.data.fields || []).map((field) => [field.key, field.value || ''])));
} catch (requestError) {
setError(errorMessage(requestError));
}
}
useEffect(() => {
loadSettings();
}, []);
async function saveSettings(event) {
event.preventDefault();
setSaving(true);
setMessage('');
setError('');
try {
setApiToken(values.API_TOKEN);
const result = await api.put('/settings', { values });
setFields(result.data.fields || []);
setValues(Object.fromEntries((result.data.fields || []).map((field) => [field.key, field.value || ''])));
setMessage('Settings saved. Running jobs use the updated values; a changed PORT still needs a backend restart.');
await onChanged?.();
} catch (requestError) {
setError(errorMessage(requestError));
} finally {
setSaving(false);
}
}
return (
<section className="space-y-5">
<div className="border-b border-zinc-800 pb-5">
<h1 className="text-2xl font-semibold text-zinc-100">Settings</h1>
<p className="mt-2 max-w-3xl text-sm leading-6 text-zinc-500">
Values are written to <span className="font-mono text-zinc-400">backend/.env</span>. Keep access to this UI restricted because secrets are editable here.
</p>
</div>
{error ? <Notice tone="bad" text={error} /> : null}
{message ? <Notice tone="good" text={message} /> : null}
{requiredMissing.length ? (
<Notice tone="warn" text={`Missing required values: ${requiredMissing.map((field) => field.key).join(', ')}`} />
) : null}
<form className="rounded-md border border-zinc-800 bg-zinc-900/70" onSubmit={saveSettings}>
<div className="grid gap-4 p-4 lg:grid-cols-2">
{fields.map((field) => {
const secretVisible = visibleSecrets[field.key];
return (
<label className="block text-sm text-zinc-300" key={field.key}>
<span className="flex items-center justify-between gap-3">
<span>
{field.label}
{field.required ? <span className="text-red-300"> *</span> : null}
</span>
<span className="font-mono text-xs text-zinc-500">{field.key}</span>
</span>
<span className="mt-2 flex rounded-md border border-zinc-700 bg-zinc-950 focus-within:border-cyan-600">
<input
className="h-10 min-w-0 flex-1 bg-transparent px-3 text-zinc-100 outline-none"
onChange={(event) => setValues((current) => ({ ...current, [field.key]: event.target.value }))}
type={field.secret && !secretVisible ? 'password' : 'text'}
value={values[field.key] || ''}
/>
{field.secret ? (
<button
className="flex h-10 w-10 items-center justify-center border-l border-zinc-800 text-zinc-400 hover:text-zinc-100"
onClick={() => setVisibleSecrets((current) => ({ ...current, [field.key]: !current[field.key] }))}
type="button"
>
{secretVisible ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
) : null}
</span>
</label>
);
})}
</div>
<div className="flex justify-end border-t border-zinc-800 px-4 py-3">
<button
className="inline-flex h-9 items-center gap-2 rounded-md border border-cyan-800 bg-cyan-950 px-3 text-sm text-cyan-100 hover:bg-cyan-900"
disabled={saving}
type="submit"
>
<Save className="h-4 w-4" />
Save Settings
</button>
</div>
</form>
</section>
);
}
function Notice({ text, tone }) {
const classes = {
bad: 'border-red-900 bg-red-950/50 text-red-200',
good: 'border-emerald-900 bg-emerald-950/50 text-emerald-200',
warn: 'border-amber-900 bg-amber-950/50 text-amber-200',
};
return <div className={`rounded-md border px-4 py-3 text-sm ${classes[tone]}`}>{text}</div>;
}