mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-14 18:56:52 +00:00
refine post-install and hardware GPU docs, Monitor UX and CLI styling
- rewrite the 15 post-install pages and the 3 hardware GPU pages so they reflect the current scripts (reversibility, tracked-tool counts, kernel parameters, per-tool commands, Alpine LXC propagation flow) - migrate the legacy step-badge helper on post-install/optional and create-vm/synology to the canonical pill component, with the stepLabel key added in each locale - fix rich-text i18n calls missing helpers across network, automated, optional, security, customization and the post-install landing pages, and escape the `<iface>` placeholder in automated so intl no longer parses it as a tag - remove the mouse-follow blue overlay from the docs landing layout - reposition the App-tab Edit button and stack the Search and Register controls vertically on mobile - move the Bulk update Configure/Edit control into the section header so it behaves the same on desktop and mobile - show a spinner during the final autoremove/autoclean pass of update-pve-safe so the cleanup step reads as active instead of silent - restyle the shell spinner and msg_info in a distinctive purple and drop the unused msg_lang duplicate - add a web-docs i18n build script and its CI workflow, plus tests for the pushover notification channel
This commit is contained in:
@@ -2402,12 +2402,13 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
|
||||
detections, so the user gets one-click Restore before hand-
|
||||
typing a custom app. */}
|
||||
{apps.length > 0 && (
|
||||
<div className="flex flex-wrap justify-end items-center gap-2">
|
||||
<div className="flex flex-col items-stretch gap-2 max-w-xs mx-auto sm:flex-row sm:flex-wrap sm:justify-end sm:items-center sm:max-w-none sm:mx-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={searchInstalledApplications}
|
||||
disabled={searchingApplications || editMode}
|
||||
className="w-full sm:w-auto order-2 sm:order-1"
|
||||
>
|
||||
{searchingApplications
|
||||
? <Loader2 className="h-4 w-4 mr-1.5 animate-spin" />
|
||||
@@ -2421,6 +2422,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
|
||||
size="sm"
|
||||
onClick={openBrowseOrEditor}
|
||||
disabled={editMode}
|
||||
className="w-full sm:w-auto order-3 sm:order-2"
|
||||
>
|
||||
<PlusCircle className="h-4 w-4 mr-1.5" />
|
||||
{t("vmLxc.appEditor.addAnotherApplication")}
|
||||
@@ -2433,7 +2435,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditMode((v) => !v)}
|
||||
className="h-9 px-3 text-sm rounded-md border border-border bg-background hover:bg-muted transition-colors inline-flex items-center gap-1.5"
|
||||
className="h-9 px-3 text-sm rounded-md border border-border bg-background hover:bg-muted transition-colors inline-flex items-center justify-center gap-1.5 w-full sm:w-auto order-1 sm:order-3"
|
||||
>
|
||||
{editMode ? (
|
||||
<>
|
||||
|
||||
@@ -30,6 +30,12 @@ interface ChannelConfig {
|
||||
url?: string
|
||||
token?: string
|
||||
webhook_url?: string
|
||||
// Pushover channel fields
|
||||
user_key?: string
|
||||
api_token?: string
|
||||
device?: string
|
||||
sound?: string
|
||||
critical_priority?: boolean
|
||||
// Email channel fields
|
||||
host?: string
|
||||
port?: string
|
||||
@@ -146,7 +152,7 @@ function validateGotifyUrl(url: string): { error?: string; warning?: string } {
|
||||
|
||||
const EVENT_CATEGORIES = ["vm_ct", "backup", "resources", "storage", "network", "security", "cluster", "services", "health", "updates", "other"].map(key => ({ key }))
|
||||
|
||||
const CHANNEL_TYPES = ["telegram", "gotify", "discord", "email", "apprise"] as const
|
||||
const CHANNEL_TYPES = ["telegram", "gotify", "discord", "email", "pushover", "apprise"] as const
|
||||
|
||||
const AI_PROVIDERS = [
|
||||
{
|
||||
@@ -242,6 +248,7 @@ const DEFAULT_CONFIG: NotificationConfig = {
|
||||
gotify: { enabled: false },
|
||||
discord: { enabled: false },
|
||||
email: { enabled: false },
|
||||
pushover: { enabled: false, critical_priority: true },
|
||||
apprise: { enabled: false },
|
||||
},
|
||||
event_categories: {
|
||||
@@ -256,6 +263,7 @@ const DEFAULT_CONFIG: NotificationConfig = {
|
||||
gotify: { categories: {}, events: {} },
|
||||
discord: { categories: {}, events: {} },
|
||||
email: { categories: {}, events: {} },
|
||||
pushover: { categories: {}, events: {} },
|
||||
apprise: { categories: {}, events: {} },
|
||||
},
|
||||
ai_enabled: false,
|
||||
@@ -287,6 +295,7 @@ const DEFAULT_CONFIG: NotificationConfig = {
|
||||
gotify: "brief",
|
||||
discord: "brief",
|
||||
email: "detailed",
|
||||
pushover: "brief",
|
||||
apprise: "brief",
|
||||
},
|
||||
hostname: "",
|
||||
@@ -1070,6 +1079,8 @@ export function NotificationSettings() {
|
||||
gt_token: { channel: "gotify", field: "token" },
|
||||
dc_hook: { channel: "discord", field: "webhook_url" },
|
||||
em_pass: { channel: "email", field: "password" },
|
||||
po_user: { channel: "pushover", field: "user_key" },
|
||||
po_token: { channel: "pushover", field: "api_token" },
|
||||
apprise_url: { channel: "apprise", field: "url" },
|
||||
}
|
||||
|
||||
@@ -1429,7 +1440,7 @@ export function NotificationSettings() {
|
||||
|
||||
<div className="rounded-lg border border-border/50 bg-muted/20 p-3">
|
||||
<Tabs defaultValue="telegram" className="w-full">
|
||||
<TabsList className="w-full grid grid-cols-5 h-8">
|
||||
<TabsList className="w-full grid grid-cols-3 sm:grid-cols-6 h-auto">
|
||||
<TabsTrigger value="telegram" className="text-xs data-[state=active]:text-blue-500">
|
||||
Telegram
|
||||
</TabsTrigger>
|
||||
@@ -1442,6 +1453,9 @@ export function NotificationSettings() {
|
||||
<TabsTrigger value="email" className="text-xs data-[state=active]:text-amber-500">
|
||||
Email
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="pushover" className="text-xs data-[state=active]:text-rose-500">
|
||||
Pushover
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="apprise" className="text-xs data-[state=active]:text-cyan-500">
|
||||
Apprise
|
||||
</TabsTrigger>
|
||||
@@ -1876,10 +1890,145 @@ export function NotificationSettings() {
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* Pushover */}
|
||||
<TabsContent value="pushover" className="space-y-3 pt-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-xs font-medium">{t("settings.notifications.ui.enablePushover")}</Label>
|
||||
<a
|
||||
href="https://pushover.net/apps/build"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-[10px] text-rose-500 hover:text-rose-400 hover:underline"
|
||||
>
|
||||
{t("settings.notifications.ui.setupGuide")}
|
||||
<ExternalLink className="h-2.5 w-2.5" />
|
||||
</a>
|
||||
</div>
|
||||
<button
|
||||
className={`relative w-9 h-[18px] rounded-full transition-colors ${
|
||||
config.channels.pushover?.enabled ? "bg-blue-600" : "bg-muted-foreground/20 border border-muted-foreground/40"
|
||||
} ${!editMode ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`}
|
||||
onClick={() => { if (editMode) updateChannel("pushover", "enabled", !config.channels.pushover?.enabled) }}
|
||||
disabled={!editMode}
|
||||
role="switch"
|
||||
aria-checked={config.channels.pushover?.enabled || false}
|
||||
>
|
||||
<span className={`absolute top-[1px] left-[1px] h-4 w-4 rounded-full bg-white shadow transition-transform ${
|
||||
config.channels.pushover?.enabled ? "translate-x-[18px]" : "translate-x-0"
|
||||
}`} />
|
||||
</button>
|
||||
</div>
|
||||
{config.channels.pushover?.enabled && (
|
||||
<>
|
||||
<p className="text-[10px] text-muted-foreground leading-relaxed">
|
||||
{t("settings.notifications.ui.pushoverCredentialsHint")}
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<div className="space-y-1.5 min-w-0">
|
||||
<Label className="text-[11px] text-muted-foreground">{t("settings.notifications.ui.pushoverUserKey")}</Label>
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<Input
|
||||
type={showSecrets["po_user"] ? "text" : "password"}
|
||||
className={`h-7 text-xs font-mono min-w-0 flex-1 ${!editMode ? "opacity-50" : ""}`}
|
||||
placeholder="uQiRzpo4DXghDmr9QzzfQu27cmVRsG"
|
||||
value={secretValue("po_user", config.channels.pushover?.user_key || "")}
|
||||
onChange={e => updateChannel("pushover", "user_key", e.target.value)}
|
||||
disabled={!editMode}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="h-7 w-7 shrink-0 flex items-center justify-center rounded-md border border-border hover:bg-muted text-muted-foreground"
|
||||
onClick={() => toggleSecret("po_user")}
|
||||
>
|
||||
{showSecrets["po_user"] ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5 min-w-0">
|
||||
<Label className="text-[11px] text-muted-foreground">{t("settings.notifications.ui.pushoverApiToken")}</Label>
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<Input
|
||||
type={showSecrets["po_token"] ? "text" : "password"}
|
||||
className={`h-7 text-xs font-mono min-w-0 flex-1 ${!editMode ? "opacity-50" : ""}`}
|
||||
placeholder="azGDORePK8gMaC0QOYAMyEEuzJnyUi"
|
||||
value={secretValue("po_token", config.channels.pushover?.api_token || "")}
|
||||
onChange={e => updateChannel("pushover", "api_token", e.target.value)}
|
||||
disabled={!editMode}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="h-7 w-7 shrink-0 flex items-center justify-center rounded-md border border-border hover:bg-muted text-muted-foreground"
|
||||
onClick={() => toggleSecret("po_token")}
|
||||
>
|
||||
{showSecrets["po_token"] ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[11px] text-muted-foreground">{t("settings.notifications.ui.pushoverDevice")} ({t("settings.notifications.ui.optional")})</Label>
|
||||
<Input
|
||||
className={`h-7 text-xs font-mono ${!editMode ? "opacity-50" : ""}`}
|
||||
placeholder="iphone"
|
||||
value={config.channels.pushover?.device || ""}
|
||||
onChange={e => updateChannel("pushover", "device", e.target.value)}
|
||||
disabled={!editMode}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[11px] text-muted-foreground">{t("settings.notifications.ui.pushoverSound")} ({t("settings.notifications.ui.optional")})</Label>
|
||||
<Input
|
||||
className={`h-7 text-xs font-mono ${!editMode ? "opacity-50" : ""}`}
|
||||
placeholder="pushover"
|
||||
value={config.channels.pushover?.sound || ""}
|
||||
onChange={e => updateChannel("pushover", "sound", e.target.value)}
|
||||
disabled={!editMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3 py-1">
|
||||
<div>
|
||||
<Label className="text-xs font-medium">{t("settings.notifications.ui.pushoverCriticalPriority")}</Label>
|
||||
<p className="text-[10px] text-muted-foreground">{t("settings.notifications.ui.pushoverCriticalPriorityDescription")}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={config.channels.pushover?.critical_priority !== false}
|
||||
disabled={!editMode}
|
||||
className={`relative w-9 h-[18px] shrink-0 rounded-full transition-colors ${
|
||||
!editMode ? "opacity-50 cursor-not-allowed" : "cursor-pointer"
|
||||
} ${config.channels.pushover?.critical_priority !== false ? "bg-blue-600" : "bg-muted-foreground/20 border border-muted-foreground/40"}`}
|
||||
onClick={() => { if (editMode) updateChannel("pushover", "critical_priority", config.channels.pushover?.critical_priority === false) }}
|
||||
>
|
||||
<span className={`absolute top-[1px] left-[1px] h-4 w-4 rounded-full bg-white shadow transition-transform ${
|
||||
config.channels.pushover?.critical_priority !== false ? "translate-x-[18px]" : "translate-x-0"
|
||||
}`} />
|
||||
</button>
|
||||
</div>
|
||||
{renderChannelCategories("pushover")}
|
||||
{renderQuietHours("pushover")}
|
||||
{renderDailyDigest("pushover")}
|
||||
<div className="flex items-center gap-2 pt-2 border-t border-border/50">
|
||||
<button
|
||||
className="h-7 px-3 text-xs rounded-md bg-rose-600 hover:bg-rose-700 text-white transition-colors flex items-center gap-1.5 disabled:opacity-50"
|
||||
onClick={() => handleTest("pushover")}
|
||||
disabled={testing === "pushover" || !config.channels.pushover?.user_key || !config.channels.pushover?.api_token}
|
||||
>
|
||||
{testing === "pushover" ? <Loader2 className="h-3 w-3 animate-spin" /> : <TestTube2 className="h-3 w-3" />}
|
||||
{t("settings.notifications.ui.sendTest")}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* Apprise — issue #207. Single URL talks to ~80
|
||||
notification services. The operator pastes one
|
||||
`tgram://`, `discord://`, `ntfy://`, `matrix://`,
|
||||
`pushover://` etc. URL and the AppriseChannel
|
||||
`pover://` etc. URL and the AppriseChannel
|
||||
backend handles the transport. Mirrors the same
|
||||
Enable toggle + Test button pattern as the other
|
||||
channels. */}
|
||||
@@ -1888,7 +2037,7 @@ export function NotificationSettings() {
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-xs font-medium">{t("settings.notifications.ui.enableApprise")}</Label>
|
||||
<a
|
||||
href="https://github.com/caronc/apprise/wiki"
|
||||
href="https://appriseit.com/services/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[10px] text-cyan-500 hover:text-cyan-400 hover:underline"
|
||||
@@ -1946,11 +2095,11 @@ export function NotificationSettings() {
|
||||
<code className="text-foreground/80 mx-0.5">slack://</code>,
|
||||
<code className="text-foreground/80 mx-0.5">ntfy://</code>,
|
||||
<code className="text-foreground/80 mx-0.5">matrix://</code>,
|
||||
<code className="text-foreground/80 mx-0.5">pushover://</code>,
|
||||
<code className="text-foreground/80 mx-0.5">pover://</code>,
|
||||
<code className="text-foreground/80 mx-0.5">mailto://</code>… {t("settings.notifications.ui.seeThe")}
|
||||
{" "}
|
||||
<a
|
||||
href="https://github.com/caronc/apprise/wiki"
|
||||
href="https://appriseit.com/services/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-cyan-500 hover:underline"
|
||||
|
||||
@@ -5764,14 +5764,22 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
{t("vmLxc.bulkUpdate.description")}
|
||||
</p>
|
||||
</div>
|
||||
{!bulkEditMode && bulkConfigured && (
|
||||
{!bulkEditMode && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBulkEditMode(true)}
|
||||
onClick={() => {
|
||||
if (!bulkConfigured) {
|
||||
setBulkTargets(["os"])
|
||||
setBulkError(null)
|
||||
}
|
||||
setBulkEditMode(true)
|
||||
}}
|
||||
className="h-8 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors inline-flex items-center gap-1.5 flex-shrink-0"
|
||||
>
|
||||
<Settings2 className="h-3.5 w-3.5" />
|
||||
{t("vmLxc.bulkUpdate.edit")}
|
||||
{bulkConfigured
|
||||
? t("vmLxc.bulkUpdate.edit")
|
||||
: t("vmLxc.bulkUpdate.configure")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -5932,22 +5940,8 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t("vmLxc.bulkUpdate.notConfigured")}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setBulkTargets(["os"])
|
||||
setBulkEditMode(true)
|
||||
setBulkError(null)
|
||||
}}
|
||||
className="h-8 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors inline-flex items-center justify-center gap-1.5 flex-shrink-0"
|
||||
>
|
||||
<Settings2 className="h-3.5 w-3.5" />
|
||||
{t("vmLxc.bulkUpdate.configure")}
|
||||
</button>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t("vmLxc.bulkUpdate.notConfigured")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user