mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-07-26 18:38:30 +00:00
Update 1.2.4
This commit is contained in:
@@ -1,21 +1,21 @@
|
||||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { Plus, Share, X } from "lucide-react"
|
||||
import { Download, Plus, Share, X } from "lucide-react"
|
||||
|
||||
// ==========================================================
|
||||
// PwaInstallPrompt
|
||||
// ==========================================================
|
||||
// Bottom-sheet shown on mobile when the Monitor is opened in
|
||||
// a browser (not launched as an installed PWA). Two variants:
|
||||
// iOS Safari → manual 3-step instructions
|
||||
// Android → generic "browser menu → Add to Home Screen"
|
||||
// iOS Safari → manual 3-step instructions (no browser API)
|
||||
// Android → native install prompt when the browser
|
||||
// fires `beforeinstallprompt` (Chromium 89+
|
||||
// delivers it based on manifest validity alone,
|
||||
// no Service Worker required), with the manual
|
||||
// "browser menu → Add to Home Screen" steps
|
||||
// always shown below as a fallback.
|
||||
//
|
||||
// No `beforeinstallprompt` handling: the Monitor no longer
|
||||
// registers a Service Worker (see `pwa-register.tsx`), so
|
||||
// Chromium never fires it. Installation is always manual.
|
||||
//
|
||||
// Never shown on desktop, or when already running standalone.
|
||||
// Dismissal options:
|
||||
// "Not now" → temporary, hidden for 30 days
|
||||
// "Don't show again" → permanent (no expiry)
|
||||
@@ -27,6 +27,13 @@ const DISMISSED_FOREVER_KEY = "proxmenux-install-dismissed"
|
||||
const DISMISSED_UNTIL_KEY = "proxmenux-install-dismissed-until"
|
||||
const NOT_NOW_DAYS = 30
|
||||
|
||||
// `BeforeInstallPromptEvent` isn't in the TS DOM lib yet.
|
||||
interface BeforeInstallPromptEvent extends Event {
|
||||
readonly platforms: string[]
|
||||
readonly userChoice: Promise<{ outcome: "accepted" | "dismissed"; platform: string }>
|
||||
prompt(): Promise<void>
|
||||
}
|
||||
|
||||
function isMobileDevice(): boolean {
|
||||
if (typeof window === "undefined") return false
|
||||
// Prefer feature detection (coarse pointer + touch) over UA sniffing,
|
||||
@@ -57,6 +64,7 @@ function isIOS(): boolean {
|
||||
export function PwaInstallPrompt() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [platform, setPlatform] = useState<"ios" | "android" | null>(null)
|
||||
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return
|
||||
@@ -79,6 +87,28 @@ export function PwaInstallPrompt() {
|
||||
setOpen(true)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return
|
||||
const onBeforeInstall = (e: Event) => {
|
||||
// Suppress the browser's own mini-infobar so the Install button
|
||||
// in the bottom sheet is the primary path.
|
||||
e.preventDefault()
|
||||
setDeferredPrompt(e as BeforeInstallPromptEvent)
|
||||
}
|
||||
const onInstalled = () => {
|
||||
// The browser confirms the install landed on the home screen —
|
||||
// close the sheet and drop the deferred event.
|
||||
setDeferredPrompt(null)
|
||||
setOpen(false)
|
||||
}
|
||||
window.addEventListener("beforeinstallprompt", onBeforeInstall)
|
||||
window.addEventListener("appinstalled", onInstalled)
|
||||
return () => {
|
||||
window.removeEventListener("beforeinstallprompt", onBeforeInstall)
|
||||
window.removeEventListener("appinstalled", onInstalled)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleNotNow = useCallback(() => {
|
||||
try {
|
||||
const until = Date.now() + NOT_NOW_DAYS * 24 * 60 * 60 * 1000
|
||||
@@ -107,6 +137,25 @@ export function PwaInstallPrompt() {
|
||||
setOpen(false)
|
||||
}, [])
|
||||
|
||||
const handleInstall = useCallback(async () => {
|
||||
if (!deferredPrompt) return
|
||||
try {
|
||||
await deferredPrompt.prompt()
|
||||
const { outcome } = await deferredPrompt.userChoice
|
||||
// A `beforeinstallprompt` event can only be used once; drop the
|
||||
// reference either way so the button hides.
|
||||
setDeferredPrompt(null)
|
||||
if (outcome === "accepted") {
|
||||
setOpen(false)
|
||||
}
|
||||
} catch {
|
||||
// Browser refused to run the prompt (already handled, race with
|
||||
// appinstalled, etc.) — leave the sheet open so the operator can
|
||||
// fall back to the manual steps rendered below.
|
||||
setDeferredPrompt(null)
|
||||
}
|
||||
}, [deferredPrompt])
|
||||
|
||||
if (!open || !platform) return null
|
||||
|
||||
return (
|
||||
@@ -187,11 +236,33 @@ export function PwaInstallPrompt() {
|
||||
</li>
|
||||
</ol>
|
||||
) : (
|
||||
<div className="mb-4 rounded-lg border border-border bg-muted/50 px-3.5 py-3 text-[13px] leading-relaxed text-muted-foreground">
|
||||
Open the browser menu <b className="text-foreground">⋮</b> →{" "}
|
||||
<b className="text-foreground">Add to Home Screen</b> → confirm by tapping{" "}
|
||||
<b className="text-foreground">Install</b>.
|
||||
</div>
|
||||
<>
|
||||
{deferredPrompt && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleInstall}
|
||||
className="mb-3 flex w-full items-center justify-center gap-2 rounded-xl bg-primary px-4 py-3 text-[14px] font-semibold text-primary-foreground shadow-sm hover:opacity-90 active:opacity-80 transition-opacity"
|
||||
>
|
||||
<Download className="h-4 w-4" aria-hidden="true" />
|
||||
Install
|
||||
</button>
|
||||
)}
|
||||
<div className="mb-4 rounded-lg border border-border bg-muted/50 px-3.5 py-3 text-[13px] leading-relaxed text-muted-foreground">
|
||||
{deferredPrompt ? (
|
||||
<>
|
||||
Or install manually: browser menu <b className="text-foreground">⋮</b> →{" "}
|
||||
<b className="text-foreground">Add to Home Screen</b> → confirm by tapping{" "}
|
||||
<b className="text-foreground">Install</b>.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Open the browser menu <b className="text-foreground">⋮</b> →{" "}
|
||||
<b className="text-foreground">Add to Home Screen</b> → confirm by tapping{" "}
|
||||
<b className="text-foreground">Install</b>.
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="mt-1 flex flex-col gap-1 border-t border-border pt-3">
|
||||
|
||||
@@ -2,13 +2,9 @@
|
||||
|
||||
import { useEffect } from "react"
|
||||
|
||||
// Unregisters any Service Worker registered against this origin on
|
||||
// mount. Having a SW active on the Monitor origin caused a mobile
|
||||
// dashboard-freeze bug over HTTPS + reverse proxy (setInterval polls
|
||||
// stopped firing real fetches under battery throttling). Confirmed
|
||||
// as the sole cause by the affected reporter on Brave and Firefox
|
||||
// Android. `sw.js` is kept in the tree so PWA offline support can
|
||||
// be revisited later without archaeology.
|
||||
// Unregister any Service Worker on this origin at mount. A SW here
|
||||
// interacts badly with mobile battery throttling behind reverse
|
||||
// proxies. `sw.js` is kept for a future PWA-offline revisit.
|
||||
export function PwaRegister() {
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return
|
||||
@@ -19,10 +15,7 @@ export function PwaRegister() {
|
||||
if (regs.length === 0) return
|
||||
return Promise.all(regs.map((r) => r.unregister()))
|
||||
})
|
||||
.catch(() => {
|
||||
// Best-effort — private browsing or a locked-down browser
|
||||
// may reject the API; nothing to do.
|
||||
})
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -405,14 +405,8 @@ const RestoreDetailModal: React.FC<{
|
||||
)
|
||||
}
|
||||
|
||||
// ── Data-pools auto-import block ──────────────────────────────
|
||||
// Rendered inside the RestoreDetailModal (and the compact header
|
||||
// badge on the card) so the operator sees which ZFS data pools
|
||||
// were imported automatically during the restore, which ones ZFS
|
||||
// took with -f because the on-disk hostid didn't match the fresh
|
||||
// install, and which ones were skipped and need manual attention.
|
||||
// The section is populated by backup_host.sh:_rs_persist_datapool_import
|
||||
// and survives the ScriptTerminalModal being closed.
|
||||
// Rendered inside RestoreDetailModal — one row per outcome category
|
||||
// (imported / forced / partial skip / missing skip / failed).
|
||||
const DataPoolsBlock: React.FC<{ section: DataPoolsImport }> = ({ section }) => {
|
||||
const total =
|
||||
section.ok.length +
|
||||
|
||||
@@ -262,6 +262,10 @@ def terminal_websocket(ws):
|
||||
_term_env.setdefault('COLORTERM', 'truecolor')
|
||||
_term_env.setdefault('LANG', 'C.UTF-8')
|
||||
_term_env.setdefault('LC_ALL', 'C.UTF-8')
|
||||
# Inherited by every child of this shell (including `menu`), so the
|
||||
# update path can tell it's running inside a WebSocket-backed session
|
||||
# that would be cut mid-install if the Monitor service restarted.
|
||||
_term_env['PROXMENUX_TERMINAL'] = 'monitor'
|
||||
_term_env.pop('PS1', None)
|
||||
_home = _term_env.get('HOME') or os.path.expanduser('~') or '/root'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user