fix(monitor): welcome dialogs one at a time and a usable terminal on phones

This commit is contained in:
MacRimi
2026-11-05 23:03:05 +01:00
parent e945051f88
commit 5bfc524359
14 changed files with 247 additions and 33 deletions
@@ -96,6 +96,8 @@ const slides: OnboardingSlide[] = [
},
]
export const ONBOARDING_CLOSED_EVENT = "proxmenux-onboarding-closed"
export function OnboardingCarousel() {
const t = useT()
const [open, setOpen] = useState(false)
@@ -110,6 +112,14 @@ export function OnboardingCarousel() {
}
}, [])
// The release notes wait for the tour: two identical dialogs on top of each
// other made closing the first one look as if nothing happened.
const [wasOpen, setWasOpen] = useState(false)
useEffect(() => {
if (open) setWasOpen(true)
else if (wasOpen) window.dispatchEvent(new Event(ONBOARDING_CLOSED_EVENT))
}, [open, wasOpen])
const handleNext = () => {
if (currentSlide < slides.length - 1) {
setDirection("next")
+4 -2
View File
@@ -489,7 +489,9 @@ function MonitorDashboard({ product }: { product: Product }) {
)}
<header
className="border-b border-border bg-card sticky top-0 z-50 shadow-sm cursor-pointer hover:bg-accent/5 transition-colors"
// On a short screen (a phone turned sideways) the header and the bar below
// scroll with the page instead of covering half of it.
className="border-b border-border bg-card sticky [@media(max-height:500px)]:static top-0 z-50 shadow-sm cursor-pointer hover:bg-accent/5 transition-colors"
onClick={() => setShowHealthModal(true)}
>
<div className="container mx-auto px-4 md:px-6 py-4 md:py-4">
@@ -635,7 +637,7 @@ function MonitorDashboard({ product }: { product: Product }) {
</header>
<div
className={`sticky z-40 bg-background
className={`sticky [@media(max-height:500px)]:static z-40 bg-background
top-[120px] lg:top-[76px]
transition-all duration-700 ease-in-out
${showNavigation ? "translate-y-0 opacity-100" : "-translate-y-[120%] opacity-0 pointer-events-none"}
+57 -3
View File
@@ -1,7 +1,7 @@
"use client"
import { useCallback, useEffect, useState } from "react"
import { Plus, Share, X } from "lucide-react"
import { ExternalLink, Plus, Share, X } from "lucide-react"
import { useT } from "../lib/i18n/provider"
// ==========================================================
@@ -30,6 +30,7 @@ import { useT } from "../lib/i18n/provider"
const DISMISSED_FOREVER_KEY = "proxmenux-install-dismissed"
const DISMISSED_UNTIL_KEY = "proxmenux-install-dismissed-until"
const NOT_NOW_DAYS = 30
const HTTPS_DOCS = "https://macrimi.github.io/ProxMenux/docs/monitor/access-auth#https"
function isMobileDevice(): boolean {
if (typeof window === "undefined") return false
@@ -62,6 +63,9 @@ export function PwaInstallPrompt() {
const t = useT()
const [open, setOpen] = useState(false)
const [platform, setPlatform] = useState<"ios" | "android" | null>(null)
// Chrome installs a web app only from a secure origin: over plain HTTP it
// can add a shortcut, never the app. Say so instead of steps that fail.
const [secure, setSecure] = useState(true)
useEffect(() => {
if (typeof window === "undefined") return
@@ -80,8 +84,36 @@ export function PwaInstallPrompt() {
// localStorage unavailable (private mode etc.) — treat as not dismissed.
}
setSecure(window.isSecureContext)
setPlatform(isIOS() ? "ios" : "android")
// A dialog of the dashboard (welcome tour, release notes) blocks pointer
// events on everything outside it, so this prompt would show on top but
// could not be tapped. Wait until no dialog is open, and confirm it a
// moment later, since one dialog can open right after another closes.
const dialogOpen = () => document.querySelector('[role="dialog"][data-state="open"]') !== null
let timer: ReturnType<typeof setTimeout> | undefined
// Dialogs mount in portals directly under <body> and announce their state
// in data-state; the rest of the page changing does not reset the wait.
const portals = new MutationObserver(() => schedule())
const states = new MutationObserver(() => schedule())
const stop = () => {
portals.disconnect()
states.disconnect()
clearTimeout(timer)
}
const schedule = () => {
clearTimeout(timer)
timer = setTimeout(() => {
if (dialogOpen()) return
stop()
setOpen(true)
}, 800)
}
portals.observe(document.body, { childList: true })
states.observe(document.body, { subtree: true, attributes: true, attributeFilter: ["data-state"] })
schedule()
return stop
}, [])
const handleNotNow = useCallback(() => {
@@ -120,6 +152,8 @@ export function PwaInstallPrompt() {
aria-modal="true"
aria-labelledby="pwa-install-title"
className="fixed inset-0 z-[100] flex items-end justify-center bg-black/60 backdrop-blur-sm animate-in fade-in duration-200"
// Stays usable even if another dialog opens and disables pointer events on the page.
style={{ pointerEvents: "auto" }}
onClick={(e) => {
if (e.target === e.currentTarget) handleClose()
}}
@@ -167,7 +201,9 @@ export function PwaInstallPrompt() {
<p className="mt-1 text-[13px] leading-snug text-muted-foreground">
{platform === "ios"
? t("pwaInstall.iosDescription")
: t("pwaInstall.androidDescription")}
: secure
? t("pwaInstall.androidDescription")
: t("pwaInstall.insecure.description")}
</p>
</div>
</div>
@@ -208,12 +244,30 @@ export function PwaInstallPrompt() {
</span>
</li>
</ol>
) : (
) : secure ? (
<div className="mb-4 rounded-lg border border-border bg-muted/50 px-3.5 py-3 text-[13px] leading-relaxed text-muted-foreground">
{t("pwaInstall.android.stepOpenMenu")} <b className="text-foreground">⋮</b> →{" "}
<b className="text-foreground">{t("pwaInstall.installApp")}</b> {t("pwaInstall.android.or")}{" "}
<b className="text-foreground">{t("pwaInstall.addToHomeScreen")}</b> → {t("pwaInstall.android.stepConfirm")}{" "}
<b className="text-foreground">{t("pwaInstall.android.install")}</b>.
</div>
) : (
<div className="mb-4 flex flex-col gap-2 rounded-lg border border-border bg-muted/50 px-3.5 py-3 text-[13px] leading-relaxed text-muted-foreground">
<p>{t("pwaInstall.insecure.howTo")}</p>
<a
href={HTTPS_DOCS}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 font-semibold text-primary hover:underline"
>
{t("pwaInstall.insecure.docs")}
<ExternalLink className="h-3 w-3" aria-hidden="true" />
</a>
<p>
{t("pwaInstall.insecure.shortcut")} <b className="text-foreground">⋮</b> →{" "}
<b className="text-foreground">{t("pwaInstall.addToHomeScreen")}</b>.
</p>
</div>
)}
<div className="mt-1 flex flex-col gap-1 border-t border-border pt-3">
+9 -4
View File
@@ -7,6 +7,7 @@ import { X, Sparkles, Thermometer, Activity, HardDrive, Shield, Globe, Cpu, Zap,
import { Checkbox } from "./ui/checkbox"
import { useT } from "../lib/i18n/provider"
import { APP_VERSION } from "../lib/version"
import { ONBOARDING_CLOSED_EVENT } from "./onboarding-carousel"
interface ReleaseNote {
date: string
@@ -533,11 +534,15 @@ export function useVersionCheck() {
const [showReleaseNotes, setShowReleaseNotes] = useState(false)
useEffect(() => {
const lastSeenVersion = localStorage.getItem("proxmenux-last-seen-version")
if (lastSeenVersion !== APP_VERSION) {
setShowReleaseNotes(true)
if (localStorage.getItem("proxmenux-last-seen-version") === APP_VERSION) return
const show = () => setShowReleaseNotes(true)
if (localStorage.getItem("proxmenux-onboarding-seen")) {
show()
return
}
// The welcome tour opens first; the notes follow once it is closed.
window.addEventListener(ONBOARDING_CLOSED_EVENT, show, { once: true })
return () => window.removeEventListener(ONBOARDING_CLOSED_EVENT, show)
}, [])
return { showReleaseNotes, setShowReleaseNotes }
+77 -6
View File
@@ -19,6 +19,7 @@ import {
ChevronDown,
Copy,
Clipboard,
RotateCw,
} from "lucide-react"
import { copyTerminalSelection, pasteFromClipboard } from "@/lib/terminal-clipboard"
import {
@@ -97,6 +98,10 @@ const proxmoxCommands = [
"scp file user@host:/path", "tail -f /var/log/syslog", "history", "clear",
]
const ROTATE_HINT_KEY = "proxmenux-terminal-rotate-hint-dismissed"
// Toolbar above the terminal and key bar below it, in CSS pixels.
const LANDSCAPE_CHROME_PX = 120
export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onClose }) => {
const t = useT()
const localizedCommands = proxmoxCommands.map((cmd, index) => ({
@@ -109,6 +114,17 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
const [isMobile, setIsMobile] = useState(false)
const [isTablet, setIsTablet] = useState(false)
const [terminalHeight, setTerminalHeight] = useState<number>(500) // altura por defecto en px
const [viewportHeight, setViewportHeight] = useState(0)
const [portrait, setPortrait] = useState(false)
// ProxMenux menus need about 80 columns; a phone held upright gives ~50.
const [rotateHintHidden, setRotateHintHidden] = useState(true)
const panelRef = useRef<HTMLDivElement | null>(null)
// A phone turned sideways uses the tablet layout with little height: keep the
// terminal, its toolbar and its key bar inside the screen.
const fittedHeight = isTablet && viewportHeight
? Math.min(terminalHeight, Math.max(200, viewportHeight - LANDSCAPE_CHROME_PX))
: terminalHeight
const [searchModalOpen, setSearchModalOpen] = useState(false)
const [searchQuery, setSearchQuery] = useState("")
const [filteredCommands, setFilteredCommands] = useState<Array<{ cmd: string; desc: string }>>(localizedCommands)
@@ -129,6 +145,8 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
setIsMobile(width < 768)
setIsTablet(isTouchDevice && isTabletSize)
setViewportHeight(window.innerHeight)
setPortrait(window.innerHeight > width)
}
updateDeviceType()
@@ -139,6 +157,11 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
if (savedHeight) {
setTerminalHeight(Number.parseInt(savedHeight, 10))
}
try {
setRotateHintHidden(localStorage.getItem(ROTATE_HINT_KEY) === "1")
} catch {
setRotateHintHidden(false)
}
return () => {
window.removeEventListener("resize", handleResize)
@@ -417,12 +440,25 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
const container = containerRefs.current[terminal.id]
if (!terminal.term && container) {
initializeTerminal(terminal, container)
} else if (terminal.term?.element && container && terminal.term.element.parentElement !== container) {
// Turning the device switches between the phone and tablet layouts,
// which render new containers: move the open terminal into its new
// place instead of leaving it in the one React removed.
container.appendChild(terminal.term.element)
try {
terminal.fitAddon?.fit()
if (terminal.ws?.readyState === WebSocket.OPEN) {
terminal.ws.send(JSON.stringify({ type: "resize", cols: terminal.term.cols, rows: terminal.term.rows }))
}
} catch (err) {
console.warn("[Terminal] resize after layout change failed:", err)
}
}
})
}, 50)
return () => clearTimeout(timer)
}, [terminals, isMobile])
}, [terminals, isMobile, isTablet])
useEffect(() => {
if (window.innerWidth < 640) return
@@ -449,7 +485,15 @@ export const TerminalPanel: React.FC<TerminalPanelProps> = ({ websocketUrl, onCl
}
}
})
}, [terminalHeight, layout, terminals, isMobile])
}, [terminalHeight, fittedHeight, layout, terminals, isMobile])
useEffect(() => {
// Turned sideways, the dashboard header fills most of a phone screen:
// scroll so the whole terminal is in view.
if (isTablet && !portrait && viewportHeight && viewportHeight < terminalHeight + LANDSCAPE_CHROME_PX) {
panelRef.current?.scrollIntoView({ block: "start", behavior: "smooth" })
}
}, [isTablet, portrait])
const initializeTerminal = async (terminal: TerminalInstance, container: HTMLDivElement) => {
const [TerminalClass, FitAddonClass] = await Promise.all([
@@ -766,8 +810,17 @@ const handleClose = () => {
const activeTerminal = terminals.find((t) => t.id === activeTerminalId)
const dismissRotateHint = () => {
setRotateHintHidden(true)
try {
localStorage.setItem(ROTATE_HINT_KEY, "1")
} catch {
// Best effort: the hint shows again on the next visit.
}
}
return (
<div className="flex flex-col h-full bg-zinc-950 rounded-md overflow-hidden">
<div ref={panelRef} className="flex flex-col h-full bg-zinc-950 rounded-md overflow-hidden">
<div className="flex items-center justify-between px-4 py-2 bg-zinc-900 border-b border-zinc-800">
<div className="flex items-center gap-3">
<Activity className="h-5 w-5 text-blue-500" />
@@ -843,13 +896,30 @@ const handleClose = () => {
</div>
</div>
{isMobile && portrait && !rotateHintHidden && (
<div className="flex items-center gap-2 px-3 py-1.5 bg-zinc-900 border-b border-zinc-800 text-xs text-zinc-400">
<RotateCw className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
<span className="flex-1">{t("terminal.rotateHint")}</span>
<button
type="button"
onClick={dismissRotateHint}
aria-label={t("actions.close")}
className="-my-1.5 -mr-2 flex h-8 w-8 shrink-0 items-center justify-center rounded-full hover:bg-zinc-800 touch-manipulation"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
)}
<div
data-terminal-container
ref={(el) => {
containerRefs.current["main"] = el
}}
className={`overflow-hidden flex flex-col ${isMobile ? "flex-1 h-[60vh]" : "overflow-hidden"} w-full max-w-full`}
style={!isMobile || isTablet ? { height: `${terminalHeight}px`, flexShrink: 0 } : undefined}
// In a column flex parent, flex-1 (basis 0) would override the height
// and the terminal would shrink to xterm's default 24 rows.
className={`overflow-hidden flex flex-col ${isMobile ? `${portrait && !rotateHintHidden ? "h-[calc(60dvh-36px)]" : "h-[60dvh]"} shrink-0` : "overflow-hidden"} w-full max-w-full`}
style={!isMobile || isTablet ? { height: `${fittedHeight}px`, flexShrink: 0 } : undefined}
>
{isMobile ? (
<Tabs value={activeTerminalId} onValueChange={setActiveTerminalId} className="h-full flex flex-col">
@@ -876,7 +946,8 @@ const handleClose = () => {
key={terminal.id}
value={terminal.id}
forceMount
className={`flex-1 h-full mt-0 ${activeTerminalId === terminal.id ? "block" : "hidden"}`}
// Below the tab list: h-full would take the whole container and push the tabs out.
className={`flex-1 min-h-0 mt-0 ${activeTerminalId === terminal.id ? "block" : "hidden"}`}
>
<div
ref={(el) => (containerRefs.current[terminal.id] = el)}
+11 -2
View File
@@ -5516,6 +5516,7 @@
"iosDescription": "Fügen Sie den Monitor für einen schnellen Zugriff zu Ihrem Startbildschirm hinzu.",
"androidDescription": "Fügen Sie den Monitor als App hinzu, damit er wie eine normale Anwendung geöffnet wird.",
"addToHomeScreen": "Zum Startbildschirm hinzufügen",
"installApp": "App installieren",
"ios": {
"stepShareBefore": "Tippen Sie auf",
"share": "Aktie",
@@ -5528,10 +5529,17 @@
"android": {
"stepOpenMenu": "Öffnen Sie das Browsermenü",
"stepConfirm": "durch Antippen bestätigen",
"install": "Installieren"
"install": "Installieren",
"or": "oder"
},
"notNow": "Nicht jetzt",
"neverAgain": "Nicht mehr anzeigen"
"neverAgain": "Nicht mehr anzeigen",
"insecure": {
"description": "Über unverschlüsseltes HTTP kann Chrome nur eine Verknüpfung auf dem Startbildschirm hinzufügen.",
"howTo": "Um den Monitor als App zu installieren, öffnen Sie ihn über HTTPS mit einem gültigen Zertifikat: ein ACME-Zertifikat von Proxmox unter Admin → Sicherheit, ein Reverse-Proxy oder Tailscale.",
"docs": "HTTPS einrichten",
"shortcut": "Bis dahin können Sie eine Verknüpfung hinzufügen:"
}
},
"vmMetrics": {
"title": "Metriken – {name}",
@@ -5681,6 +5689,7 @@
}
},
"terminal": {
"rotateHint": "Drehen Sie das Telefon quer, um die ProxMenux-Menüs vollständig zu sehen.",
"connected": "Verbunden",
"disconnected": "Getrennt",
"terminalCount": "{count} / 4 Terminals",
+11 -2
View File
@@ -5643,6 +5643,7 @@
"iosDescription": "Add the Monitor to your home screen for quick access.",
"androidDescription": "Add the Monitor as an app so it opens like a normal application.",
"addToHomeScreen": "Add to Home Screen",
"installApp": "Install app",
"ios": {
"stepShareBefore": "Tap the",
"share": "Share",
@@ -5655,10 +5656,17 @@
"android": {
"stepOpenMenu": "Open the browser menu",
"stepConfirm": "confirm by tapping",
"install": "Install"
"install": "Install",
"or": "or"
},
"notNow": "Not now",
"neverAgain": "Don’t show again"
"neverAgain": "Don’t show again",
"insecure": {
"description": "Over plain HTTP, Chrome can only add a shortcut to the home screen.",
"howTo": "To install the Monitor as an app, open it over HTTPS with a valid certificate: a Proxmox ACME certificate in Admin → Security, a reverse proxy, or Tailscale.",
"docs": "How to set up HTTPS",
"shortcut": "Meanwhile, you can add a shortcut:"
}
},
"vmMetrics": {
"title": "Metrics — {name}",
@@ -5808,6 +5816,7 @@
}
},
"terminal": {
"rotateHint": "Turn your phone sideways to see the ProxMenux menus in full.",
"connected": "Connected",
"disconnected": "Disconnected",
"terminalCount": "{count} / 4 terminals",
+11 -2
View File
@@ -5669,6 +5669,7 @@
"iosDescription": "Añade el monitor a tu pantalla de inicio para un acceso rápido.",
"androidDescription": "Añade el Monitor como una aplicación para que se abra como una aplicación normal.",
"addToHomeScreen": "Añadir a la pantalla de inicio",
"installApp": "Instalar aplicación",
"ios": {
"stepShareBefore": "Toca el",
"share": "Compartir",
@@ -5681,10 +5682,17 @@
"android": {
"stepOpenMenu": "Abre el menú del navegador",
"stepConfirm": "confirmar tocando",
"install": "Instalar"
"install": "Instalar",
"or": "o"
},
"notNow": "Ahora no",
"neverAgain": "No volver a mostrar"
"neverAgain": "No volver a mostrar",
"insecure": {
"description": "Por HTTP sin cifrar, Chrome solo puede añadir un acceso directo a la pantalla de inicio.",
"howTo": "Para instalar el Monitor como aplicación, ábrelo por HTTPS con un certificado válido: un certificado ACME de Proxmox en Administración → Seguridad, un proxy inverso o Tailscale.",
"docs": "Cómo configurar HTTPS",
"shortcut": "Mientras tanto, puedes añadir un acceso directo:"
}
},
"vmMetrics": {
"title": "Métricas: {name}",
@@ -5834,6 +5842,7 @@
}
},
"terminal": {
"rotateHint": "Gira el móvil para ver completos los menús de ProxMenux.",
"connected": "Conectado",
"disconnected": "Desconectado",
"terminalCount": "{count} / 4 terminales",
+11 -2
View File
@@ -5516,6 +5516,7 @@
"iosDescription": "Ajoutez le moniteur à votre écran d'accueil pour un accès rapide.",
"androidDescription": "Ajoutez le moniteur en tant qu'application pour qu'il s'ouvre comme une application normale.",
"addToHomeScreen": "Ajouter à l'écran d'accueil",
"installApp": "Installer l'application",
"ios": {
"stepShareBefore": "Appuyez sur le",
"share": "Partager",
@@ -5528,10 +5529,17 @@
"android": {
"stepOpenMenu": "Ouvrez le menu du navigateur",
"stepConfirm": "confirmer en appuyant sur",
"install": "Installer"
"install": "Installer",
"or": "ou"
},
"notNow": "Pas maintenant",
"neverAgain": "Ne plus montrer"
"neverAgain": "Ne plus montrer",
"insecure": {
"description": "En HTTP non chiffré, Chrome peut seulement ajouter un raccourci à l'écran d'accueil.",
"howTo": "Pour installer le moniteur comme application, ouvrez-le en HTTPS avec un certificat valide : un certificat ACME de Proxmox dans Administrateur → Sécurité, un proxy inverse ou Tailscale.",
"docs": "Configurer HTTPS",
"shortcut": "En attendant, vous pouvez ajouter un raccourci :"
}
},
"vmMetrics": {
"title": "Métriques — {name}",
@@ -5681,6 +5689,7 @@
}
},
"terminal": {
"rotateHint": "Tournez le téléphone à l'horizontale pour voir les menus de ProxMenux en entier.",
"connected": "Connecté",
"disconnected": "Déconnecté",
"terminalCount": "{count} / 4 bornes",
+11 -2
View File
@@ -5516,6 +5516,7 @@
"iosDescription": "Aggiungi il Monitor alla schermata iniziale per un accesso rapido.",
"androidDescription": "Aggiungi il Monitor come app in modo che si apra come una normale applicazione.",
"addToHomeScreen": "Aggiungi alla schermata Home",
"installApp": "Installa app",
"ios": {
"stepShareBefore": "Tocca il pulsante",
"share": "Condividi",
@@ -5528,10 +5529,17 @@
"android": {
"stepOpenMenu": "Apri il menu del browser",
"stepConfirm": "conferma toccando",
"install": "Installa"
"install": "Installa",
"or": "oppure"
},
"notNow": "Non adesso",
"neverAgain": "Non mostrare più"
"neverAgain": "Non mostrare più",
"insecure": {
"description": "Tramite HTTP non cifrato, Chrome può solo aggiungere una scorciatoia alla schermata Home.",
"howTo": "Per installare il Monitor come app, aprilo tramite HTTPS con un certificato valido: un certificato ACME di Proxmox in Amministratore → Sicurezza, un reverse proxy o Tailscale.",
"docs": "Come configurare HTTPS",
"shortcut": "Nel frattempo, puoi aggiungere una scorciatoia:"
}
},
"vmMetrics": {
"title": "Metriche — {name}",
@@ -5681,6 +5689,7 @@
}
},
"terminal": {
"rotateHint": "Ruota il telefono in orizzontale per vedere per intero i menu di ProxMenux.",
"connected": "Collegato",
"disconnected": "Disconnesso",
"terminalCount": "{count} / 4 terminali",
+11 -2
View File
@@ -5516,6 +5516,7 @@
"iosDescription": "Adicione o monitor à sua tela inicial para acesso rápido.",
"androidDescription": "Adicione o Monitor como um aplicativo para que ele abra como um aplicativo normal.",
"addToHomeScreen": "Adicionar à tela inicial",
"installApp": "Instalar app",
"ios": {
"stepShareBefore": "Toque no",
"share": "Compartilhar",
@@ -5528,10 +5529,17 @@
"android": {
"stepOpenMenu": "Abra o menu do navegador",
"stepConfirm": "confirme tocando",
"install": "Instalar"
"install": "Instalar",
"or": "ou"
},
"notNow": "Agora não",
"neverAgain": "Não mostre novamente"
"neverAgain": "Não mostre novamente",
"insecure": {
"description": "Por HTTP sem criptografia, o Chrome só pode adicionar um atalho à tela inicial.",
"howTo": "Para instalar o Monitor como aplicativo, abra-o por HTTPS com um certificado válido: um certificado ACME do Proxmox em Administrador → Segurança, um proxy reverso ou o Tailscale.",
"docs": "Como configurar o HTTPS",
"shortcut": "Enquanto isso, você pode adicionar um atalho:"
}
},
"vmMetrics": {
"title": "Métricas — {name}",
@@ -5681,6 +5689,7 @@
}
},
"terminal": {
"rotateHint": "Gire o celular na horizontal para ver os menus do ProxMenux por completo.",
"connected": "Conectado",
"disconnected": "Desconectado",
"terminalCount": "{count} / 4 terminais",
+11 -2
View File
@@ -5515,6 +5515,7 @@
"iosDescription": "Pridajte Monitor na plochu, aby bol rýchlo poruke.",
"androidDescription": "Pridajte Monitor ako aplikáciu, aby sa otváral ako bežná appka.",
"addToHomeScreen": "Pridať na plochu",
"installApp": "Inštalovať aplikáciu",
"ios": {
"stepShareBefore": "Ťuknite na",
"share": "Zdieľať",
@@ -5527,10 +5528,17 @@
"android": {
"stepOpenMenu": "Otvorte menu prehliadača",
"stepConfirm": "potom potvrďte ťuknutím na",
"install": "Inštalovať"
"install": "Inštalovať",
"or": "alebo"
},
"notNow": "Teraz nie",
"neverAgain": "Už nezobrazovať"
"neverAgain": "Už nezobrazovať",
"insecure": {
"description": "Cez nešifrované HTTP môže Chrome pridať na plochu iba odkaz.",
"howTo": "Ak chcete Monitor nainštalovať ako aplikáciu, otvorte ho cez HTTPS s platným certifikátom: certifikát ACME z Proxmoxu v Správa → Bezpečnosť, reverzný proxy alebo Tailscale.",
"docs": "Ako nastaviť HTTPS",
"shortcut": "Zatiaľ môžete pridať odkaz:"
}
},
"vmMetrics": {
"title": "Metriky — {name}",
@@ -5680,6 +5688,7 @@
}
},
"terminal": {
"rotateHint": "Otočte telefón na šírku, aby ste videli celé ponuky ProxMenux.",
"connected": "Pripojené",
"disconnected": "Odpojené",
"terminalCount": "{count} zo 4 terminálov",
+11 -2
View File
@@ -5516,6 +5516,7 @@
"iosDescription": "Lägg till monitorn på din startskärm för snabb åtkomst.",
"androidDescription": "Lägg till Monitor som en app så att den öppnas som en vanlig applikation.",
"addToHomeScreen": "Lägg till på startskärmen",
"installApp": "Installera app",
"ios": {
"stepShareBefore": "Tryck på",
"share": "Dela",
@@ -5528,10 +5529,17 @@
"android": {
"stepOpenMenu": "Öppna webbläsarmenyn",
"stepConfirm": "bekräfta genom att trycka på",
"install": "Installera"
"install": "Installera",
"or": "eller"
},
"notNow": "Inte nu",
"neverAgain": "Visa inte igen"
"neverAgain": "Visa inte igen",
"insecure": {
"description": "Över okrypterad HTTP kan Chrome bara lägga till en genväg på startskärmen.",
"howTo": "För att installera Monitor som en app, öppna den över HTTPS med ett giltigt certifikat: ett ACME-certifikat från Proxmox under Administration → Säkerhet, en omvänd proxy eller Tailscale.",
"docs": "Så konfigurerar du HTTPS",
"shortcut": "Under tiden kan du lägga till en genväg:"
}
},
"vmMetrics": {
"title": "Mätvärden — {name}",
@@ -5681,6 +5689,7 @@
}
},
"terminal": {
"rotateHint": "Vrid telefonen till liggande läge för att se ProxMenux-menyerna i sin helhet.",
"connected": "Ansluten",
"disconnected": "Frånkopplad",
"terminalCount": "{count} / 4 terminaler",
@@ -545,7 +545,7 @@ curl -X DELETE http://<host>:8008/api/auth/api-tokens/<token-id> \\
{t.rich("apiTokens.outro", { apiLink, intLink })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("https.heading")}</h2>
<h2 id="https" className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("https.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("https.intro")}</p>
<ol className="list-decimal pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{httpsItems.map((_, idx) => (