feat(manager): show LAN waiter/manager addresses and pairing QR in App Info
App Info now lists 'Σερβιτόροι (WiFi)' http://<LAN IP> with a QR code encoding http://<LAN IP>/?pair=<site_id> (phone camera opens the waiter app; the native app will read the same code) and 'Διαχείριση (WiFi)' http://<LAN IP>/manager. The LAN IP comes from HOST_IP, else from the dashboard's own address when opened by IP; otherwise a hint to set HOST_IP. QRModal takes an optional caption; the legacy waiter-domain QR is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -302,7 +302,7 @@ function DataTransferSection() {
|
||||
}
|
||||
|
||||
|
||||
function QRModal({ url, onClose }) {
|
||||
function QRModal({ url, caption = 'Σαρώστε με το κινητό για σύνδεση στο σύστημα.', onClose }) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" onClick={onClose}>
|
||||
<div
|
||||
@@ -314,15 +314,29 @@ function QRModal({ url, onClose }) {
|
||||
<div className="p-3 bg-white border border-gray-200 rounded-xl">
|
||||
<QRCodeSVG value={url} size={220} includeMargin={false} />
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 text-center">
|
||||
Σαρώστε με το κινητό για σύνδεση στο σύστημα.
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 text-center">{caption}</p>
|
||||
<button onClick={onClose} className="btn w-full text-sm">Κλείσιμο</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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.
|
||||
function resolveLanIp(status) {
|
||||
if (status?.lan_ip) return status.lan_ip
|
||||
const host = window.location.hostname
|
||||
return IPV4.test(host) && !host.startsWith('127.') ? host : null
|
||||
}
|
||||
|
||||
// Pairing URL: opens the waiter app when scanned with the phone camera, and the
|
||||
// native app reads the same code (?pair=<site_id>) to pair with this venue.
|
||||
function waiterPairUrl(lanIp, siteId) {
|
||||
return `http://${lanIp}/` + (siteId ? `?pair=${encodeURIComponent(siteId)}` : '')
|
||||
}
|
||||
|
||||
function formatUptime(seconds) {
|
||||
const h = Math.floor(seconds / 3600)
|
||||
const m = Math.floor((seconds % 3600) / 60)
|
||||
@@ -334,7 +348,7 @@ export default function AppInfoTab() {
|
||||
const user = useAuthStore(s => s.user)
|
||||
const qc = useQueryClient()
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [qrOpen, setQrOpen] = useState(false)
|
||||
const [qr, setQr] = useState(null) // { url, caption } | null
|
||||
const { data: status, isLoading } = useQuery({
|
||||
queryKey: ['system-status'],
|
||||
queryFn: () => client.get('/api/system/status').then(r => r.data),
|
||||
@@ -414,13 +428,46 @@ export default function AppInfoTab() {
|
||||
<div className="font-medium text-gray-800">{new Date(status.expires_at).toLocaleDateString('el-GR')}</div>
|
||||
</>
|
||||
)}
|
||||
{(() => {
|
||||
const lanIp = resolveLanIp(status)
|
||||
if (!lanIp) {
|
||||
return (
|
||||
<>
|
||||
<div className="text-gray-500">Τοπική πρόσβαση</div>
|
||||
<div className="text-xs text-amber-700">
|
||||
Άγνωστη IP του server — ορίστε <span className="font-mono">HOST_IP</span> στο .env
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
const pairUrl = waiterPairUrl(lanIp, status?.site_id)
|
||||
return (
|
||||
<>
|
||||
<div className="text-gray-500">Σερβιτόροι (WiFi)</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-medium text-gray-800 text-xs font-mono break-all">http://{lanIp}</span>
|
||||
<button
|
||||
onClick={() => setQr({
|
||||
url: pairUrl,
|
||||
caption: 'Σαρώστε με την κάμερα του κινητού — ανοίγει την εφαρμογή σερβιτόρου. Το κινητό πρέπει να είναι στο ίδιο WiFi.',
|
||||
})}
|
||||
className="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"
|
||||
>
|
||||
QR Code
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-gray-500">Διαχείριση (WiFi)</div>
|
||||
<div className="font-medium text-gray-800 text-xs font-mono break-all">http://{lanIp}/manager</div>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
{status?.waiter_domain && (
|
||||
<>
|
||||
<div className="text-gray-500">Waiter Domain</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-medium text-gray-800 text-xs font-mono break-all">{status.waiter_domain}</span>
|
||||
<button
|
||||
onClick={() => setQrOpen(true)}
|
||||
onClick={() => setQr({ url: status.waiter_domain })}
|
||||
className="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"
|
||||
>
|
||||
QR Code
|
||||
@@ -450,9 +497,7 @@ export default function AppInfoTab() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{qrOpen && status?.waiter_domain && (
|
||||
<QRModal url={status.waiter_domain} onClose={() => setQrOpen(false)} />
|
||||
)}
|
||||
{qr && <QRModal url={qr.url} caption={qr.caption} onClose={() => setQr(null)} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user