feat: install.sh One-Liner-Bootstrap + System-Adressen-Card auf IP-Page

scripts/install.sh: full curl-Onliner für Debian 13 trixie analog
mail-gateway/scripts/install.sh — OS+Arch-Detection, Pre-flight-
Tools, GPG-Key (nmg.asc, geteilt mit mail-gateway), APT-Source-Line
trixie main, apt install edgeguard, Service-Smoke + healthz-Probe.
Bestimmungsort: get.netcell-edgeguard.de (Hosting separat).

UI: IP-Adressen-Page bekommt eine "Adressen am Kernel"-Card oben,
analog zur Networks-Page. Listet jede vom Kernel sichtbare IP
(lo + eth0 + …) mit Family-Tag (IPv4/IPv6) — read-only. Verwaltete
Adressen darunter wie zuvor. User-Feedback: "die bestehenden
IP-Adressen werden nicht angezeigt" — adressiert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-09 16:23:58 +02:00
parent ca03e69637
commit 4f6b7b34fc
4 changed files with 258 additions and 17 deletions

View File

@@ -1,5 +1,5 @@
import { useState } from 'react'
import { Button, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Typography, message } from 'antd'
import { Button, Card, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Typography, message } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
@@ -19,6 +19,20 @@ interface IPAddress {
updated_at: string
}
interface SystemInterface {
ifname: string
flags?: string[]
link_type?: string
addr_info?: Array<{ family: 'inet' | 'inet6'; local: string; prefixlen: number }>
}
interface SystemAddress {
ifname: string
family: 'inet' | 'inet6'
address: string
prefix: number
}
interface IPFormValues {
interface_id: number
address: string
@@ -42,12 +56,38 @@ async function listIfaces(): Promise<NetworkInterface[]> {
return (r.data.data as { interfaces?: NetworkInterface[] }).interfaces ?? []
}
// Flatten /system/interfaces into one row per (ifname, address) so
// the operator sees every kernel-side IP at a glance — including
// addresses that EdgeGuard hasn't taken under management yet.
async function listSystemAddresses(): Promise<SystemAddress[]> {
const r = await apiClient.get('/system/interfaces')
if (!isEnvelope(r.data)) return []
const ifs = (r.data.data as { interfaces?: SystemInterface[] }).interfaces ?? []
const out: SystemAddress[] = []
for (const i of ifs) {
for (const a of i.addr_info ?? []) {
out.push({
ifname: i.ifname,
family: a.family,
address: a.local,
prefix: a.prefixlen,
})
}
}
return out
}
export default function IPAddressesPage() {
const { t } = useTranslation()
const qc = useQueryClient()
const { data: ips, isLoading } = useQuery({ queryKey: ['ip-addresses'], queryFn: listIPs })
const { data: ifs } = useQuery({ queryKey: ['network-interfaces'], queryFn: listIfaces })
const { data: sysAddrs } = useQuery({
queryKey: ['system', 'addresses'],
queryFn: listSystemAddresses,
refetchInterval: 60_000,
})
const ifaceLabel = (id: number) => {
const i = ifs?.find((x) => x.id === id)
@@ -122,6 +162,27 @@ export default function IPAddressesPage() {
<div>
<Typography.Title level={3}>{t('ips.title')}</Typography.Title>
<Typography.Paragraph type="secondary">{t('ips.intro')}</Typography.Paragraph>
<Card title={t('ips.systemDiscovered')} size="small" style={{ marginBottom: 16 }}>
{(sysAddrs ?? []).length === 0
? <Typography.Text type="secondary"></Typography.Text>
: (
<Table
size="small"
rowKey={(r) => `${r.ifname}-${r.address}`}
dataSource={sysAddrs ?? []}
pagination={false}
columns={[
{ title: t('ips.interface'), dataIndex: 'ifname', key: 'ifname', render: (s: string) => <code>{s}</code> },
{ title: t('ips.address'), key: 'addr', render: (_, row: SystemAddress) => <code>{row.address}/{row.prefix}</code> },
{ title: t('ips.family'), dataIndex: 'family', key: 'family', render: (f: string) => <Tag>{f === 'inet' ? 'IPv4' : 'IPv6'}</Tag> },
]}
/>
)
}
</Card>
<Typography.Title level={5} style={{ marginTop: 8 }}>{t('ips.managedTitle')}</Typography.Title>
<Button type="primary" style={{ marginBottom: 16 }} onClick={() => {
setCreating(true); form.resetFields()
form.setFieldsValue({ prefix: 24, is_vip: false, active: true })