Fix healthchecks: install curl in backend and frontend images

This commit is contained in:
Agent Zero
2026-05-24 21:54:58 +00:00
commit cdf350c618
52 changed files with 7885 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import api from '../services/api';
import { useAuth } from '../contexts/AuthContext';
const Dashboard = () => {
const [drawings, setDrawings] = useState([]);
const [loading, setLoading] = useState(true);
const { user, logout } = useAuth();
const navigate = useNavigate();
useEffect(() => {
api.get('/drawings').then(res => {
setDrawings(res.data);
setLoading(false);
}).catch(() => setLoading(false));
}, []);
const handleCreate = async () => {
try {
const res = await api.post('/drawings', { name: 'Neue Zeichnung' });
navigate(`/editor/${res.data.id}`);
} catch (err) {
alert('Fehler beim Erstellen');
}
};
const handleDelete = async (id) => {
if (!confirm('Zeichnung wirklich löschen?')) return;
try {
await api.delete(`/drawings/${id}`);
setDrawings(drawings.filter(d => d.id !== id));
} catch (err) {
alert('Fehler beim Löschen');
}
};
if (loading) return <div style={{ padding: 20 }}>Lade...</div>;
return (
<div style={{ maxWidth: 800, margin: '20px auto', padding: 20 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
<h2>Meine Zeichnungen ({user?.email})</h2>
<button onClick={logout} style={{ padding: '8px 16px', backgroundColor: '#ff4d4f', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>Abmelden</button>
</div>
<button onClick={handleCreate} style={{ padding: '10px 20px', backgroundColor: '#1890ff', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer', marginBottom: 20 }}>Neue Zeichnung</button>
{drawings.length === 0 ? (
<p>Keine Zeichnungen vorhanden.</p>
) : (
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr><th style={{ textAlign: 'left', padding: 8, borderBottom: '1px solid #ddd' }}>Name</th><th style={{ textAlign: 'left', padding: 8, borderBottom: '1px solid #ddd' }}>Zuletzt geändert</th><th style={{ textAlign: 'left', padding: 8, borderBottom: '1px solid #ddd' }}>Aktionen</th></tr>
</thead>
<tbody>
{drawings.map(d => (
<tr key={d.id}>
<td style={{ padding: 8, borderBottom: '1px solid #eee' }}>{d.name}</td>
<td style={{ padding: 8, borderBottom: '1px solid #eee' }}>{new Date(d.updatedAt).toLocaleString()}</td>
<td style={{ padding: 8, borderBottom: '1px solid #eee' }}>
<button onClick={() => navigate(`/editor/${d.id}`)} style={{ marginRight: 8, padding: '4px 12px', backgroundColor: '#52c41a', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>Öffnen</button>
<button onClick={() => handleDelete(d.id)} style={{ padding: '4px 12px', backgroundColor: '#ff4d4f', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>Löschen</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
};
export default Dashboard;
+238
View File
@@ -0,0 +1,238 @@
import React, { useState, useEffect, useRef } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import api from '../services/api';
import CADCanvas from '../components/CADCanvas';
import Toolbar from '../components/Toolbar';
import LayerPanel from '../components/LayerPanel';
import BlockLibrary from '../components/BlockLibrary';
import pluginRegistry from '../components/PluginRegistry';
const Editor = () => {
const { id } = useParams();
const navigate = useNavigate();
const canvasRef = useRef(null);
const [drawing, setDrawing] = useState(null);
const [activeTool, setActiveTool] = useState('select');
const [layers, setLayers] = useState([
{ name: 'Layer 0', visible: true, locked: false },
]);
const [activeLayer, setActiveLayer] = useState('Layer 0');
const [showLayers, setShowLayers] = useState(true);
const [showBlocks, setShowBlocks] = useState(true);
const [saveStatus, setSaveStatus] = useState('');
const fileInputRef = useRef(null);
// Load drawing
useEffect(() => {
if (id && id !== 'new') {
api.get(`/drawings/${id}`).then(res => {
setDrawing(res.data);
if (res.data.canvas_json) {
try {
const json = JSON.parse(res.data.canvas_json);
canvasRef.current?.loadFromJSON?.(json);
} catch (e) {
console.warn('Canvas JSON parse error, starting fresh');
}
}
}).catch(err => {
console.error(err);
navigate('/dashboard');
});
} else {
api.post('/drawings', { name: 'Neue Zeichnung' }).then(res => {
setDrawing(res.data);
navigate(`/editor/${res.data.id}`, { replace: true });
}).catch(err => {
console.error(err);
navigate('/dashboard');
});
}
}, [id]);
// Initialize plugin registry when canvas is ready
const handleCanvasReady = (canvas) => {
const cadApp = {
getCanvas: () => canvas,
getApi: () => api,
getDrawing: () => drawing,
registerTool: (id, label, icon, handler) => {
console.log(`Plugin registered tool: ${id}`);
// Would integrate into toolbar dynamically
},
};
pluginRegistry.init(cadApp);
};
// Save canvas JSON to backend
const saveCanvas = async () => {
if (!drawing || !canvasRef.current) return;
try {
setSaveStatus('Speichere...');
const json = canvasRef.current.toJSON();
await api.put(`/drawings/${drawing.id}`, {
canvas_json: JSON.stringify(json),
name: drawing.name,
});
setSaveStatus('Gespeichert ✓');
setTimeout(() => setSaveStatus(''), 2000);
} catch (err) {
setSaveStatus('Fehler ✗');
console.error(err);
}
};
// DXF Import
const handleDxfImport = async (e) => {
const file = e.target.files?.[0];
if (!file) return;
try {
const formData = new FormData();
formData.append('file', file);
const res = await api.post('/dxf/import', formData);
if (res.data?.objects && canvasRef.current) {
canvasRef.current.addObjects(res.data.objects);
setSaveStatus('DXF importiert ✓');
setTimeout(() => setSaveStatus(''), 2000);
}
e.target.value = '';
} catch (err) {
console.error('DXF import error:', err);
setSaveStatus('DXF-Import Fehler ✗');
setTimeout(() => setSaveStatus(''), 3000);
e.target.value = '';
}
};
// DXF Export
const handleDxfExport = async () => {
if (!canvasRef.current) return;
try {
const json = canvasRef.current.toJSON();
const res = await api.post('/dxf/export', { objects: json.objects || [] }, {
responseType: 'blob',
});
const blob = new Blob([res.data], { type: 'application/dxf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = (drawing?.name || 'zeichnung') + '.dxf';
a.click();
URL.revokeObjectURL(url);
setSaveStatus('DXF exportiert ✓');
setTimeout(() => setSaveStatus(''), 2000);
} catch (err) {
console.error('DXF export error:', err);
setSaveStatus('DXF-Export Fehler ✗');
setTimeout(() => setSaveStatus(''), 3000);
}
};
// SVG Export (client-side)
const handleSvgExport = () => {
if (!canvasRef.current) return;
const canvas = canvasRef.current.getCanvas();
if (!canvas) return;
try {
const svg = canvas.toSVG();
const blob = new Blob([svg], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = (drawing?.name || 'zeichnung') + '.svg';
a.click();
URL.revokeObjectURL(url);
setSaveStatus('SVG exportiert ✓');
setTimeout(() => setSaveStatus(''), 2000);
} catch (err) {
console.error('SVG export error:', err);
}
};
const handleDelete = () => {
if (!confirm('Ausgewähltes Objekt löschen?')) return;
const canvas = canvasRef.current?.getCanvas();
if (canvas) {
const active = canvas.getActiveObject();
if (active) {
canvas.remove(active);
canvas.renderAll();
}
}
};
// Keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e) => {
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
saveCanvas();
} else if (e.key === 'Delete' || e.key === 'Backspace') {
handleDelete();
} else if (e.ctrlKey && e.key === 'z') {
// Undo would need command stack
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [drawing]);
return (
<div style={{ display: 'flex', height: '100vh', overflow: 'hidden' }}>
{/* Toolbar */}
<div style={{ width: 50, background: '#2c3e50', color: 'white', display: 'flex', flexDirection: 'column', alignItems: 'center', paddingTop: 10, gap: 10, zIndex: 20 }}>
<Toolbar activeTool={activeTool} setActiveTool={setActiveTool} canvasRef={canvasRef} />
<button
onClick={handleDelete}
title="Löschen"
style={{ width: 36, height: 36, background: '#c0392b', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer', marginTop: 'auto', marginBottom: 10 }}
>
🗑
</button>
</div>
{/* Canvas area */}
<div style={{ flex: 1, position: 'relative', background: '#f0f0f0' }}>
<div style={{ position: 'absolute', top: 10, left: 10, zIndex: 10, display: 'flex', gap: 10, flexWrap: 'wrap' }}>
<button onClick={() => navigate('/dashboard')} style={{ padding: '4px 12px', background: '#1890ff', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}> Zurück</button>
<button onClick={saveCanvas} style={{ padding: '4px 12px', background: '#52c41a', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>Speichern</button>
<button onClick={() => fileInputRef.current?.click()} style={{ padding: '4px 12px', background: '#faad14', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>DXF Import</button>
<button onClick={handleDxfExport} style={{ padding: '4px 12px', background: '#722ed1', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>DXF Export</button>
<button onClick={handleSvgExport} style={{ padding: '4px 12px', background: '#13c2c2', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>SVG Export</button>
<span style={{ padding: '4px 12px', background: 'white', borderRadius: 4, fontSize: 14 }}>{saveStatus}</span>
</div>
<input
type="file"
ref={fileInputRef}
onChange={handleDxfImport}
accept=".dxf"
style={{ display: 'none' }}
/>
<CADCanvas
ref={canvasRef}
activeTool={activeTool}
setActiveTool={setActiveTool}
onCanvasReady={handleCanvasReady}
/>
</div>
{/* Right panels */}
<div style={{ width: 250, background: '#fafafa', borderLeft: '1px solid #ddd', overflowY: 'auto' }}>
<div style={{ padding: 10, borderBottom: '1px solid #ddd', cursor: 'pointer', fontWeight: 'bold', userSelect: 'none' }} onClick={() => setShowLayers(!showLayers)}>
Layer {showLayers ? '▲' : '▼'}
</div>
{showLayers && (
<LayerPanel layers={layers} setLayers={setLayers} activeLayer={activeLayer} setActiveLayer={setActiveLayer} />
)}
<div style={{ padding: 10, borderBottom: '1px solid #ddd', cursor: 'pointer', fontWeight: 'bold', userSelect: 'none' }} onClick={() => setShowBlocks(!showBlocks)}>
Teilebibliothek {showBlocks ? '▲' : '▼'}
</div>
{showBlocks && (
<BlockLibrary canvasRef={canvasRef} />
)}
</div>
</div>
);
};
export default Editor;
+45
View File
@@ -0,0 +1,45 @@
import React, { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const { login } = useAuth();
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
try {
await login(email, password);
navigate('/dashboard');
} catch (err) {
setError(err.response?.data?.error || 'Login fehlgeschlagen');
}
};
return (
<div style={{ maxWidth: 400, margin: '100px auto', padding: 20, background: 'white', borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.15)' }}>
<h2>Web-CAD Anmeldung</h2>
{error && <p style={{ color: 'red' }}>{error}</p>}
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: 12 }}>
<label>E-Mail</label><br />
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required style={{ width: '100%', padding: 8, borderRadius: 4, border: '1px solid #ccc' }} />
</div>
<div style={{ marginBottom: 16 }}>
<label>Passwort</label><br />
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required style={{ width: '100%', padding: 8, borderRadius: 4, border: '1px solid #ccc' }} />
</div>
<button type="submit" style={{ width: '100%', padding: 10, backgroundColor: '#1890ff', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>Anmelden</button>
</form>
<p style={{ marginTop: 12, textAlign: 'center' }}>
Kein Konto? <Link to="/register">Registrieren</Link>
</p>
</div>
);
};
export default Login;
+54
View File
@@ -0,0 +1,54 @@
import React, { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
const Register = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [passwordConfirm, setPasswordConfirm] = useState('');
const [error, setError] = useState('');
const { register } = useAuth();
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
if (password !== passwordConfirm) {
setError('Passwörter stimmen nicht überein');
return;
}
try {
await register(email, password);
navigate('/dashboard');
} catch (err) {
setError(err.response?.data?.error || 'Registrierung fehlgeschlagen');
}
};
return (
<div style={{ maxWidth: 400, margin: '100px auto', padding: 20, background: 'white', borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.15)' }}>
<h2>Registrierung</h2>
{error && <p style={{ color: 'red' }}>{error}</p>}
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: 12 }}>
<label>E-Mail</label><br />
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required style={{ width: '100%', padding: 8, borderRadius: 4, border: '1px solid #ccc' }} />
</div>
<div style={{ marginBottom: 12 }}>
<label>Passwort</label><br />
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required style={{ width: '100%', padding: 8, borderRadius: 4, border: '1px solid #ccc' }} />
</div>
<div style={{ marginBottom: 16 }}>
<label>Passwort bestätigen</label><br />
<input type="password" value={passwordConfirm} onChange={(e) => setPasswordConfirm(e.target.value)} required style={{ width: '100%', padding: 8, borderRadius: 4, border: '1px solid #ccc' }} />
</div>
<button type="submit" style={{ width: '100%', padding: 10, backgroundColor: '#52c41a', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>Registrieren</button>
</form>
<p style={{ marginTop: 12, textAlign: 'center' }}>
Bereits registriert? <Link to="/login">Anmelden</Link>
</p>
</div>
);
};
export default Register;