From 676a7258071f5d23e6797a48ee96d019b19b46bc Mon Sep 17 00:00:00 2001 From: bonamin Date: Mon, 28 Sep 2026 16:13:17 +0300 Subject: [PATCH] =?UTF-8?q?feat(manager):=20"=CE=A4=CE=BF=CF=80=CE=B9?= =?UTF-8?q?=CE=BA=CF=8C=20=CE=B4=CE=AF=CE=BA=CF=84=CF=85=CE=BF"=20card=20-?= =?UTF-8?q?=20server=20IP=20source,=20override=20editor,=20DHCP-change=20w?= =?UTF-8?q?arning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit App Info gets its own LAN card: the IP phones use and where it comes from (manual / automatic detection / .env / browser address), an inline editor to pin it or return to automatic, a note when detection is unavailable (Docker Desktop, helper stopped/stale), and an amber warning with a one-click fix when the pinned address is no longer this machine's. Waiter URL + pairing QR and manager URL moved here from the System grid. Co-Authored-By: Claude Opus 5.5 --- .../src/pages/Settings/tabs/AppInfoTab.jsx | 164 ++++++++++++++---- 1 file changed, 127 insertions(+), 37 deletions(-) diff --git a/manager_dashboard/src/pages/Settings/tabs/AppInfoTab.jsx b/manager_dashboard/src/pages/Settings/tabs/AppInfoTab.jsx index 3a858df..1d97833 100644 --- a/manager_dashboard/src/pages/Settings/tabs/AppInfoTab.jsx +++ b/manager_dashboard/src/pages/Settings/tabs/AppInfoTab.jsx @@ -323,12 +323,133 @@ function QRModal({ url, caption = 'Σαρώστε με το κινητό για const IPV4 = /^\d{1,3}(\.\d{1,3}){3}$/ -// Address phones use on the LAN. HOST_IP from the server wins; otherwise, if this -// dashboard was itself opened by IP (http://192.168.x.x:8081), that IP is the server. +// Address phones use on the LAN. The server resolves it (manual override → +// live detection → HOST_IP, see local_backend/services/lan_ip.py); if it knows +// nothing and this dashboard was itself opened by IP, that IP is the server. function resolveLanIp(status) { - if (status?.lan_ip) return status.lan_ip + if (status?.lan_ip) return { ip: status.lan_ip, source: status.lan_ip_info?.source ?? 'env' } const host = window.location.hostname - return IPV4.test(host) && !host.startsWith('127.') ? host : null + return IPV4.test(host) && !host.startsWith('127.') ? { ip: host, source: 'browser' } : { ip: null, source: null } +} + +const LAN_SOURCE_LABEL = { + override: 'Χειροκίνητη ρύθμιση', + detected: 'Αυτόματη ανίχνευση', + env: 'Από το .env (HOST_IP)', + browser: 'Από τη διεύθυνση του browser', +} + +const DETECTION_NOTE = { + unsupported: 'Η αυτόματη ανίχνευση λειτουργεί μόνο σε Linux server (όχι σε Docker Desktop).', + stale: 'Η αυτόματη ανίχνευση σταμάτησε να ενημερώνεται — ελέγξτε το container netinfo.', + unavailable: 'Η αυτόματη ανίχνευση δεν είναι διαθέσιμη (το container netinfo δεν τρέχει).', +} + +const SMALL_BTN = 'flex items-center gap-1 h-6 px-2 rounded-md border border-gray-300 bg-white text-gray-600 text-xs font-medium hover:bg-gray-50 transition-colors flex-shrink-0 disabled:opacity-50' + +function LanAccessSection({ status, onShowQr }) { + const qc = useQueryClient() + const info = status?.lan_ip_info + const { ip, source } = resolveLanIp(status) + const [editing, setEditing] = useState(false) + const [value, setValue] = useState('') + + const saveMut = useMutation({ + mutationFn: newIp => client.put('/api/system/lan-ip-override', { ip: newIp }).then(r => r.data), + onSuccess: (_, newIp) => { + toast.success(newIp ? 'Η IP αποθηκεύτηκε' : 'Επαναφορά σε αυτόματη ανίχνευση') + setEditing(false) + qc.invalidateQueries({ queryKey: ['system-status'] }) + }, + onError: err => toast.error(err.response?.data?.detail || 'Αποτυχία αποθήκευσης'), + }) + + function startEdit() { + setValue(info?.override || ip || '') + setEditing(true) + } + + return ( +
+

Τοπικό δίκτυο

+ + {info?.mismatch && ( +
+

+ Χρησιμοποιείται η IP {ip}, όμως ο server έχει τώρα + την {info.detected}. Τα κινητά και το QR δεν θα + συνδέονται μέχρι να διορθωθεί. +

+ +
+ )} + +
+
IP server
+
+ {editing ? ( +
+ setValue(e.target.value)} + onKeyDown={e => e.key === 'Enter' && value.trim() && saveMut.mutate(value.trim())} + placeholder={info?.detected || '192.168.1.50'} + className="input h-7 text-xs font-mono w-36" + autoFocus + /> + + {info?.override && ( + + )} + +
+ ) : ( +
+ {ip ?? '—'} + {source && ( + {LAN_SOURCE_LABEL[source]} + )} + +
+ )} + {!editing && info && info.detection !== 'ok' && ( +

{DETECTION_NOTE[info.detection]}

+ )} + {!ip && !editing && ( +

+ Άγνωστη IP — ορίστε την εδώ (π.χ. 192.168.1.50) ή HOST_IP στο .env. +

+ )} +
+ + {ip && ( + <> +
Σερβιτόροι (WiFi)
+
+ http://{ip} + +
+
Διαχείριση (WiFi)
+
http://{ip}/manager
+ + )} +
+
+ ) } // Pairing URL: opens the waiter app when scanned with the phone camera, and the @@ -428,39 +549,6 @@ export default function AppInfoTab() {
{new Date(status.expires_at).toLocaleDateString('el-GR')}
)} - {(() => { - const lanIp = resolveLanIp(status) - if (!lanIp) { - return ( - <> -
Τοπική πρόσβαση
-
- Άγνωστη IP του server — ορίστε HOST_IP στο .env -
- - ) - } - const pairUrl = waiterPairUrl(lanIp, status?.site_id) - return ( - <> -
Σερβιτόροι (WiFi)
-
- http://{lanIp} - -
-
Διαχείριση (WiFi)
-
http://{lanIp}/manager
- - ) - })()} {status?.waiter_domain && ( <>
Waiter Domain
@@ -478,6 +566,8 @@ export default function AppInfoTab() { + +