350 lines
15 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useState } from 'react';
import type { AppConfig, DeployServer, GitServer } from '../api/client';
import { api } from '../api/client';
export default function SettingsPage() {
const [config, setConfig] = useState<AppConfig | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState('');
useEffect(() => {
api.getConfig().then(c => { setConfig(c); setLoading(false); }).catch(() => setLoading(false));
}, []);
async function handleSave(e: React.FormEvent) {
e.preventDefault();
if (!config) return;
setSaving(true);
setMessage('');
try {
await api.updateConfig({
FOXY_WORKSPACE: config.FOXY_WORKSPACE,
GITEA_SERVER: config.GITEA_SERVER,
DEPLOYMENT_SERVER: config.DEPLOYMENT_SERVER,
DEPLOYMENT_USER: config.DEPLOYMENT_USER,
LOG_LEVEL: config.LOG_LEVEL,
});
setMessage('✅ Configuration sauvegardée');
} catch (err) {
setMessage('❌ ' + (err instanceof Error ? err.message : 'Erreur'));
}
setSaving(false);
}
if (loading || !config) {
return <div className="flex items-center justify-center h-64"><div className="text-fox-500 animate-spin-slow text-4xl">🦊</div></div>;
}
const fields: { key: keyof AppConfig; label: string; icon: string; editable: boolean; secret?: boolean }[] = [
{ key: 'FOXY_WORKSPACE', label: 'Workspace Foxy (Conteneur)', icon: '📁', editable: true },
{ key: 'GITEA_SERVER', label: 'Serveur Gitea', icon: '🌐', editable: true },
{ key: 'GITEA_OPENCLAW_TOKEN', label: 'Token Gitea', icon: '🔑', editable: false, secret: true },
{ key: 'DEPLOYMENT_SERVER', label: 'Serveur de déploiement', icon: '🖥️', editable: true },
{ key: 'DEPLOYMENT_USER', label: 'Utilisateur déploiement', icon: '👤', editable: true },
{ key: 'DEPLOYMENT_PWD', label: 'Mot de passe déploiement', icon: '🔒', editable: false, secret: true },
{ key: 'TELEGRAM_BOT_TOKEN', label: 'Token Telegram', icon: '🤖', editable: false, secret: true },
{ key: 'TELEGRAM_CHAT_ID', label: 'Chat ID Telegram', icon: '💬', editable: true },
{ key: 'LOG_LEVEL', label: 'Niveau de log', icon: '📊', editable: true },
];
return (
<div className="space-y-6 animate-fade-in">
<h1 className="text-xl font-bold text-white"> Configuration</h1>
{/* Global config */}
<form onSubmit={handleSave} className="glass-card p-6 space-y-4">
<h2 className="text-lg font-bold text-white">🔧 Paramètres généraux</h2>
{message && (
<div className={`text-sm p-3 rounded-lg ${message.startsWith('✅') ? 'bg-green-500/10 text-green-400' : 'bg-red-500/10 text-red-400'}`}>
{message}
</div>
)}
{fields.map(f => (
<div key={f.key}>
<label className="text-xs text-gray-400 uppercase tracking-wider mb-1 flex items-center gap-2">
<span>{f.icon}</span> {f.label}
{f.secret && <span className="text-fox-500 text-[10px]">(protégé)</span>}
</label>
<input
className="input"
value={config[f.key]}
onChange={e => setConfig({ ...config, [f.key]: e.target.value })}
disabled={!f.editable}
type={f.secret ? 'password' : 'text'}
/>
</div>
))}
<div className="pt-2">
<button type="submit" className="btn btn-primary" disabled={saving}>
{saving ? '⏳ Sauvegarde...' : '💾 Sauvegarder'}
</button>
</div>
</form>
{/* Deploy Servers */}
<DeployServerManager />
{/* Git Servers */}
<GitServerManager />
{/* Workflows info */}
<WorkflowsInfo />
</div>
);
}
// ═══════════════════════════════════════════════════════════════════════════════
// Deploy Server Manager
// ═══════════════════════════════════════════════════════════════════════════════
function DeployServerManager() {
const [servers, setServers] = useState<DeployServer[]>([]);
const [showAdd, setShowAdd] = useState(false);
const [form, setForm] = useState({ name: '', host: '', user: 'deploy', password: '', ssh_port: 22, description: '' });
const [error, setError] = useState('');
async function fetchServers() {
try { setServers(await api.listDeployServers()); } catch { /* ignore */ }
}
useEffect(() => { fetchServers(); }, []);
async function handleAdd(e: React.FormEvent) {
e.preventDefault();
setError('');
try {
await api.createDeployServer({
...form,
password: form.password || undefined,
description: form.description || undefined,
});
setForm({ name: '', host: '', user: 'deploy', password: '', ssh_port: 22, description: '' });
setShowAdd(false);
fetchServers();
} catch (err) {
setError(err instanceof Error ? err.message : 'Erreur');
}
}
async function handleDelete(id: number, name: string) {
if (!confirm(`Supprimer le serveur "${name}" ?`)) return;
try {
await api.deleteDeployServer(id);
fetchServers();
} catch (err) {
alert(err instanceof Error ? err.message : 'Erreur');
}
}
return (
<div className="glass-card p-6 space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-bold text-white">🖥 Serveurs de déploiement</h2>
<button className="btn btn-ghost text-xs" onClick={() => setShowAdd(!showAdd)}>
{showAdd ? '✕ Fermer' : ' Ajouter'}
</button>
</div>
{/* Add form */}
{showAdd && (
<form onSubmit={handleAdd} className="p-4 rounded-xl bg-surface-800/50 space-y-3">
{error && <div className="text-red-400 text-sm bg-red-400/10 p-2 rounded-lg">{error}</div>}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
<div>
<label className="text-xs text-gray-400 mb-1 block">Nom</label>
<input className="input" value={form.name} onChange={e => setForm({...form, name: e.target.value})} required placeholder="prod-server" />
</div>
<div>
<label className="text-xs text-gray-400 mb-1 block">Hôte (IP / hostname)</label>
<input className="input" value={form.host} onChange={e => setForm({...form, host: e.target.value})} required placeholder="192.168.1.100" />
</div>
<div>
<label className="text-xs text-gray-400 mb-1 block">Utilisateur SSH</label>
<input className="input" value={form.user} onChange={e => setForm({...form, user: e.target.value})} placeholder="deploy" />
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
<div>
<label className="text-xs text-gray-400 mb-1 block">Mot de passe SSH</label>
<input className="input" type="password" value={form.password} onChange={e => setForm({...form, password: e.target.value})} placeholder="(optionnel)" />
</div>
<div>
<label className="text-xs text-gray-400 mb-1 block">Port SSH</label>
<input className="input" type="number" value={form.ssh_port} onChange={e => setForm({...form, ssh_port: Number(e.target.value)})} />
</div>
<div>
<label className="text-xs text-gray-400 mb-1 block">Description</label>
<input className="input" value={form.description} onChange={e => setForm({...form, description: e.target.value})} placeholder="Serveur production" />
</div>
</div>
<button type="submit" className="btn btn-primary text-sm">💾 Ajouter le serveur</button>
</form>
)}
{/* Server list */}
{servers.length === 0 ? (
<p className="text-gray-500 text-sm">Aucun serveur de déploiement configuré.</p>
) : (
<div className="space-y-2">
{servers.map(s => (
<div key={s.id} className="flex items-center justify-between p-3 rounded-xl bg-surface-800/50">
<div>
<span className="text-white font-semibold">{s.name}</span>
<span className="text-gray-500 text-sm ml-3">{s.user}@{s.host}:{s.ssh_port}</span>
{s.description && <span className="text-gray-600 text-xs ml-3"> {s.description}</span>}
</div>
<button className="text-red-400 hover:text-red-300 text-sm" onClick={() => handleDelete(s.id, s.name)}>🗑 Supprimer</button>
</div>
))}
</div>
)}
</div>
);
}
// ═══════════════════════════════════════════════════════════════════════════════
// Git Server Manager
// ═══════════════════════════════════════════════════════════════════════════════
function GitServerManager() {
const [servers, setServers] = useState<GitServer[]>([]);
const [showAdd, setShowAdd] = useState(false);
const [form, setForm] = useState({ name: '', url: '', token: '', org: 'openclaw', description: '' });
const [error, setError] = useState('');
async function fetchServers() {
try { setServers(await api.listGitServers()); } catch { /* ignore */ }
}
useEffect(() => { fetchServers(); }, []);
async function handleAdd(e: React.FormEvent) {
e.preventDefault();
setError('');
try {
await api.createGitServer({
...form,
token: form.token || undefined,
description: form.description || undefined,
});
setForm({ name: '', url: '', token: '', org: 'openclaw', description: '' });
setShowAdd(false);
fetchServers();
} catch (err) {
setError(err instanceof Error ? err.message : 'Erreur');
}
}
async function handleDelete(id: number, name: string) {
if (!confirm(`Supprimer le serveur Git "${name}" ?`)) return;
try {
await api.deleteGitServer(id);
fetchServers();
} catch (err) {
alert(err instanceof Error ? err.message : 'Erreur');
}
}
return (
<div className="glass-card p-6 space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-bold text-white">🌐 Serveurs Git</h2>
<button className="btn btn-ghost text-xs" onClick={() => setShowAdd(!showAdd)}>
{showAdd ? '✕ Fermer' : ' Ajouter'}
</button>
</div>
{/* Add form */}
{showAdd && (
<form onSubmit={handleAdd} className="p-4 rounded-xl bg-surface-800/50 space-y-3">
{error && <div className="text-red-400 text-sm bg-red-400/10 p-2 rounded-lg">{error}</div>}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
<div>
<label className="text-xs text-gray-400 mb-1 block">Nom</label>
<input className="input" value={form.name} onChange={e => setForm({...form, name: e.target.value})} required placeholder="gitea-local" />
</div>
<div>
<label className="text-xs text-gray-400 mb-1 block">URL</label>
<input className="input" value={form.url} onChange={e => setForm({...form, url: e.target.value})} required placeholder="https://git.example.com" />
</div>
<div>
<label className="text-xs text-gray-400 mb-1 block">Organisation</label>
<input className="input" value={form.org} onChange={e => setForm({...form, org: e.target.value})} placeholder="openclaw" />
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
<div>
<label className="text-xs text-gray-400 mb-1 block">Token API</label>
<input className="input" type="password" value={form.token} onChange={e => setForm({...form, token: e.target.value})} placeholder="(optionnel)" />
</div>
<div>
<label className="text-xs text-gray-400 mb-1 block">Description</label>
<input className="input" value={form.description} onChange={e => setForm({...form, description: e.target.value})} placeholder="Gitea local" />
</div>
</div>
<button type="submit" className="btn btn-primary text-sm">💾 Ajouter le serveur Git</button>
</form>
)}
{/* Server list */}
{servers.length === 0 ? (
<p className="text-gray-500 text-sm">Aucun serveur Git configuré.</p>
) : (
<div className="space-y-2">
{servers.map(s => (
<div key={s.id} className="flex items-center justify-between p-3 rounded-xl bg-surface-800/50">
<div>
<span className="text-white font-semibold">{s.name}</span>
<span className="text-gray-500 text-sm ml-3">{s.url}</span>
<span className="text-gray-600 text-xs ml-2">(org: {s.org})</span>
{s.description && <span className="text-gray-600 text-xs ml-3"> {s.description}</span>}
</div>
<button className="text-red-400 hover:text-red-300 text-sm" onClick={() => handleDelete(s.id, s.name)}>🗑 Supprimer</button>
</div>
))}
</div>
)}
</div>
);
}
// ═══════════════════════════════════════════════════════════════════════════════
// Workflows Info
// ═══════════════════════════════════════════════════════════════════════════════
function WorkflowsInfo() {
const [workflows, setWorkflows] = useState<{ type: string; label: string; path: string; steps: { agent: string; model: string }[] }[]>([]);
useEffect(() => {
api.listWorkflows().then(setWorkflows).catch(() => {});
}, []);
if (workflows.length === 0) return null;
return (
<div className="glass-card p-6">
<h2 className="text-lg font-bold text-white mb-4">🔄 Workflows disponibles</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{workflows.map(wf => (
<div key={wf.type} className="p-4 rounded-xl bg-surface-800/50">
<h3 className="text-white font-semibold mb-1">{wf.label}</h3>
<p className="text-xs text-gray-500 mb-3 font-mono">{wf.path}</p>
<div className="flex items-center gap-1 flex-wrap">
{wf.steps.map((s, i) => (
<div key={i} className="flex items-center gap-1">
<span className="badge bg-fox-600/15 text-fox-400 text-[10px]">{s.agent}</span>
{i < wf.steps.length - 1 && <span className="text-gray-600"></span>}
</div>
))}
</div>
</div>
))}
</div>
</div>
);
}