feat: add Agrarmonitor integration module

- New backend module (agrarmonitor) with status check and device registration
- Frontend settings tab with connection status display and registration form
- Environment variables for base URLs, credentials, cookie path and encryption key
- Docker Compose env passthrough for agrarmonitor config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 21:30:46 +02:00
parent f482304061
commit 1f5dcf4a17
10 changed files with 881 additions and 4 deletions
+16
View File
@@ -176,3 +176,19 @@ export const INBOX_ACTION_LABELS: Record<InboxActionType, string> = {
EXPORT: 'Export (FTP/WebDAV)',
PAPERLESS: 'In Paperless importieren',
};
export interface AgrarmonitorStatusData {
connected: boolean;
registriert: boolean | null;
freigeschaltet: boolean | null;
error?: string;
}
export const agrarmonitorApi = {
getStatus: () =>
api.get<AgrarmonitorStatusData>('/api/agrarmonitor/status').then((r) => r.data),
registerDevice: (pcName: string, agrarmonitorId: string) =>
api
.post<{ success: boolean; message: string }>('/api/agrarmonitor/register', { pcName, agrarmonitorId })
.then((r) => r.data),
};
+131 -1
View File
@@ -8,7 +8,7 @@ import {
UserOutlined, FileTextOutlined, ThunderboltOutlined,
PlusOutlined, DeleteOutlined, EditOutlined, CloudUploadOutlined,
HistoryOutlined, MinusCircleOutlined, CopyOutlined, KeyOutlined,
QrcodeOutlined, UnorderedListOutlined, PrinterOutlined,
QrcodeOutlined, UnorderedListOutlined, PrinterOutlined, GlobalOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import type { FormInstance } from 'antd';
@@ -19,6 +19,7 @@ import {
type SettingPostprocessingLog, type FilterGroup, type FilterCondition,
INBOX_ACTION_LABELS,
type InboxAction, type InboxActionType,
agrarmonitorApi, type AgrarmonitorStatusData,
} from '../api/settings';
import { clientsApi, type Client } from '../api/inbox';
import { apiKeysApi, type ApiKey } from '../api/api-keys';
@@ -2221,6 +2222,130 @@ function BarcodeTemplatesTab() {
}
// ═══════════════════════════════════════════════════════════════════
// Agrarmonitor Tab
// ═══════════════════════════════════════════════════════════════════
function AgrarmonitorTab() {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [registering, setRegistering] = useState(false);
const [status, setStatus] = useState<AgrarmonitorStatusData | null>(null);
const [registerResult, setRegisterResult] = useState<{ success: boolean; message: string } | null>(null);
const handleLoadStatus = async () => {
setLoading(true);
setRegisterResult(null);
try {
const data = await agrarmonitorApi.getStatus();
setStatus(data);
} catch {
setStatus({ connected: false, registriert: null, freigeschaltet: null, error: 'Netzwerkfehler' });
} finally {
setLoading(false);
}
};
const handleRegister = async () => {
const values = await form.validateFields();
setRegistering(true);
setRegisterResult(null);
try {
const result = await agrarmonitorApi.registerDevice(values.pcName, values.agrarmonitorId);
setRegisterResult(result);
if (result.success) {
message.success('Gerät erfolgreich registriert');
await handleLoadStatus();
}
} catch {
setRegisterResult({ success: false, message: 'Registrierung fehlgeschlagen' });
} finally {
setRegistering(false);
}
};
const renderStatusTag = (value: boolean | null, labelTrue: string, labelFalse: string) => {
if (value === null) return <Tag></Tag>;
return value
? <Tag color="success">{labelTrue}</Tag>
: <Tag color="error">{labelFalse}</Tag>;
};
return (
<div style={{ maxWidth: 600 }}>
<Typography.Title level={4}>Agrarmonitor</Typography.Title>
<Typography.Paragraph type="secondary">
Verbindungsstatus und Geräte-Registrierung für die Agrarmonitor-Schnittstelle.
Zugangsdaten werden in der <code>.env</code> konfiguriert.
</Typography.Paragraph>
<Space direction="vertical" style={{ width: '100%' }}>
<div>
<Button loading={loading} onClick={handleLoadStatus}>
Status abrufen
</Button>
</div>
{status && (
<Card size="small" title="Status">
<Space direction="vertical">
<div>
<strong>Verbindung: </strong>
{status.connected
? <Tag color="success">Verbunden</Tag>
: <Tag color="error">Nicht verbunden</Tag>}
</div>
<div>
<strong>Registriert: </strong>
{renderStatusTag(status.registriert, 'Ja', 'Nein')}
</div>
<div>
<strong>Freigeschaltet: </strong>
{renderStatusTag(status.freigeschaltet, 'Ja', 'Nein')}
</div>
{status.error && (
<div style={{ color: '#ff4d4f' }}>{status.error}</div>
)}
</Space>
</Card>
)}
{status?.registriert === false && (
<Card size="small" title="Gerät registrieren">
<Form form={form} layout="vertical">
<Form.Item
name="pcName"
label="PC-Name"
rules={[{ required: true, message: 'Bitte PC-Name eingeben' }]}
>
<Input placeholder="BUERO-PC-01" />
</Form.Item>
<Form.Item
name="agrarmonitorId"
label="Agrarmonitor-ID / Firma"
rules={[{ required: true, message: 'Bitte Agrarmonitor-ID eingeben' }]}
>
<Input placeholder="Agrarmonitor-ID" />
</Form.Item>
<Button type="primary" loading={registering} onClick={handleRegister}>
Gerät registrieren
</Button>
</Form>
{registerResult && (
<div style={{ marginTop: 12 }}>
<Tag color={registerResult.success ? 'success' : 'error'}>
{registerResult.message}
</Tag>
</div>
)}
</Card>
)}
</Space>
</div>
);
}
// ═══════════════════════════════════════════════════════════════════
// Settings Page
// ═══════════════════════════════════════════════════════════════════
@@ -2277,6 +2402,11 @@ export default function SettingsPage() {
label: <span><KeyOutlined /> API-Keys</span>,
children: <ApiKeysTab />,
},
{
key: 'agrarmonitor',
label: <span><GlobalOutlined /> Agrarmonitor</span>,
children: <AgrarmonitorTab />,
},
]}
/>
</Card>