mirror of
https://github.com/community-scripts/ProxmoxVE-Local.git
synced 2026-05-11 02:11:41 -04:00
* Add GeneratorTab and UI refactor/styles
Introduce a full-featured GeneratorTab component for building and exporting script configuration (script selector, CPU/RAM/Disk sliders, advanced networking/features, generate/copy/export/import, and execute). Apply visual and structural UI refactors: new glass-card styles across ScriptCard, ScriptCardList, ScriptDetailModal, Terminal and Footer; update button variants and interaction classes; add Heart icon and extra footer links; update layout to use Manrope + JetBrains Mono fonts, sticky navbar, ambient backgrounds and metadata tweaks. Wire GeneratorTab into the main page and add related icon imports. Misc: small code-style/formatting adjustments and improved deriveScriptPath / handler signatures for clarity.
* Add script notes, server presets & APT proxy
Add persistent script notes and server presets, plus APT proxy settings and various UI/behavior improvements.
- DB: new migration adds script_notes and server_presets tables and indexes; Prisma schema updated with ScriptNote and ServerPreset models.
- Notes: new client component ScriptNotesPanel with CRUD using a new TRPC router (scriptNotes) and integration into ScriptDetailModal to show/manage private/shared notes.
- APT proxy: new Next.js API route /api/settings/apt-proxy that reads/writes .env vars; GeneralSettingsModal and ConfigurationModal load/save the proxy and pre-fill advanced install vars.
- UX/UI: add copyable install command, version badges on script cards/detail, container ID input in advanced configuration, and styling tweaks (glass-card-static).
- Server/script detection: installedScripts router now attempts to resolve the actual Proxmox node name via SSH and relaxes ID parsing to numeric-only.
- Notifications: appriseService extended to support Gotify endpoints and allow gotify:// scheme, and URL validation adjusted accordingly.
These changes enable user-maintained notes/presets, persistent APT-cacher defaults, improved install UX, better server detection, and additional notification backend support.
* Add server presets and silent batch updates
Introduce server presets and silent batch update support across API, server, and UI. Added a new serverPresets TRPC router and wired it into the API root. ConfigurationModal now lets users save/load/delete presets tied to a server. InstalledScriptsTab adds a "Silent update" option, a sequential "Update All Containers" batch flow that runs updates with PHS_SILENT=1, and passes envVars into update executions. ScriptExecutionHandler (server.js) now accepts envVars for updates and injects export commands into both local and SSH update flows so environment flags (e.g. PHS_SILENT) are honored. Also added "updated" sort handling in ScriptsGrid, DownloadedScriptsTab and FilterBar, and small typing fixes in appriseService.
* Use per-router Prisma client with Better SQLite3
Replace usage of the shared prisma import with per-router PrismaClient instances configured with @prisma/adapter-better-sqlite3. Add getNotesDb/getPresetsDb helpers (with DATABASE_URL fallback) and update scriptNotes and serverPresets to use the new client. Also add error handling in read endpoints to return empty arrays on DB failures and ensure create/update/delete use the new client.
* Icon-only action buttons; use presets DB
Convert HelpButton, ResyncButton, ServerSettingsButton and SettingsButton from labeled outline controls to compact ghost icon-only buttons with aria-labels and smaller icons/spinners for a more compact header UI. Simplify ResyncButton markup (smaller spinner/SVG, removed extra contextual text/lastSync layout) and tweak sync message styling. Add Server icon import in ServerSettingsButton. In serverPresets router, replace direct prisma calls with getPresetsDb() and use the returned db instance for all queries and mutations to support the presets DB connection.
* Add quick filters, Dev/ARM badges, and CSS tweaks
Introduce quick filter support and visual indicators for developer/ARM scripts. Added QuickFilter type and quickFilter to FilterState (getDefaultFilters/mergeFiltersWithDefaults) and a quick-filter UI in FilterBar (All, New, Updated, In Dev, ARM). Implemented filtering logic in ScriptsGrid and added category dev counts to surface per-category "dev" counts in CategorySidebar. Added DevBadge and ArmBadge components, used in ScriptCard and ScriptDetailModal; ScriptCard also gets a subtle violet highlight when script.is_dev is true. Fixed script path generation in GeneratorTab to include a `scripts/` prefix. Minor dark-theme adjustments to .glass-card/.glass-card-static background and border-color for improved contrast.
* Add appearance settings and persist UI prefs
Introduce an Appearance tab in the Settings modal to control theme, text size and layout width (default vs wide). Preferences are saved to localStorage and applied immediately via helper functions (applyTextSize/applyLayoutWidth) and a small inline script injected into <head> to apply saved settings on first paint. Add CSS utility classes for text-size variants and wire up theme buttons (using lucide icons). Also include minor cleanup: optional chaining fix in CategorySidebar and removal of an unused import in ResyncButton.
* Normalize quotes and format components
Apply consistent formatting across several components: convert single quotes to double quotes, reformat the QuickFilter union type and quick-filters array in FilterBar, adjust JSX prop and className string formatting in HelpButton, ScriptCard, ServerSettingsButton, and SettingsButton. These are cosmetic/formatting changes only and do not alter runtime behavior.
* Refactor Badge & CategorySidebar types/styles
Normalize formatting and strengthen typings for Badge; convert single quotes to double, expand variant/type unions, and tidy JSX/props and helper badge components. Rework CategorySidebar: refactor CategoryIcon SVGs for readability, standardize className usage, improve collapsed-state layout and buttons, and ensure categories are filtered/sorted by count with correct icon mapping and counts display. Overall cleanup improves consistency, readability, and type safety across these components.
* Add modal stacking and portal support
Introduce a modal stacking system and portal to ensure modals escape parent stacking contexts. ModalStackProvider now computes a zIndex for each registered modal and returns an unregister handle; useRegisterModal returns the zIndex. A ModalPortal component (createPortal to document.body) was added and all modal components were updated to: capture the returned zIndex, wrap their markup in <ModalPortal>, and apply the dynamic zIndex instead of a hardcoded z-50 class. This improves correct layering of multiple modals and avoids backdrop-filter / transform stacking issues.
* Wrap modals in portal and use zIndex
Use ModalPortal and zIndex values from useRegisterModal across modal components to avoid hardcoded z-50 stacking. Updated ExecutionModeModal, LXCSettingsModal, PublicKeyModal, ReleaseNotesModal, SetupModal, StorageSelectionModal, and TextViewer to import ModalPortal, capture the zIndex returned by useRegisterModal, wrap modal markup in <ModalPortal>, and apply style={{ zIndex }} to backdrops; LXC result overlay uses zIndex + 10 for proper stacking. Also changed LoadingOverlay in VersionDisplay to render with createPortal(document.body) and added the import. These changes centralize stacking behavior and prevent z-index conflicts when multiple modals/overlays are present.
* Use categoryIconColorMap for sidebar icons
Update CategorySidebar.tsx to derive non-selected icon color classes from categoryIconColorMap for both the "template" and per-category icons. Selected icons still use text-primary; fallbacks preserve previous muted and group-hover classes to retain existing styling when a mapping is absent.
* Standardize modal layout and form styles
Refactor modal markup across many components to use a consistent wrapper structure and unified Tailwind utility ordering (moved zIndex to outer container, standardized backdrop and centering). Clean up form layouts: align icons, labels and inputs, normalize spacing, error styling, and button variants (including icon/ghost adjustments). Improve AuthModal and CloneCountInputModal UX (loading/error states, closer placement, validation), and normalize BackupWarningModal and other modal presentations. Update ConfigurationModal advanced UI: reorganize network fields and add/handle extra options (IPv6 static, gateway, MTU, MAC, VLAN, DNS), refine presets save/cancel flow, and apply general formatting/whitespace cleanups.
* Escape env var values; update favicons & logo
Fix shell escaping when building env export commands by escaping backslashes and quotes (prevents broken exported values) in ScriptExecutionHandler (two locations). Update app metadata to point to favicon files under /favicon, add site manifest, and replace the header Package icon with a next/image using the android-chrome PNG (import Image and adjust markup) for consistent asset handling.
* Add favicon set and logo; remove old favicon
Replace legacy root favicon.png with a full favicon set under public/favicon (android-chrome 192/512, apple-touch-icon, favicon-16x16, favicon-32x32, favicon.ico, favicon.png) and add site.webmanifest. Also add public/logo.png. This organizes favicon assets for multiple platforms and adds a webmanifest for PWA/icon declarations.
* Portalize script dropdown and add outside click
Render the script selector dropdown into document.body via createPortal to escape stacking/overflow contexts. Add triggerRef/dropdownRef, compute fixed position from the trigger on open, and attach an outside-click handler to close the dropdown. Also import useRef/useEffect and createPortal; preserve existing search, selection, and reset behaviors while adjusting z-index/positioning.
* Update favicon and logo assets
Replace favicon and logo image files with updated versions to refresh branding. Updated files: public/favicon/android-chrome-192x192.png, public/favicon/android-chrome-512x512.png, public/favicon/apple-touch-icon.png, public/favicon/favicon-16x16.png, public/favicon/favicon-32x32.png, public/favicon/favicon.ico, public/logo.png.
* Add appearance modal & improve script detail UI
Add an AppearanceModal and AppearanceButton to let users change theme, text size and layout width (persisted in localStorage). Enhance FilterBar with category filtering (selectedCategory, dropdown UI, counts, and outside-click handling). Heavily refactor ScriptDetailModal: layout and visual refresh, new icons, better loading/update/delete/install flows, memoized install command, improved copy behavior, keyboard navigation between scripts (using orderedSlugs + onSelectSlug), clearer status/messages, and various performance/UX tweaks. Wire DownloadedScriptsTab to pass orderedSlugs and onSelectSlug into the ScriptDetailModal. Overall small UI/UX and state-management improvements across components.
* Optimize rendering, lazy-load tabs, add sync modal
Performance and UX improvements: memoize computed values in InstalledScriptsTab (scriptsWithStatus, filteredScripts, uniqueServers) and use useCallback/useMemo in ScriptsGrid to avoid unnecessary re-renders. Memoize ScriptCard and ScriptCardList components and tighten their ScriptCard type alias. Introduce a new SyncModal (ResyncButton) component to run/resync scripts with a progress UI and retry/close behavior, and replace the previous ResyncButton import in page.tsx. Lazy-load heavy tab components (Downloaded, Installed, Backups, Generator) with next/dynamic and add a TabSkeleton loader; consolidate tab definitions into a memoized tabs array. Misc: small prop/prop-name fixes and minor JSX formatting adjustments.
* Add Arcane script, status UI, validation & cache
Multiple changes improving UX, reliability, and CI permissions:
- Add scripts/tools/addon/arcane.sh: installer/update/uninstall helper for Arcane (Docker Compose), creates update helper and generates secrets.
- Add ServerStatusIndicator component and integrate into header (src/app/_components/ServerStatusIndicator.tsx, src/app/page.tsx); adjust hero/layout to accommodate inline version + status.
- Enhance GeneratorTab (src/app/_components/GeneratorTab.tsx): fetch full script details by slug, derive and apply default resource values from install_methods, and show an App Defaults info panel.
- Harden repo provider detection (src/server/lib/repositoryUrlValidation.js/.ts): use URL parsing for hostname matching with safe fallback to 'custom'.
- Tune client caching (src/trpc/query-client.ts): increase staleTime to 30min, set gcTime to 1h, and disable automatic refetches on mount/window focus/reconnect.
- Update GitHub Actions permissions: node.js.yml grants contents: read; release-drafter.yml grants contents: write and pull-requests: read.
These changes aim to provide better default resource handling for scripts, visible server reachability, more robust URL parsing, improved client-side cache behavior, and explicit workflow permissions.
* Add suppressHydrationWarning to <html>
Add the React prop suppressHydrationWarning to the <html> element in RootLayout (src/app/layout.tsx) to suppress hydration mismatch warnings. This helps avoid noisy console warnings when server-rendered markup and client rendering differ (e.g., due to dynamic font class injection or other runtime-only differences).
* Refactor GeneratorTab layout and add SSH imports
Rework GeneratorTab rendering and small UI/format fixes: tidy the scriptDetail useMemo formatting and restructure the Script Defaults block so defaults (CPU, RAM, HDD) are always shown and OS/version/variant badges render correctly. Minor formatting change to ServerStatusIndicator's fetch call and a small spacing fix on the home page title. Also add imports for getSSHService and the Server type to servers router in preparation for SSH-related functionality.
* Add explicit typing for servers in indicator
Annotate the `servers` variable with an explicit array type (id, name, ip, online) and cast `data?.servers` to that type, defaulting to an empty array. This clarifies TypeScript inference and prevents `servers` from being undefined for downstream logic without changing runtime behavior.
* perf/cleanup: audit fixes - hoist iconMap, pin Next.js, strip console.log, lighten glass-card, fix dev badge
* Refactor icons, remove debug logs, pin Next
Replace the large inline SVG icon map with a compact iconPaths mapping and fallback path, simplifying CategoryIcon rendering. Tighten the dev-count badge markup/styles for better layout. Remove leftover console.debug/log statements from InstalledScriptsTab and LXCSettingsModal. Pin Next.js version to 16.2.1 in package.json (deps and devDeps). Slightly adjust dark glass-card background opacity/hover values in globals.css.
* refactor: migrate server TS files to structured logger, accessibility improvements
* perf: fix 14s page load - remove SSH from initial query, defer tab-specific data
- getAllInstalledScripts: removed SSH batchDetectContainerTypes, use DB-only
heuristic (lxc_config presence) for VM/LXC detection
- Defer installedScripts + backups queries until their tab is activated
- Initial batch now only fires 3 fast DB queries instead of 7 (incl. SSH)
* perf: split SSH queries from tRPC batch, skip save-on-mount
- Use splitLink to route servers.checkServersStatus through a separate
non-batched httpLink. SSH queries no longer block fast DB queries from
returning (was causing 14s+ batch response holding all data hostage).
- Add initialization refs in ScriptsGrid and DownloadedScriptsTab so
save-filter/view-mode POST effects skip their first fire after load,
eliminating 2-4 unnecessary network requests per tab mount.
* refactor: restore SSH detection for installed scripts, cache badge counts
- Restore batchDetectContainerTypes SSH calls in getAllInstalledScripts
(shows real VM/LXC state, not just DB heuristic)
- Route getAllInstalledScripts through splitLink (non-batched) so SSH
never blocks fast DB queries
- Cache installed/backups badge counts in localStorage so tabs show
last-known counts instantly, updated when fresh data arrives
- Query still deferred until tab is visited (no eager SSH on page load)
* Refactor init checks, footer, and parallel fetch
Make several small refactors and updates across the app:
- Expand single-line early-return checks into multi-line blocks in DownloadedScriptsTab and ScriptsGrid for clarity when skipping initial effect triggers.
- Update Footer links: add separate GitHub (ProxmoxVE-Local) and GitHub (ProxmoxVE) buttons and change the site link to community-scripts.org.
- Minor formatting tweaks in page.tsx (useState initializer and backups ternary) for readability.
- In the scripts API router, fetch script cards and metadata in parallel via Promise.all to reduce latency and return both together.
These changes improve readability and slightly optimize the scripts fetch path.
* Add in-memory PB cache and invalidate on resync
Introduce a simple server-side in-memory cache for PocketBase data with a 10-minute TTL to reduce PB requests for rarely-changing data. Adds getCached/setCache helpers and a shared _cache store, and exports invalidatePbCache() to clear cached entries. Apply caching to getScriptCards, getCategories, and getScriptTypes, and call invalidatePbCache() in the resyncScripts mutation so fresh data is fetched after a resync.
* Update react.tsx
* Show script download flow & per-node server status
GeneratorTab: add detection of locally downloaded scripts (query + mutation), compute a slug set for O(1) lookup, and gate selection/execution on download state. Update script list UI to indicate downloadable items, grayscale/opacity non-downloaded entries, and show a download confirmation modal that triggers loadScript mutation and invalidates cache. Add Loader2 and AlertTriangle icons and disable Execute when the selected script isn't downloaded.
ServerStatusIndicator: change from a single aggregate dot to per-node indicators showing each host name and status (green online with ping animation, red offline). Improve handling of loading vs no-configured-servers states and update tooltip/title text for clarity.
* Improve updater script and minor UI tweaks
Refactor update.sh to make the updater more robust and flexible: add support for specifying a target release (get_release), include tar in dependency checks, strengthen download/extract logic and error logging, improve source-dir detection, and tighten backup/restore, install/build and service start/rollback flows. Also normalize indentation and logging output and add clearer diagnostic messages on failures. Minor UI cleanups in TSX: simplify GeneratorTab conditional formatting and adjust ServerStatusIndicator class ordering.
* Bump deps and add VSCode Next.js prompt setting
Update multiple dependencies to newer patch/minor versions (Prisma, @tanstack/react-query, axios, better-sqlite3, dotenv, lucide-react, next, react, react-dom, vite, vitest tooling, eslint, jsdom, postcss, prettier, prisma, etc.). Replace legacy typescript-eslint package with @typescript-eslint/eslint-plugin and @typescript-eslint/parser. Add .vscode/settings.json to mark WillLuke.nextjs.hasPrompted as true to suppress the Next.js prompt in VS Code.
* Normalize indentation in core scripts
Reformat scripts/core/alpine-install.func and scripts/core/api.func for consistent indentation and whitespace. Changes are formatting-only (no logic changes): converted leading tabs to spaces, aligned multiline blocks, and adjusted a comment reference (error-handler → error_handler) for consistency. Improves readability and maintainability without affecting behavior.
* Show/hide install command UI and service fixes
UI: Add a Terminal icon, show/hide toggle, and display block for the install command in ScriptDetailModal (new showCommand state and button).
Server: installedScripts - default unknown container types to LXC (safe default) and simplify VM/LXC detection comments.
AppriseService: support gotifys:// (HTTPS) and gotify:// formats, select protocol correctly, and update URL regex.
BackupService: include PBS namespace (--ns) when storage.namespace is configured so proxmox-backup-client commands include the namespace.
ScriptDownloader: add fallback to attempt downloading a ct/<slug>.sh when install_methods is empty for CT/LXC scripts, improve returned success/message semantics, better 404-aware error messaging, and check for fallback CT script presence when scanning downloaded files.
Other: add /examples to .gitignore and remove restore.log.
These changes improve robustness for missing install metadata, add HTTPS Gotify support, ensure PBS namespace usage, and improve UX for viewing install commands.
* Prefer local install scripts; add envVars & token
Allow using local copies of helper/install scripts during development and forward environment variables and a GitHub token through the UI and websocket layer.
Changes:
- scripts/core/alpine-install.func & scripts/core/install.func: try to load misc/tools.func from the local scripts directory before falling back to downloading from GitHub; improved error messaging on failure.
- scripts/core/build.func: resolve SCRIPT_DIR, prefer local alpine/install function files and export their contents to FUNCTIONS_FILE_PATH; fall back to remote download if local files are missing; use local install scripts for lxc-attach when available (fallback to remote). Adjusted dev MOTD path to source from /tmp.
- src/app/_components/ConfigurationModal.tsx: added a password input for var_github_token (passed as GITHUB_TOKEN to mitigate API rate limits).
- src/server/api/websocket/handler.ts: websocket handler now accepts an envVars object and forwards it into startScriptExecution so environment variables from the client can be supplied to script runs.
Overall this improves local development workflows and enables passing custom env vars and a GitHub token to scripts.
* Replace *_json with install_methods/notes
Adapt code to the updated PocketBase schema by renaming install_methods_json and notes_json to install_methods and notes across services and routers. Update parsing in pbScripts.ts, mappings in scripts.ts, autoSyncService.js, and localScripts.ts, and adjust scriptDownloader fallback comment/logic to reference the new field. Also add new PBScript metadata fields (github_data, deleted_message, disable_message, last_update_commit) so the PBScript type reflects the extended record shape.
* Add InstallCommandBlock and integrate installer UI
Introduce a full InstallCommandBlock React component (new file) that builds, previews, and copies install commands with support for GitHub/Gitea, Alpine/Advanced modes, ARM flag, resource presets, and dev gating. Integrate it into ScriptDetailModal: compute hasAlpine and installDefaults from script.install_methods, remove the old simple install command UI/handlers, and embed the new component for non-misc scripts. Update ScriptsGrid to allow aborting batch downloads, invalidate downloaded-scripts cache after downloads, and adjust orderedSlugs passed to the modal (prefer newest when no active filters). On the server, make ScriptDownloaderService resolve dev scripts to the ProxmoxVED repository (dev-specific repo path) while preserving explicit repository_url and default repo behavior.
* Fix envStr possibly undefined in InstallCommandBlock
* Redesign Install section: inline node picker, remove old modals, drop My Notes
* Remove bash command display, pass envVars to SSH, inline Terminal below Install
* Fix envVars: always pass mode to skip whiptail dialog; fix terminal clipping
* Remove dangerous source envVar, fix View button gating, clean dead onInstallScript prop chains
* Tidy JSX formatting and remove extra whitespace
Clean up minor JSX formatting and stray blank lines across components. Removed extra blank lines in DownloadedScriptsTab and ScriptsGrid modal props, reformatted the terminal container div in InstallCommandBlock for clearer attribute layout, and simplified conditional JSX in page.tsx to single-line renders. No functional changes intended—only stylistic/formatting adjustments to improve readability.
* refactor: comprehensive code quality improvements
- Move Terminal component into GeneratorTab (self-contained)
- Replace all alert()/confirm() with modal dialogs
- Type server prop as Server instead of any
- Fix deprecated onKeyPress -> onKeyDown
- Memoize stat counters + fix O(n^2) scriptCounts with Set lookups
- Remove dead hasActions function (always returned true)
- Decompose InstalledScriptsTab: extract Stats, Filters, StatusMessage
- Add buildServerFromScript helper (deduplicates 5 identical blocks)
- Fix eslint-disable blanket: targeted disables + void floating promises
* fix: resolve LXCSettingsModal rootfs_size TS error blocking build
- Type configData as Record<string,unknown> so TS union includes rootfs_size
- Include formatter fixes for extracted sub-components
* feat: bootstrap updater - self-updating update engine
Split update system into two files:
- update.sh: Thin bootstrap (~100 lines) that fetches the latest
update-engine.sh from main branch before executing it
- update-engine.sh: Full update logic (moved from old update.sh)
- UPDATER_VERSION: Version tracker for the engine
How it works:
1. User runs 'bash update.sh' (unchanged workflow)
2. Bootstrap checks UPDATER_VERSION on main vs local
3. If different, downloads latest update-engine.sh from main
4. Hands off to update-engine.sh with all arguments
This ensures bugfixes to the update logic reach users automatically,
without requiring a manual intervention or a new app release.
* fix: widen terminal - collapse sidebar to single column when terminal active
- Add onTerminalChange callback to InstallCommandBlock
- ScriptDetailModal switches from 2-col grid to single column when terminal runs
- Sidebar (ACCESS/DETAILS/INSTALL PROFILES) moves below terminal
- Remove max-w-4xl cap from Terminal.tsx so it fills full container width
- Reset terminalActive state when navigating between scripts
* Release prep: bump to 1.0.0-pre1 + add updater
Update project to pre-release 1.0.0-pre1 and add an interactive updater.
- Bump VERSION and package.json version to 1.0.0-pre1.
- Add pre-release-updater.sh: interactive script to list GitHub prereleases, backup current install, download/extract a selected prerelease, install deps, run migrations, build, and restart the service.
- Minor JSX formatting cleanup in ScriptDetailModal.tsx (split long className into multiple lines for readability; no functional change).
* fix: correct INSTALL_DIR to /opt/ProxmoxVE-Local
* fix(modal): resolve z-index race in ModalStackProvider; refactor(installed-scripts): UI overhaul - stats cards, filters, toolbar, table
* feat(installed-scripts): add Restart/Reboot action; fix orphaned cleanup for missing-server records
* fix(cleanup): delete SSH records with server but no container_id (Pass 1.5); feat(backups): add Create Backup via vzdump in BackupsTab
* fix(generator): await refetch after download; add server selection for SSH execution
- Fix download race condition: await getAllDownloadedScripts.refetch() before
closing dialog and selecting script so isScriptDownloaded returns true
- Add Server type import + state + useEffect to fetch /api/servers on mount
- Add server selection pill buttons above Execute (auto-selects single server)
- Execute button label changes to 'Execute on <name>' or 'Execute Locally'
- Pass mode + server to Terminal for correct local vs SSH execution
- Add selectedServer to handleExecute useCallback dependencies
* fix(generator): restore missing setTimeout body (syntax error)
* style: auto-format BackupsTab and GeneratorTab
* feat(pre2): ProxmoxVED toggle, floating shell dialog, settings UI polish
- Settings: remove Theme block (use AppearanceButton instead), restyle tabs
to match main nav pill style
- Settings: add ProxmoxVED / Dev Scripts toggle (default: off); persisted in
localStorage; StorageEvent keeps ScriptsGrid in sync without context
- ScriptsGrid: filter is_dev scripts from Newest carousel and full grid when
ProxmoxVED is disabled; add showDevScripts -> FilterBar prop
- FilterBar: hide 'In Dev' quick-filter pill when ProxmoxVED is disabled
- ShellContext: new React context (open/minimize/restore/close) for shell sessions
- FloatingShell: new floating dialog with minimize-to-pill, maximize, close;
persists across tab switches; renders via createPortal
- InstalledScriptsTab: delegate shell open to useShell() context instead of
local inline terminal; remove inline shell JSX
- page.tsx: wrap Home in ShellProvider, render FloatingShell at page level
* feat: implement execute_in container picker for addon scripts
- Add execute_in?: string[] | null to Script type
- Forward execute_in from PocketBase via pbToScript() mapper
- Add listContainersOnServer tRPC query (pct list + qm list via SSH)
- GeneratorTab: show container/VM picker when script has execute_in flags
- lxc, pbs, pmg -> show LXC picker (pbs/pmg pin matching containers)
- vm -> show VM picker
- Passes CTID env var to the script on execution
- Picker resets on server or script change
* fix(generator): move container-reset useEffect after selectedSlug declaration
* feat: add prerelease update channel toggle + bump version to 1.0.0-pre2
- Add ALLOW_PRERELEASE env var to env.js
- Add /api/settings/prerelease GET/POST route (persists to .env)
- version.ts: getVersionStatus + getLatestRelease use /releases (all) when
ALLOW_PRERELEASE=true, otherwise /releases/latest (stable only)
- GitHubRelease interface: add prerelease boolean field
- GeneralSettingsModal: add Update Channel toggle in General tab
- VERSION: bump to 1.0.0-pre2
- page.tsx: destructure isAuthenticated from useAuth()
- GeneratorTab.tsx: move container-reset useEffect after selectedSlug decl
* fix(page): destructure logout from useAuth
* feat: floating terminal for backup + New Backup dialog + fixes
- FloatingShell: support backup task mode (isBackup Terminal when
session.backupStorage is set); adds HardDrive icon, onComplete callback
- ShellContext: extend ShellSession with backupStorage, title, onComplete
- BackupsTab: replace blocking createBackupMutation with FloatingShell backup;
add 'New Backup' dialog (server -> container -> storage selection)
- InstalledScriptsTab: fix fetchStorages to not show blocking error modal
- VERSION: bump to 1.0.0-pre3, fixed UTF-8 BOM encoding
* fix: keep Terminal mounted on minimize, add drag-to-move support
- FloatingShell is now a single always-mounted window instead of
conditional branches — visibility:hidden on minimize keeps the
xterm.js WebSocket alive so 'starting shell session' no longer
appears on restore
- Header acts as drag handle: mouse-drag repositions the floating
window freely so the background remains usable
- Maximize still covers full viewport; restore returns to last
dragged position (or CSS-centered if never dragged)
* fix: Server.host -> Server.ip in BackupsTab new backup dialog
* fix: use lxc/vm fields from listContainersOnServer (no containers/vmid)
* fix: bundle core.func+error-handler.func into FUNCTIONS_FILE_PATH
* fix: typed container list in New Backup dialog (no unsafe any)
* feat: v1.0.0-pre4 - multi-session shell, multi-LXC backup, auto-discover after backup, local generator command, addon execute_in
* feat: add Refresh button to Downloaded Scripts tab
* fix: add executeInContainer to WebSocketMessage typedef
* fix: installationId typedef + verbose logo error logging
* fix: add JSDoc type annotations to implicit any callback params
* fix: guard against undefined results[j] in logo cache loop
* Refactor Backups modal and minor UI cleanups
Refactor the Backups create dialog for clarity and better state handling: reorganized JSX, normalized formatting, extract typed container lists, and ensure closing the dialog clears selected storage. Adjust server/container/storage selection flows and button behaviors to improve readability and UX. Also apply small readability/formatting fixes in DownloadedScriptsTab (expand onClick), FloatingShell (simplify Math.min expression), and GeneratorTab (wrap ternary for containerId). These are mostly non-functional cleanup and UI clarity changes across the mentioned components.
* feat: execute_in container routing + install UI overhaul
- GeneratorTab: remove && !!selectedServer from execInContainer so addon
scripts work in local mode (pct exec works without SSH)
- InstallCommandBlock: add executeIn prop + container picker via tRPC
listContainersOnServer; pass executeInContainer/containerId/containerType
to Terminal so addon scripts run inside the container, not on the host
- InstallCommandBlock: remove GitHub/Gitea source toggle; promote
My Defaults + App Defaults to top-level tabs alongside Default/Alpine/Advanced
- ScriptDetailModal: pass execute_in to InstallCommandBlock; add Runs In
badges to Details panel
* fix: GitHub PAT not applied at runtime + light mode contrast
- route.ts: also set process.env.GITHUB_TOKEN in memory after writing to
.env file - tokens saved via UI now take effect immediately without restart
- github.ts: read process.env.GITHUB_TOKEN directly instead of the frozen
t3-env snapshot (env.GITHUB_TOKEN is captured once at startup)
- github.ts: switch Authorization to 'Bearer' scheme (works for both classic
ghp_ and fine-grained github_pat_ tokens per GitHub docs)
- GeneralSettingsModal: fix text-success-foreground / text-error-foreground
-> text-success / text-error so messages are readable in light mode
(success-foreground is white, which is invisible on bg-success/10)
- GeneralSettingsModal: update PAT description to mention both token types
* fix: unblock pre-release updater TypeScript build + cut pre5
- InstallCommandBlock: fix trpc input type for listContainersOnServer
(serverId now numeric fallback instead of string)
- InstallCommandBlock: align container list mapping to API response shape
(use id/name instead of vmid)
- InstallCommandBlock: narrow running.containerType type to 'lxc' | 'vm'
- VERSION: bump to 1.0.0-pre5
- package.json: bump app version to 1.0.0-pre5 (installer/build logs)
This resolves the pre4 updater build failures reported in PR comments.
* feat: backup dialog UX + generator local-node workflow fixes
Backups:
- render create-backup dialog in portal with full-screen backdrop
- fix multi-select flow: explicit steps (server -> containers -> storage)
- keep container selection until user clicks Continue
- show storage free capacity (GB) from pvesm status
- show estimated max backup size from selected container disk templates
Generator:
- remove 'This machine (local)' execution option
- require node selection; execute over SSH only
- generated command now always uses local script path (scripts/...)
instead of remote curl command
- add install mode tabs: Default / My Defaults / App Defaults / Advanced
- apply mode to command/envVars (mode=default|mydefaults|appdefaults|generated)
- use selected container resources as template defaults (cpu/ram/disk)
API:
- getBackupStorages now enriches storages with available/used/total GB
- add getContainersResourceTemplates for per-container cpu/ram/disk templates
* Add advanced options to generator tab
Expose many new advanced/container, network and feature settings in GeneratorTab: password, tags, timezone, container/template storage, protection, IPv6 method/ip/gateway, search domain, nameserver, TUN, keyctl, mknod, verbose, APT cacher (with IP), mount filesystems and SSH authorized key. Wire these into command overrides, env-vars generation, export/import and reset logic. UI updated with grouped sections, IPv6 selector buttons, new inputs and toggles, and FieldInput now supports input type and hint. Also minor refactor to templateDefaults lookup formatting.
* Format JSX in BackupsTab and GeneralSettingsModal
Apply code style/formatting updates across BackupsTab.tsx and GeneralSettingsModal.tsx: reflow JSX props and elements, add consistent line breaks/commas and minor spacing adjustments (including a spacing fix in the GitHub token text). No logic or behavioral changes.
* feat: filter addon/pve types from sync, display, and script loading
- Add UNSUPPORTED_TYPES constant ['addon', 'pve'] in scripts router
- Filter these types from getScriptCards, getScriptCardsWithCategories
- Block loadScript and loadMultipleScripts for unsupported types
- Remove addon/pve filter options from FilterBar UI
- Remove unused Wrench/Server icon imports from FilterBar
* chore: bump version to 1.0.0-pre6
* fix: add pbs_username to hand-written PBSStorageCredential type
* chore: update dependencies and regenerate lockfile
* feat: hide dev count badge in category sidebar when dev mode is off
* feat: add DHCP/STATIC IPv4 mode pills to generator, show IP/gateway fields only when static
* Fix CategorySidebar JSX and format GeneratorTab
Close a mismatched <span> in CategorySidebar so the inner "dev" badge is properly nested and rendered. Also reformat several single-line conditionals and adjust className ordering/formatting in GeneratorTab for improved readability; these are stylistic changes with no functional behavior changes.
* fix: keep floating shell windows mounted when minimized to preserve session state
Instead of unmounting FloatingShellWindow (which destroyed xterm + WebSocket),
all session windows are now kept in the DOM and hidden via CSS (display: none)
when minimized. This preserves the terminal session, history, and WebSocket
connection across minimize/restore cycles.
* feat: add floating shell access to Scripts, Downloaded, and Detail modal
Shell buttons now appear everywhere an installed container is recognized:
- ScriptDetailModal sidebar: 'Containers' section shows each matching
installed container with a direct Shell button (covers Scripts +
Downloaded tabs via the modal)
- ScriptsGrid / ScriptCard: shell button appears on cards (card and list
view) for scripts that have an installed container
- DownloadedScriptsTab: same shell button on cards and list rows
- ScriptCardList: shell button in the header row alongside website link
Matching is done by normalizing the installed script_name against the
card slug (lowercase, strip extension, replace non-alphanumeric with dash).
Only containers with a valid container_id and non-failed status are shown.
* fix: restore downloaded addon/pve scripts visibility in Downloaded tab
The UNSUPPORTED_TYPES filter was applied at the router level in
getScriptCardsWithCategories and getScriptCards. Since DownloadedScriptsTab
builds its list by cross-referencing local files against the GitHub cards
from this endpoint, addon/pve scripts that were already downloaded locally
were invisibly dropped.
Fix: remove the filter from the router, apply it client-side in
ScriptsGrid's combinedScripts memo instead. This means:
- Scripts grid: still hides addon/pve (can't install them)
- Downloaded tab: shows ALL locally downloaded scripts regardless of type
- loadScript / loadMultipleScripts: still block new addon/pve downloads
* Format imports and object indentation
Reformat code in DownloadedScriptsTab.tsx and ScriptDetailModal.tsx for readability: split the React import across multiple lines, adjust placement/indentation of eslint-disable comments, and reflow object property lines in ScriptDetailModal. These are stylistic changes only and do not modify runtime behavior.
* feat: v1.0.0-pre7 - updater channel fix, configurable detection tag, static IP guard
Fixes:
- fix: updater no longer downgrades prerelease installs to stable
version.ts executeUpdate now uses the v1.0.0 branch when VERSION
contains 'pre', so prerelease users always get the prerelease update.sh
- fix: static IP guard in build.func - if NET is the literal word 'static'
(no CIDR entered), fall back to dhcp with a warning instead of creating
a container with ip=static
- fix: ConfigurationModal no longer sends var_net=static to the script
when no IP address was entered in the static IP field
Features:
- feat: configurable container detection tag (Settings > General)
Users can override 'community-script' with any tag (e.g. 'cs')
Stored in .env as CONTAINER_DETECTION_TAG, used by autoDetectLXCContainers
Version:
- bump VERSION + package.json to 1.0.0-pre7
* fix: preserve downloaded scripts across updates
- pre-release updater now backs up/restores scripts ct/tools/vm/vw
- prevent rsync from overwriting downloaded script directories
- include scripts/vw in update-engine backup/restore/rollback/excludes
This prevents downloaded scripts from disappearing after update and
explains why only bundled defaults (e.g. debian + arcane) remained.
* feat(ui): unify floating terminals and enable addon execution
* chore: bump version to 1.0.0-pre8
* fix(updater): skip engine self-update gracefully when UPDATER_VERSION not on main (pre-release)
* fix: SSH interactive input, suppress rsync output, 4-theme picker, sort containers by ID
* fix(ui): overhaul terminal themes with strong contrast and real preview cards
* feat(pre9): execute_in policy matrix, full type visibility, lxc-only generator
* fix(pre9): make VM/LXC status and type detection server-scoped
---------
Co-authored-by: MickLesk <mickey.leskowitz@levelbuild.com>
1714 lines
55 KiB
Bash
1714 lines
55 KiB
Bash
#!/usr/bin/env bash
|
||
# Copyright (c) 2021-2026 community-scripts ORG
|
||
# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/LICENSE
|
||
|
||
# ==============================================================================
|
||
# CORE FUNCTIONS - LXC CONTAINER UTILITIES
|
||
# ==============================================================================
|
||
#
|
||
# This file provides core utility functions for LXC container management
|
||
# including colors, formatting, validation checks, message output, and
|
||
# execution helpers used throughout the Community-Scripts ecosystem.
|
||
#
|
||
# Usage:
|
||
# source <(curl -fsSL https://git.community-scripts.org/.../core.func)
|
||
# load_functions
|
||
#
|
||
# ==============================================================================
|
||
|
||
[[ -n "${_CORE_FUNC_LOADED:-}" ]] && return
|
||
_CORE_FUNC_LOADED=1
|
||
|
||
# ==============================================================================
|
||
# SECTION 1: INITIALIZATION & SETUP
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# load_functions()
|
||
#
|
||
# - Initializes all core utility groups (colors, formatting, icons, defaults)
|
||
# - Ensures functions are loaded only once via __FUNCTIONS_LOADED flag
|
||
# - Must be called at start of any script using these utilities
|
||
# ------------------------------------------------------------------------------
|
||
load_functions() {
|
||
[[ -n "${__FUNCTIONS_LOADED:-}" ]] && return
|
||
__FUNCTIONS_LOADED=1
|
||
color
|
||
formatting
|
||
icons
|
||
default_vars
|
||
set_std_mode
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# color()
|
||
#
|
||
# - Sets ANSI color codes for styled terminal output
|
||
# - Variables: YW (yellow), YWB (yellow bright), BL (blue), RD (red)
|
||
# GN (green), DGN (dark green), BGN (background green), CL (clear)
|
||
# ------------------------------------------------------------------------------
|
||
color() {
|
||
YW=$(echo "\033[33m")
|
||
YWB=$'\e[93m'
|
||
BL=$(echo "\033[36m")
|
||
RD=$(echo "\033[01;31m")
|
||
BGN=$(echo "\033[4;92m")
|
||
GN=$(echo "\033[1;92m")
|
||
DGN=$(echo "\033[32m")
|
||
CL=$(echo "\033[m")
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# color_spinner()
|
||
#
|
||
# - Sets ANSI color codes specifically for spinner animation
|
||
# - Variables: CS_YW (spinner yellow), CS_YWB (spinner yellow bright),
|
||
# CS_CL (spinner clear)
|
||
# - Used by spinner() function to avoid color conflicts
|
||
# ------------------------------------------------------------------------------
|
||
color_spinner() {
|
||
CS_YW=$'\033[33m'
|
||
CS_YWB=$'\033[93m'
|
||
CS_CL=$'\033[m'
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# formatting()
|
||
#
|
||
# - Defines formatting helpers for terminal output
|
||
# - BFR: Backspace and clear line sequence
|
||
# - BOLD: Bold text escape code
|
||
# - TAB/TAB3: Indentation spacing
|
||
# ------------------------------------------------------------------------------
|
||
formatting() {
|
||
BFR="\\r\\033[K"
|
||
BOLD=$(echo "\033[1m")
|
||
HOLD=" "
|
||
TAB=" "
|
||
TAB3=" "
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# icons()
|
||
#
|
||
# - Sets symbolic emoji icons used throughout user feedback
|
||
# - Provides consistent visual indicators for success, error, info, etc.
|
||
# - Icons: CM (checkmark), CROSS (error), INFO (info), HOURGLASS (wait), etc.
|
||
# ------------------------------------------------------------------------------
|
||
icons() {
|
||
CM="${TAB}✔️${TAB}"
|
||
CROSS="${TAB}✖️${TAB}"
|
||
DNSOK="✔️ "
|
||
DNSFAIL="${TAB}✖️${TAB}"
|
||
INFO="${TAB}💡${TAB}${CL}"
|
||
OS="${TAB}🖥️${TAB}${CL}"
|
||
OSVERSION="${TAB}🌟${TAB}${CL}"
|
||
CONTAINERTYPE="${TAB}📦${TAB}${CL}"
|
||
DISKSIZE="${TAB}💾${TAB}${CL}"
|
||
CPUCORE="${TAB}🧠${TAB}${CL}"
|
||
RAMSIZE="${TAB}🛠️${TAB}${CL}"
|
||
SEARCH="${TAB}🔍${TAB}${CL}"
|
||
VERBOSE_CROPPED="🔍${TAB}"
|
||
VERIFYPW="${TAB}🔐${TAB}${CL}"
|
||
CONTAINERID="${TAB}🆔${TAB}${CL}"
|
||
HOSTNAME="${TAB}🏠${TAB}${CL}"
|
||
BRIDGE="${TAB}🌉${TAB}${CL}"
|
||
NETWORK="${TAB}📡${TAB}${CL}"
|
||
GATEWAY="${TAB}🌐${TAB}${CL}"
|
||
ICON_DISABLEIPV6="${TAB}🚫${TAB}${CL}"
|
||
DEFAULT="${TAB}⚙️${TAB}${CL}"
|
||
MACADDRESS="${TAB}🔗${TAB}${CL}"
|
||
VLANTAG="${TAB}🏷️${TAB}${CL}"
|
||
ROOTSSH="${TAB}🔑${TAB}${CL}"
|
||
CREATING="${TAB}🚀${TAB}${CL}"
|
||
ADVANCED="${TAB}🧩${TAB}${CL}"
|
||
FUSE="${TAB}🗂️${TAB}${CL}"
|
||
GPU="${TAB}🎮${TAB}${CL}"
|
||
HOURGLASS="${TAB}⏳${TAB}"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# ensure_profile_loaded()
|
||
#
|
||
# - Sources /etc/profile.d/*.sh scripts if not already loaded
|
||
# - Fixes PATH issues when running via pct enter/exec (non-login shells)
|
||
# - Safe to call multiple times (uses guard variable)
|
||
# - Should be called in update_script() or any script running inside LXC
|
||
# ------------------------------------------------------------------------------
|
||
ensure_profile_loaded() {
|
||
# Skip if already loaded or running on Proxmox host
|
||
[[ -n "${_PROFILE_LOADED:-}" ]] && return
|
||
command -v pveversion &>/dev/null && return
|
||
|
||
# Source all profile.d scripts to ensure PATH is complete
|
||
if [[ -d /etc/profile.d ]]; then
|
||
for script in /etc/profile.d/*.sh; do
|
||
[[ -r "$script" ]] && source "$script" || true
|
||
done
|
||
fi
|
||
|
||
# Also ensure /usr/local/bin is in PATH (common install location)
|
||
if [[ ":$PATH:" != *":/usr/local/bin:"* ]]; then
|
||
export PATH="/usr/local/bin:$PATH"
|
||
fi
|
||
|
||
export _PROFILE_LOADED=1
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# default_vars()
|
||
#
|
||
# - Sets default retry and wait variables used for system actions
|
||
# - RETRY_NUM: Maximum number of retry attempts (default: 10)
|
||
# - RETRY_EVERY: Seconds to wait between retries (default: 3)
|
||
# - i: Counter variable initialized to RETRY_NUM
|
||
# ------------------------------------------------------------------------------
|
||
default_vars() {
|
||
RETRY_NUM=10
|
||
RETRY_EVERY=3
|
||
i=$RETRY_NUM
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# set_std_mode()
|
||
#
|
||
# - Sets default verbose mode for script and OS execution
|
||
# - If VERBOSE=yes: STD="" (show all output)
|
||
# - If VERBOSE=no: STD="silent" (suppress output via silent() wrapper)
|
||
# - If DEV_MODE_TRACE=true: Enables bash tracing (set -x)
|
||
# ------------------------------------------------------------------------------
|
||
set_std_mode() {
|
||
if [ "${VERBOSE:-no}" = "yes" ]; then
|
||
STD=""
|
||
else
|
||
STD="silent"
|
||
fi
|
||
|
||
# Enable bash tracing if trace mode active
|
||
if [[ "${DEV_MODE_TRACE:-false}" == "true" ]]; then
|
||
set -x
|
||
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# parse_dev_mode()
|
||
#
|
||
# - Parses comma-separated dev_mode variable (e.g., "motd,keep,trace")
|
||
# - Sets global flags for each mode:
|
||
# * DEV_MODE_MOTD: Setup SSH/MOTD before installation
|
||
# * DEV_MODE_KEEP: Never delete container on failure
|
||
# * DEV_MODE_TRACE: Enable bash set -x tracing
|
||
# * DEV_MODE_PAUSE: Pause after each msg_info step
|
||
# * DEV_MODE_BREAKPOINT: Open shell on error instead of cleanup
|
||
# * DEV_MODE_LOGS: Persist all logs to /var/log/community-scripts/
|
||
# * DEV_MODE_DRYRUN: Show commands without executing
|
||
# - Call this early in script execution
|
||
# ------------------------------------------------------------------------------
|
||
parse_dev_mode() {
|
||
local mode
|
||
# Initialize all flags to false
|
||
export DEV_MODE_MOTD=false
|
||
export DEV_MODE_KEEP=false
|
||
export DEV_MODE_TRACE=false
|
||
export DEV_MODE_PAUSE=false
|
||
export DEV_MODE_BREAKPOINT=false
|
||
export DEV_MODE_LOGS=false
|
||
export DEV_MODE_DRYRUN=false
|
||
|
||
# Parse comma-separated modes
|
||
if [[ -n "${dev_mode:-}" ]]; then
|
||
IFS=',' read -ra MODES <<<"$dev_mode"
|
||
for mode in "${MODES[@]}"; do
|
||
mode="$(echo "$mode" | xargs)" # Trim whitespace
|
||
case "$mode" in
|
||
motd) export DEV_MODE_MOTD=true ;;
|
||
keep) export DEV_MODE_KEEP=true ;;
|
||
trace) export DEV_MODE_TRACE=true ;;
|
||
pause) export DEV_MODE_PAUSE=true ;;
|
||
breakpoint) export DEV_MODE_BREAKPOINT=true ;;
|
||
logs) export DEV_MODE_LOGS=true ;;
|
||
dryrun) export DEV_MODE_DRYRUN=true ;;
|
||
*)
|
||
if declare -f msg_warn >/dev/null 2>&1; then
|
||
msg_warn "Unknown dev_mode: '$mode' (ignored)"
|
||
else
|
||
echo "[WARN] Unknown dev_mode: '$mode' (ignored)" >&2
|
||
fi
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# Show active dev modes
|
||
local active_modes=()
|
||
[[ $DEV_MODE_MOTD == true ]] && active_modes+=("motd")
|
||
[[ $DEV_MODE_KEEP == true ]] && active_modes+=("keep")
|
||
[[ $DEV_MODE_TRACE == true ]] && active_modes+=("trace")
|
||
[[ $DEV_MODE_PAUSE == true ]] && active_modes+=("pause")
|
||
[[ $DEV_MODE_BREAKPOINT == true ]] && active_modes+=("breakpoint")
|
||
[[ $DEV_MODE_LOGS == true ]] && active_modes+=("logs")
|
||
[[ $DEV_MODE_DRYRUN == true ]] && active_modes+=("dryrun")
|
||
|
||
if [[ ${#active_modes[@]} -gt 0 ]]; then
|
||
if declare -f msg_custom >/dev/null 2>&1; then
|
||
msg_custom "🔧" "${YWB}" "Dev modes active: ${active_modes[*]}"
|
||
else
|
||
echo "[DEV] Active modes: ${active_modes[*]}" >&2
|
||
fi
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# ==============================================================================
|
||
# SECTION 2: VALIDATION CHECKS
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# shell_check()
|
||
#
|
||
# - Verifies that the script is running under Bash shell
|
||
# - Exits with error message if different shell is detected
|
||
# - Required because scripts use Bash-specific features
|
||
# ------------------------------------------------------------------------------
|
||
shell_check() {
|
||
if [[ "$(ps -p $$ -o comm=)" != "bash" ]]; then
|
||
clear
|
||
msg_error "Your default shell is currently not set to Bash. To use these scripts, please switch to the Bash shell."
|
||
echo -e "\nExiting..."
|
||
sleep 2
|
||
exit 103
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# root_check()
|
||
#
|
||
# - Verifies script is running with root privileges
|
||
# - Detects if executed via sudo (which can cause issues)
|
||
# - Exits with error if not running as root directly
|
||
# ------------------------------------------------------------------------------
|
||
root_check() {
|
||
if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
|
||
clear
|
||
msg_error "Please run this script as root."
|
||
echo -e "\nExiting..."
|
||
sleep 2
|
||
exit 104
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# pve_check()
|
||
#
|
||
# - Validates Proxmox VE version compatibility
|
||
# - Supported: PVE 8.0-8.9 and PVE 9.0-9.1
|
||
# - Exits with error message if unsupported version detected
|
||
# ------------------------------------------------------------------------------
|
||
pve_check() {
|
||
local PVE_VER
|
||
PVE_VER="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')"
|
||
|
||
# Check for Proxmox VE 8.x: allow 8.0–8.9
|
||
if [[ "$PVE_VER" =~ ^8\.([0-9]+) ]]; then
|
||
local MINOR="${BASH_REMATCH[1]}"
|
||
if ((MINOR < 0 || MINOR > 9)); then
|
||
msg_error "This version of Proxmox VE is not supported."
|
||
msg_error "Supported: Proxmox VE version 8.0 – 8.9"
|
||
exit 105
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
# Check for Proxmox VE 9.x: allow 9.0–9.1
|
||
if [[ "$PVE_VER" =~ ^9\.([0-9]+) ]]; then
|
||
local MINOR="${BASH_REMATCH[1]}"
|
||
if ((MINOR < 0 || MINOR > 1)); then
|
||
msg_error "This version of Proxmox VE is not yet supported."
|
||
msg_error "Supported: Proxmox VE version 9.0 – 9.1"
|
||
exit 105
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
# All other unsupported versions
|
||
msg_error "This version of Proxmox VE is not supported."
|
||
msg_error "Supported versions: Proxmox VE 8.0 – 8.9 or 9.0 – 9.1"
|
||
exit 105
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# arch_check()
|
||
#
|
||
# - Validates system architecture is amd64/x86_64
|
||
# - Exits with error message for unsupported architectures (e.g., ARM/PiMox)
|
||
# - Provides link to ARM64-compatible scripts
|
||
# ------------------------------------------------------------------------------
|
||
arch_check() {
|
||
if [ "$(dpkg --print-architecture)" != "amd64" ]; then
|
||
msg_error "This script will not work with PiMox (ARM architecture detected)."
|
||
msg_warn "Visit https://github.com/asylumexp/Proxmox for ARM64 support."
|
||
sleep 2
|
||
exit 106
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# ssh_check()
|
||
#
|
||
# - Detects if script is running over SSH connection
|
||
# - Warns user for external SSH connections (recommends Proxmox shell)
|
||
# - Skips warning for local/same-subnet connections
|
||
# - Does not abort execution, only warns
|
||
# ------------------------------------------------------------------------------
|
||
ssh_check() {
|
||
if [ -n "$SSH_CLIENT" ]; then
|
||
local client_ip=$(awk '{print $1}' <<<"$SSH_CLIENT")
|
||
local host_ip=$(hostname -I | awk '{print $1}')
|
||
|
||
# Check if connection is local (Proxmox WebUI or same machine)
|
||
# - localhost (127.0.0.1, ::1)
|
||
# - same IP as host
|
||
# - local network range (10.x, 172.16-31.x, 192.168.x)
|
||
if [[ "$client_ip" == "127.0.0.1" || "$client_ip" == "::1" || "$client_ip" == "$host_ip" ]]; then
|
||
return
|
||
fi
|
||
|
||
# Check if client is in same local network (optional, safer approach)
|
||
local host_subnet=$(echo "$host_ip" | cut -d. -f1-3)
|
||
local client_subnet=$(echo "$client_ip" | cut -d. -f1-3)
|
||
if [[ "$host_subnet" == "$client_subnet" ]]; then
|
||
return
|
||
fi
|
||
|
||
# Only warn for truly external connections
|
||
msg_warn "Running via external SSH (client: $client_ip)."
|
||
msg_warn "For better stability, consider using the Proxmox Shell (Console) instead."
|
||
fi
|
||
}
|
||
|
||
# ==============================================================================
|
||
# SECTION 3: EXECUTION HELPERS
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# get_active_logfile()
|
||
#
|
||
# - Returns the appropriate log file based on execution context
|
||
# - _HOST_LOGFILE: Override for host context (keeps host logging on BUILD_LOG
|
||
# even after INSTALL_LOG is exported for the container)
|
||
# - INSTALL_LOG: Container operations (application installation)
|
||
# - BUILD_LOG: Host operations (container creation)
|
||
# - Fallback to BUILD_LOG if neither is set
|
||
# ------------------------------------------------------------------------------
|
||
get_active_logfile() {
|
||
# Host override: _HOST_LOGFILE is set (not exported) in build.func to keep
|
||
# host-side logging in BUILD_LOG after INSTALL_LOG is exported for the container.
|
||
# Without this, all host msg_info/msg_ok/msg_error would write to
|
||
# /root/.install-SESSION.log (a container path) instead of BUILD_LOG.
|
||
if [[ -n "${_HOST_LOGFILE:-}" ]]; then
|
||
echo "$_HOST_LOGFILE"
|
||
elif [[ -n "${INSTALL_LOG:-}" ]]; then
|
||
echo "$INSTALL_LOG"
|
||
elif [[ -n "${BUILD_LOG:-}" ]]; then
|
||
echo "$BUILD_LOG"
|
||
else
|
||
# Fallback for legacy scripts
|
||
echo "/tmp/build-$(date +%Y%m%d_%H%M%S).log"
|
||
fi
|
||
}
|
||
|
||
# Legacy compatibility: SILENT_LOGFILE points to active log
|
||
SILENT_LOGFILE="$(get_active_logfile)"
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# strip_ansi()
|
||
#
|
||
# - Removes ANSI escape sequences from input text
|
||
# - Used to clean colored output for log files
|
||
# - Handles both piped input and arguments
|
||
# ------------------------------------------------------------------------------
|
||
strip_ansi() {
|
||
if [[ $# -gt 0 ]]; then
|
||
echo -e "$*" | sed 's/\x1b\[[0-9;]*m//g; s/\x1b\[[0-9;]*[a-zA-Z]//g'
|
||
else
|
||
sed 's/\x1b\[[0-9;]*m//g; s/\x1b\[[0-9;]*[a-zA-Z]//g'
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# log_msg()
|
||
#
|
||
# - Writes message to active log file without ANSI codes
|
||
# - Adds timestamp prefix for log correlation
|
||
# - Creates log file if it doesn't exist
|
||
# - Arguments: message text (can include ANSI codes, will be stripped)
|
||
# ------------------------------------------------------------------------------
|
||
log_msg() {
|
||
local msg="$*"
|
||
local logfile
|
||
logfile="$(get_active_logfile)"
|
||
|
||
[[ -z "$msg" ]] && return
|
||
[[ -z "$logfile" ]] && return
|
||
|
||
# Ensure log directory exists
|
||
mkdir -p "$(dirname "$logfile")" 2>/dev/null || true
|
||
|
||
# Strip ANSI codes and write with timestamp
|
||
local clean_msg
|
||
clean_msg=$(strip_ansi "$msg")
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $clean_msg" >>"$logfile"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# log_section()
|
||
#
|
||
# - Writes a section header to the log file
|
||
# - Used for separating different phases of installation
|
||
# - Arguments: section name
|
||
# ------------------------------------------------------------------------------
|
||
log_section() {
|
||
local section="$1"
|
||
local logfile
|
||
logfile="$(get_active_logfile)"
|
||
|
||
[[ -z "$logfile" ]] && return
|
||
mkdir -p "$(dirname "$logfile")" 2>/dev/null || true
|
||
|
||
{
|
||
echo ""
|
||
echo "================================================================================"
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $section"
|
||
echo "================================================================================"
|
||
} >>"$logfile"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# silent()
|
||
#
|
||
# - Executes command with output redirected to active log file
|
||
# - On error: displays last 20 lines of log and exits with original exit code
|
||
# - Temporarily disables error trap to capture exit code correctly
|
||
# - Saves and restores previous error handling state (so callers that
|
||
# intentionally disabled error handling aren't silently re-enabled)
|
||
# - Sources explain_exit_code() for detailed error messages
|
||
# ------------------------------------------------------------------------------
|
||
silent() {
|
||
local cmd="$*"
|
||
local caller_line="${BASH_LINENO[0]:-unknown}"
|
||
local logfile="$(get_active_logfile)"
|
||
|
||
# Dryrun mode: Show command without executing
|
||
if [[ "${DEV_MODE_DRYRUN:-false}" == "true" ]]; then
|
||
if declare -f msg_custom >/dev/null 2>&1; then
|
||
msg_custom "🔍" "${BL}" "[DRYRUN] $cmd"
|
||
else
|
||
echo "[DRYRUN] $cmd" >&2
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
# Save current error handling state before disabling
|
||
# This prevents re-enabling error handling when the caller intentionally
|
||
# disabled it (e.g. build_container recovery section)
|
||
local _restore_errexit=false
|
||
[[ "$-" == *e* ]] && _restore_errexit=true
|
||
|
||
set +Eeuo pipefail
|
||
trap - ERR
|
||
|
||
"$@" >>"$logfile" 2>&1
|
||
local rc=$?
|
||
|
||
# Restore error handling ONLY if it was active before this call
|
||
if $_restore_errexit; then
|
||
set -Eeuo pipefail
|
||
trap 'error_handler' ERR
|
||
fi
|
||
|
||
if [[ $rc -ne 0 ]]; then
|
||
# Return instead of exit so that callers can use `$STD cmd || true`
|
||
# or `if $STD cmd; then ...` to handle errors gracefully.
|
||
# When no || / if is used, set -e + ERR trap will still catch it
|
||
# and error_handler() will display the error and exit.
|
||
#
|
||
# Set flag so error_handler knows to show log tail from silent's logfile
|
||
export _SILENT_FAILED_RC="$rc"
|
||
export _SILENT_FAILED_CMD="$cmd"
|
||
export _SILENT_FAILED_LINE="$caller_line"
|
||
export _SILENT_FAILED_LOG="$logfile"
|
||
|
||
return "$rc"
|
||
fi
|
||
|
||
# Clear stale flags on success (prevents false positives if a previous
|
||
# $STD cmd || true failed and a later non-silent command triggers error_handler)
|
||
unset _SILENT_FAILED_RC _SILENT_FAILED_CMD _SILENT_FAILED_LINE _SILENT_FAILED_LOG 2>/dev/null || true
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# apt_update_safe()
|
||
#
|
||
# - Runs apt-get update with graceful error handling
|
||
# - On failure: shows warning with common causes instead of aborting
|
||
# - Logs full output to active log file
|
||
# - Returns 0 even on failure so the caller can continue
|
||
# - Typical cause: enterprise repos returning 401 Unauthorized
|
||
#
|
||
# Usage:
|
||
# apt_update_safe # Warn on failure, continue without aborting
|
||
# ------------------------------------------------------------------------------
|
||
apt_update_safe() {
|
||
local logfile
|
||
logfile="$(get_active_logfile)"
|
||
|
||
local _restore_errexit=false
|
||
[[ "$-" == *e* ]] && _restore_errexit=true
|
||
|
||
set +Eeuo pipefail
|
||
trap - ERR
|
||
|
||
apt-get update >>"$logfile" 2>&1
|
||
local rc=$?
|
||
|
||
if $_restore_errexit; then
|
||
set -Eeuo pipefail
|
||
trap 'error_handler' ERR
|
||
fi
|
||
|
||
if [[ $rc -ne 0 ]]; then
|
||
msg_warn "apt-get update exited with code ${rc} — some repositories may have failed."
|
||
|
||
# Check log for common 401/403 enterprise repo issues
|
||
if grep -qiE '401\s*Unauthorized|403\s*Forbidden|enterprise\.proxmox\.com' "$logfile" 2>/dev/null; then
|
||
echo -e "${TAB}${INFO} ${YWB}Hint: Proxmox enterprise repository returned an auth error.${CL}"
|
||
echo -e "${TAB} If you don't have a subscription, you can disable the enterprise"
|
||
echo -e "${TAB} repo and use the no-subscription repo instead."
|
||
fi
|
||
|
||
echo -e "${TAB}${INFO} ${YWB}Continuing despite partial update failure — packages may still be installable.${CL}"
|
||
echo ""
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# spinner()
|
||
#
|
||
# - Displays animated spinner with rotating characters (⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏)
|
||
# - Shows SPINNER_MSG alongside animation
|
||
# - Runs in infinite loop until killed by stop_spinner()
|
||
# - Uses color_spinner() colors for output
|
||
# ------------------------------------------------------------------------------
|
||
spinner() {
|
||
local chars=(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏)
|
||
local msg="${SPINNER_MSG:-Processing...}"
|
||
local i=0
|
||
while true; do
|
||
local index=$((i++ % ${#chars[@]}))
|
||
printf "\r\033[2K%s %b" "${CS_YWB}${chars[$index]}${CS_CL}" "${CS_YWB}${msg}${CS_CL}"
|
||
sleep 0.1
|
||
done
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# clear_line()
|
||
#
|
||
# - Clears current terminal line using tput or ANSI escape codes
|
||
# - Moves cursor to beginning of line (carriage return)
|
||
# - Erases from cursor to end of line
|
||
# - Fallback to ANSI codes if tput not available
|
||
# ------------------------------------------------------------------------------
|
||
clear_line() {
|
||
tput cr 2>/dev/null || echo -en "\r"
|
||
tput el 2>/dev/null || echo -en "\033[K"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# stop_spinner()
|
||
#
|
||
# - Stops running spinner process by PID
|
||
# - Reads PID from SPINNER_PID variable or /tmp/.spinner.pid file
|
||
# - Attempts graceful kill, then forced kill if needed
|
||
# - Cleans up temp file and resets terminal state
|
||
# - Unsets SPINNER_PID and SPINNER_MSG variables
|
||
# ------------------------------------------------------------------------------
|
||
stop_spinner() {
|
||
local pid="${SPINNER_PID:-}"
|
||
[[ -z "$pid" && -f /tmp/.spinner.pid ]] && pid=$(</tmp/.spinner.pid)
|
||
|
||
if [[ -n "$pid" && "$pid" =~ ^[0-9]+$ ]]; then
|
||
if kill "$pid" 2>/dev/null; then
|
||
sleep 0.05
|
||
kill -9 "$pid" 2>/dev/null || true
|
||
wait "$pid" 2>/dev/null || true
|
||
fi
|
||
rm -f /tmp/.spinner.pid
|
||
fi
|
||
|
||
unset SPINNER_PID SPINNER_MSG
|
||
stty sane 2>/dev/null || true
|
||
stty -tostop 2>/dev/null || true
|
||
}
|
||
|
||
# ==============================================================================
|
||
# SECTION 4: MESSAGE OUTPUT
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# msg_info()
|
||
#
|
||
# - Displays informational message with spinner animation
|
||
# - Shows each unique message only once (tracked via MSG_INFO_SHOWN)
|
||
# - In verbose/Alpine mode: shows hourglass icon instead of spinner
|
||
# - Stops any existing spinner before starting new one
|
||
# - Backgrounds spinner process and stores PID for later cleanup
|
||
# ------------------------------------------------------------------------------
|
||
msg_info() {
|
||
local msg="$1"
|
||
[[ -z "$msg" ]] && return
|
||
|
||
if ! declare -p MSG_INFO_SHOWN &>/dev/null || ! declare -A MSG_INFO_SHOWN &>/dev/null; then
|
||
declare -gA MSG_INFO_SHOWN=()
|
||
fi
|
||
[[ -n "${MSG_INFO_SHOWN["$msg"]+x}" ]] && return
|
||
MSG_INFO_SHOWN["$msg"]=1
|
||
|
||
# Log to file
|
||
log_msg "[INFO] $msg"
|
||
|
||
stop_spinner
|
||
SPINNER_MSG="$msg"
|
||
|
||
if is_verbose_mode || is_alpine; then
|
||
local HOURGLASS="${TAB}⏳${TAB}"
|
||
printf "\r\e[2K%s %b" "$HOURGLASS" "${YW}${msg}${CL}" >&2
|
||
|
||
# Pause mode: Wait for Enter after each step
|
||
if [[ "${DEV_MODE_PAUSE:-false}" == "true" ]]; then
|
||
echo -en "\n${YWB}[PAUSE]${CL} Press Enter to continue..." >&2
|
||
read -r
|
||
fi
|
||
return
|
||
fi
|
||
|
||
color_spinner
|
||
spinner &
|
||
SPINNER_PID=$!
|
||
echo "$SPINNER_PID" >/tmp/.spinner.pid
|
||
disown "$SPINNER_PID" 2>/dev/null || true
|
||
|
||
# Pause mode: Stop spinner and wait
|
||
if [[ "${DEV_MODE_PAUSE:-false}" == "true" ]]; then
|
||
stop_spinner
|
||
echo -en "\n${YWB}[PAUSE]${CL} Press Enter to continue..." >&2
|
||
read -r
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# msg_ok()
|
||
#
|
||
# - Displays success message with checkmark icon
|
||
# - Stops spinner and clears line before output
|
||
# - Removes message from MSG_INFO_SHOWN to allow re-display
|
||
# - Uses green color for success indication
|
||
# ------------------------------------------------------------------------------
|
||
msg_ok() {
|
||
local msg="$1"
|
||
[[ -z "$msg" ]] && return
|
||
stop_spinner
|
||
clear_line
|
||
echo -e "$CM ${GN}${msg}${CL}"
|
||
log_msg "[OK] $msg"
|
||
local sanitized_msg
|
||
sanitized_msg=$(printf '%s' "$msg" | sed 's/\x1b\[[0-9;]*m//g; s/[^a-zA-Z0-9_]/_/g')
|
||
unset 'MSG_INFO_SHOWN['"$sanitized_msg"']' 2>/dev/null || true
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# msg_error()
|
||
#
|
||
# - Displays error message with cross/X icon
|
||
# - Stops spinner before output
|
||
# - Uses red color for error indication
|
||
# - Outputs to stderr
|
||
# ------------------------------------------------------------------------------
|
||
msg_error() {
|
||
stop_spinner
|
||
local msg="$1"
|
||
echo -e "${BFR:-}${CROSS:-✖️} ${RD}${msg}${CL}" >&2
|
||
log_msg "[ERROR] $msg"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# msg_warn()
|
||
#
|
||
# - Displays warning message with info/lightbulb icon
|
||
# - Stops spinner before output
|
||
# - Uses bright yellow color for warning indication
|
||
# - Outputs to stderr
|
||
# ------------------------------------------------------------------------------
|
||
msg_warn() {
|
||
stop_spinner
|
||
local msg="$1"
|
||
echo -e "${BFR:-}${INFO:-ℹ️} ${YWB}${msg}${CL}" >&2
|
||
log_msg "[WARN] $msg"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# msg_custom()
|
||
#
|
||
# - Displays custom message with user-defined symbol and color
|
||
# - Arguments: symbol, color code, message text
|
||
# - Stops spinner before output
|
||
# - Useful for specialized status messages
|
||
# ------------------------------------------------------------------------------
|
||
msg_custom() {
|
||
local symbol="${1:-"[*]"}"
|
||
local color="${2:-"\e[36m"}"
|
||
local msg="${3:-}"
|
||
[[ -z "$msg" ]] && return
|
||
stop_spinner
|
||
echo -e "${BFR:-} ${symbol} ${color}${msg}${CL:-\e[0m}"
|
||
log_msg "$msg"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# msg_debug()
|
||
#
|
||
# - Displays debug message with timestamp when var_full_verbose=1
|
||
# - Automatically enables var_verbose if not already set
|
||
# - Shows date/time prefix for log correlation
|
||
# - Uses bright yellow color for debug output
|
||
# ------------------------------------------------------------------------------
|
||
msg_debug() {
|
||
if [[ "${var_full_verbose:-0}" == "1" ]]; then
|
||
[[ "${var_verbose:-0}" != "1" ]] && var_verbose=1
|
||
echo -e "${YWB}[$(date '+%F %T')] [DEBUG]${CL} $*"
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# msg_dev()
|
||
#
|
||
# - Display development mode messages with 🔧 icon
|
||
# - Only shown when dev_mode is active
|
||
# - Useful for debugging and development-specific output
|
||
# - Format: [DEV] message with distinct formatting
|
||
# - Usage: msg_dev "Container ready for debugging"
|
||
# ------------------------------------------------------------------------------
|
||
msg_dev() {
|
||
if [[ -n "${dev_mode:-}" ]]; then
|
||
echo -e "${SEARCH}${BOLD}${DGN}🔧 [DEV]${CL} $*"
|
||
fi
|
||
}
|
||
#
|
||
# - Displays error message and immediately terminates script
|
||
# - Sends SIGINT to current process to trigger error handler
|
||
# - Use for unrecoverable errors that require immediate exit
|
||
# ------------------------------------------------------------------------------
|
||
fatal() {
|
||
msg_error "$1"
|
||
kill -INT $$
|
||
}
|
||
|
||
# ==============================================================================
|
||
# SECTION 5: UTILITY FUNCTIONS
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# exit_script()
|
||
#
|
||
# - Called when user cancels an action
|
||
# - Clears screen and displays exit message
|
||
# - Exits with default exit code
|
||
# ------------------------------------------------------------------------------
|
||
exit_script() {
|
||
clear
|
||
msg_error "User exited script"
|
||
exit 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# get_header()
|
||
#
|
||
# - Downloads and caches application header ASCII art
|
||
# - Falls back to local cache if already downloaded
|
||
# - Determines app type (ct/vm) from APP_TYPE variable
|
||
# - Returns header content or empty string on failure
|
||
# ------------------------------------------------------------------------------
|
||
get_header() {
|
||
local app_name=$(echo "${APP,,}" | tr -d ' ')
|
||
local app_type=${APP_TYPE:-ct} # Default to 'ct' if not set
|
||
local header_dir="${app_type}"
|
||
[[ "$app_type" == "addon" ]] && header_dir="tools"
|
||
local header_url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/${header_dir}/headers/${app_name}"
|
||
local local_header_path="/usr/local/community-scripts/headers/${app_type}/${app_name}"
|
||
|
||
mkdir -p "$(dirname "$local_header_path")"
|
||
|
||
if [ ! -s "$local_header_path" ]; then
|
||
if ! curl -fsSL "$header_url" -o "$local_header_path"; then
|
||
msg_warn "Failed to download header: $header_url"
|
||
return 250
|
||
fi
|
||
fi
|
||
|
||
cat "$local_header_path" 2>/dev/null || true
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# header_info()
|
||
#
|
||
# - Displays application header ASCII art at top of screen
|
||
# - Clears screen before displaying header
|
||
# - Detects terminal width for formatting
|
||
# - Returns silently if header not available
|
||
# ------------------------------------------------------------------------------
|
||
header_info() {
|
||
local app_name=$(echo "${APP,,}" | tr -d ' ')
|
||
local header_content
|
||
|
||
header_content=$(get_header "$app_name") || header_content=""
|
||
|
||
clear
|
||
local term_width
|
||
term_width=$(tput cols 2>/dev/null || echo 120)
|
||
|
||
if [ -n "$header_content" ]; then
|
||
echo "$header_content"
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# ensure_tput()
|
||
#
|
||
# - Ensures tput command is available for terminal control
|
||
# - Installs ncurses-bin on Debian/Ubuntu or ncurses on Alpine
|
||
# - Required for clear_line() and terminal width detection
|
||
# ------------------------------------------------------------------------------
|
||
ensure_tput() {
|
||
if ! command -v tput >/dev/null 2>&1; then
|
||
if grep -qi 'alpine' /etc/os-release; then
|
||
apk add --no-cache ncurses >/dev/null 2>&1 || msg_warn "Failed to install ncurses (tput may be unavailable)"
|
||
elif command -v apt-get >/dev/null 2>&1; then
|
||
apt-get update -qq >/dev/null
|
||
apt-get install -y -qq ncurses-bin >/dev/null 2>&1 || msg_warn "Failed to install ncurses-bin (tput may be unavailable)"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# is_alpine()
|
||
#
|
||
# - Detects if running on Alpine Linux
|
||
# - Checks var_os, PCT_OSTYPE, or /etc/os-release
|
||
# - Returns 0 if Alpine, 1 otherwise
|
||
# - Used to adjust behavior for Alpine-specific commands
|
||
# ------------------------------------------------------------------------------
|
||
is_alpine() {
|
||
local os_id="${var_os:-${PCT_OSTYPE:-}}"
|
||
|
||
if [[ -z "$os_id" && -f /etc/os-release ]]; then
|
||
os_id="$(
|
||
. /etc/os-release 2>/dev/null
|
||
echo "${ID:-}"
|
||
)"
|
||
fi
|
||
|
||
[[ "$os_id" == "alpine" ]]
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# is_verbose_mode()
|
||
#
|
||
# - Determines if script should run in verbose mode
|
||
# - Checks VERBOSE and var_verbose variables
|
||
# - Used by msg_info() to decide between spinner and static output
|
||
# - Note: Non-TTY (pipe) scenarios are handled separately in msg_info()
|
||
# to allow spinner output to pass through pipes (e.g. lxc-attach | tee)
|
||
# ------------------------------------------------------------------------------
|
||
is_verbose_mode() {
|
||
local verbose="${VERBOSE:-${var_verbose:-no}}"
|
||
[[ "$verbose" != "no" ]]
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# is_unattended()
|
||
#
|
||
# - Detects if script is running in unattended/non-interactive mode
|
||
# - Checks MODE variable first (primary method)
|
||
# - Falls back to legacy flags (PHS_SILENT, var_unattended)
|
||
# - Returns 0 (true) if unattended, 1 (false) otherwise
|
||
# - Used by prompt functions to auto-apply defaults
|
||
#
|
||
# Modes that are unattended:
|
||
# - default (1) : Use script defaults, no prompts
|
||
# - mydefaults (3) : Use user's default.vars, no prompts
|
||
# - appdefaults (4) : Use app-specific defaults, no prompts
|
||
#
|
||
# Modes that are interactive:
|
||
# - advanced (2) : Full wizard with all options
|
||
#
|
||
# Note: Even in advanced mode, install scripts run unattended because
|
||
# all values are already collected during the wizard phase.
|
||
# ------------------------------------------------------------------------------
|
||
is_unattended() {
|
||
# Primary: Check MODE variable (case-insensitive)
|
||
local mode="${MODE:-${mode:-}}"
|
||
mode="${mode,,}" # lowercase
|
||
|
||
case "$mode" in
|
||
default | 1)
|
||
return 0
|
||
;;
|
||
mydefaults | userdefaults | 3)
|
||
return 0
|
||
;;
|
||
appdefaults | 4)
|
||
return 0
|
||
;;
|
||
advanced | 2)
|
||
# Advanced mode is interactive ONLY during wizard
|
||
# Inside container (install scripts), it should be unattended
|
||
# Check if we're inside a container (no pveversion command)
|
||
if ! command -v pveversion &>/dev/null; then
|
||
# We're inside the container - all values already collected
|
||
return 0
|
||
fi
|
||
# On host during wizard - interactive
|
||
return 1
|
||
;;
|
||
esac
|
||
|
||
# Legacy fallbacks for compatibility
|
||
[[ "${PHS_SILENT:-0}" == "1" ]] && return 0
|
||
[[ "${var_unattended:-}" =~ ^(yes|true|1)$ ]] && return 0
|
||
[[ "${UNATTENDED:-}" =~ ^(yes|true|1)$ ]] && return 0
|
||
|
||
# No TTY available = unattended
|
||
[[ ! -t 0 ]] && return 0
|
||
|
||
# Default: interactive
|
||
return 1
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# show_missing_values_warning()
|
||
#
|
||
# - Displays a summary of required values that used fallback defaults
|
||
# - Should be called at the end of install scripts
|
||
# - Only shows warning if MISSING_REQUIRED_VALUES array has entries
|
||
# - Provides clear guidance on what needs manual configuration
|
||
#
|
||
# Global:
|
||
# MISSING_REQUIRED_VALUES - Array of variable names that need configuration
|
||
#
|
||
# Example:
|
||
# # At end of install script:
|
||
# show_missing_values_warning
|
||
# ------------------------------------------------------------------------------
|
||
show_missing_values_warning() {
|
||
if [[ ${#MISSING_REQUIRED_VALUES[@]} -gt 0 ]]; then
|
||
echo ""
|
||
echo -e "${YW}╔════════════════════════════════════════════════════════════╗${CL}"
|
||
echo -e "${YW}║ ⚠️ MANUAL CONFIGURATION REQUIRED ║${CL}"
|
||
echo -e "${YW}╠════════════════════════════════════════════════════════════╣${CL}"
|
||
echo -e "${YW}║ The following values were not provided and need to be ║${CL}"
|
||
echo -e "${YW}║ configured manually for the service to work properly: ║${CL}"
|
||
echo -e "${YW}╟────────────────────────────────────────────────────────────╢${CL}"
|
||
for val in "${MISSING_REQUIRED_VALUES[@]}"; do
|
||
printf "${YW}║${CL} • %-56s ${YW}║${CL}\n" "$val"
|
||
done
|
||
echo -e "${YW}╟────────────────────────────────────────────────────────────╢${CL}"
|
||
echo -e "${YW}║ Check the service configuration files or environment ║${CL}"
|
||
echo -e "${YW}║ variables and update the placeholder values. ║${CL}"
|
||
echo -e "${YW}╚════════════════════════════════════════════════════════════╝${CL}"
|
||
echo ""
|
||
return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# prompt_confirm()
|
||
#
|
||
# - Prompts user for yes/no confirmation with timeout and unattended support
|
||
# - In unattended mode: immediately returns default value
|
||
# - In interactive mode: waits for user input with configurable timeout
|
||
# - After timeout: auto-applies default value
|
||
#
|
||
# Arguments:
|
||
# $1 - Prompt message (required)
|
||
# $2 - Default value: "y" or "n" (optional, default: "n")
|
||
# $3 - Timeout in seconds (optional, default: 60)
|
||
#
|
||
# Returns:
|
||
# 0 - User confirmed (yes)
|
||
# 1 - User declined (no) or timeout with default "n"
|
||
#
|
||
# Example:
|
||
# if prompt_confirm "Proceed with installation?" "y" 30; then
|
||
# echo "Installing..."
|
||
# fi
|
||
#
|
||
# # Unattended: prompt_confirm will use default without waiting
|
||
# var_unattended=yes
|
||
# prompt_confirm "Delete files?" "n" && echo "Deleting" || echo "Skipped"
|
||
# ------------------------------------------------------------------------------
|
||
prompt_confirm() {
|
||
local message="${1:-Confirm?}"
|
||
local default="${2:-n}"
|
||
local timeout="${3:-60}"
|
||
local response
|
||
|
||
# Normalize default to lowercase
|
||
default="${default,,}"
|
||
[[ "$default" != "y" ]] && default="n"
|
||
|
||
# Build prompt hint
|
||
local hint
|
||
if [[ "$default" == "y" ]]; then
|
||
hint="[Y/n]"
|
||
else
|
||
hint="[y/N]"
|
||
fi
|
||
|
||
# Unattended mode: apply default immediately
|
||
if is_unattended; then
|
||
if [[ "$default" == "y" ]]; then
|
||
return 0
|
||
else
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
# Check if running in a TTY
|
||
if [[ ! -t 0 ]]; then
|
||
# Not a TTY, use default
|
||
if [[ "$default" == "y" ]]; then
|
||
return 0
|
||
else
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
# Interactive prompt with timeout
|
||
echo -en "${YW}${message} ${hint} (auto-${default} in ${timeout}s): ${CL}"
|
||
|
||
if read -t "$timeout" -r response; then
|
||
# User provided input
|
||
response="${response,,}" # lowercase
|
||
case "$response" in
|
||
y | yes)
|
||
return 0
|
||
;;
|
||
n | no)
|
||
return 1
|
||
;;
|
||
"")
|
||
# Empty response, use default
|
||
if [[ "$default" == "y" ]]; then
|
||
return 0
|
||
else
|
||
return 1
|
||
fi
|
||
;;
|
||
*)
|
||
# Invalid input, use default
|
||
echo -e "${YW}Invalid response, using default: ${default}${CL}"
|
||
if [[ "$default" == "y" ]]; then
|
||
return 0
|
||
else
|
||
return 1
|
||
fi
|
||
;;
|
||
esac
|
||
else
|
||
# Timeout occurred
|
||
echo "" # Newline after timeout
|
||
echo -e "${YW}Timeout - auto-selecting: ${default}${CL}"
|
||
if [[ "$default" == "y" ]]; then
|
||
return 0
|
||
else
|
||
return 1
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# prompt_input()
|
||
#
|
||
# - Prompts user for text input with timeout and unattended support
|
||
# - In unattended mode: immediately returns default value
|
||
# - In interactive mode: waits for user input with configurable timeout
|
||
# - After timeout: auto-applies default value
|
||
#
|
||
# Arguments:
|
||
# $1 - Prompt message (required)
|
||
# $2 - Default value (optional, default: "")
|
||
# $3 - Timeout in seconds (optional, default: 60)
|
||
#
|
||
# Output:
|
||
# Prints the user input or default value to stdout
|
||
#
|
||
# Example:
|
||
# username=$(prompt_input "Enter username:" "admin" 30)
|
||
# echo "Using username: $username"
|
||
#
|
||
# # With validation
|
||
# while true; do
|
||
# port=$(prompt_input "Enter port:" "8080" 30)
|
||
# [[ "$port" =~ ^[0-9]+$ ]] && break
|
||
# echo "Invalid port number"
|
||
# done
|
||
# ------------------------------------------------------------------------------
|
||
prompt_input() {
|
||
local message="${1:-Enter value:}"
|
||
local default="${2:-}"
|
||
local timeout="${3:-60}"
|
||
local response
|
||
|
||
# Build display default hint
|
||
local hint=""
|
||
[[ -n "$default" ]] && hint=" (default: ${default})"
|
||
|
||
# Unattended mode: return default immediately
|
||
if is_unattended; then
|
||
echo "$default"
|
||
return 0
|
||
fi
|
||
|
||
# Check if running in a TTY
|
||
if [[ ! -t 0 ]]; then
|
||
# Not a TTY, use default
|
||
echo "$default"
|
||
return 0
|
||
fi
|
||
|
||
# Interactive prompt with timeout
|
||
echo -en "${YW}${message}${hint} (auto-default in ${timeout}s): ${CL}" >&2
|
||
|
||
if read -t "$timeout" -r response; then
|
||
# User provided input (or pressed Enter for empty)
|
||
if [[ -n "$response" ]]; then
|
||
echo "$response"
|
||
else
|
||
echo "$default"
|
||
fi
|
||
else
|
||
# Timeout occurred
|
||
echo "" >&2 # Newline after timeout
|
||
echo -e "${YW}Timeout - using default: ${default}${CL}" >&2
|
||
echo "$default"
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# prompt_input_required()
|
||
#
|
||
# - Prompts user for REQUIRED text input with fallback support
|
||
# - In unattended mode: Uses fallback value if no env var set (with warning)
|
||
# - In interactive mode: loops until user provides non-empty input
|
||
# - Tracks missing required values for end-of-script summary
|
||
#
|
||
# Arguments:
|
||
# $1 - Prompt message (required)
|
||
# $2 - Fallback/example value for unattended mode (optional)
|
||
# $3 - Timeout in seconds (optional, default: 120)
|
||
# $4 - Environment variable name hint for error messages (optional)
|
||
#
|
||
# Output:
|
||
# Prints the user input or fallback value to stdout
|
||
#
|
||
# Returns:
|
||
# 0 - Success (value provided or fallback used)
|
||
# 1 - Failed (interactive timeout without input)
|
||
#
|
||
# Global:
|
||
# MISSING_REQUIRED_VALUES - Array tracking fields that used fallbacks
|
||
#
|
||
# Example:
|
||
# # With fallback - script continues even in unattended mode
|
||
# token=$(prompt_input_required "Enter API Token:" "YOUR_TOKEN_HERE" 60 "var_api_token")
|
||
#
|
||
# # Check at end of script if any values need manual configuration
|
||
# if [[ ${#MISSING_REQUIRED_VALUES[@]} -gt 0 ]]; then
|
||
# msg_warn "Please configure: ${MISSING_REQUIRED_VALUES[*]}"
|
||
# fi
|
||
# ------------------------------------------------------------------------------
|
||
# Global array to track missing required values
|
||
declare -g -a MISSING_REQUIRED_VALUES=()
|
||
|
||
prompt_input_required() {
|
||
local message="${1:-Enter required value:}"
|
||
local fallback="${2:-CHANGE_ME}"
|
||
local timeout="${3:-120}"
|
||
local env_var_hint="${4:-}"
|
||
local response=""
|
||
|
||
# Check if value is already set via environment variable (if hint provided)
|
||
if [[ -n "$env_var_hint" ]]; then
|
||
local env_value="${!env_var_hint:-}"
|
||
if [[ -n "$env_value" ]]; then
|
||
echo "$env_value"
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
# Unattended mode: use fallback with warning
|
||
if is_unattended; then
|
||
if [[ -n "$env_var_hint" ]]; then
|
||
echo -e "${YW}⚠ Required value '${env_var_hint}' not set - using fallback: ${fallback}${CL}" >&2
|
||
MISSING_REQUIRED_VALUES+=("$env_var_hint")
|
||
else
|
||
echo -e "${YW}⚠ Required value not provided - using fallback: ${fallback}${CL}" >&2
|
||
MISSING_REQUIRED_VALUES+=("(unnamed)")
|
||
fi
|
||
echo "$fallback"
|
||
return 0
|
||
fi
|
||
|
||
# Check if running in a TTY
|
||
if [[ ! -t 0 ]]; then
|
||
echo -e "${YW}⚠ Not interactive - using fallback: ${fallback}${CL}" >&2
|
||
MISSING_REQUIRED_VALUES+=("${env_var_hint:-unnamed}")
|
||
echo "$fallback"
|
||
return 0
|
||
fi
|
||
|
||
# Interactive prompt - loop until non-empty input or use fallback on timeout
|
||
local attempts=0
|
||
while [[ -z "$response" ]]; do
|
||
attempts=$((attempts + 1))
|
||
|
||
if [[ $attempts -gt 3 ]]; then
|
||
echo -e "${YW}Too many empty inputs - using fallback: ${fallback}${CL}" >&2
|
||
MISSING_REQUIRED_VALUES+=("${env_var_hint:-manual_input}")
|
||
echo "$fallback"
|
||
return 0
|
||
fi
|
||
|
||
echo -en "${YW}${message} (required, timeout ${timeout}s): ${CL}" >&2
|
||
|
||
if read -t "$timeout" -r response; then
|
||
if [[ -z "$response" ]]; then
|
||
echo -e "${YW}This field is required. Please enter a value. (attempt ${attempts}/3)${CL}" >&2
|
||
fi
|
||
else
|
||
# Timeout occurred - use fallback
|
||
echo "" >&2
|
||
echo -e "${YW}Timeout - using fallback value: ${fallback}${CL}" >&2
|
||
MISSING_REQUIRED_VALUES+=("${env_var_hint:-timeout}")
|
||
echo "$fallback"
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
echo "$response"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# prompt_select()
|
||
#
|
||
# - Prompts user to select from a list of options with timeout support
|
||
# - In unattended mode: immediately returns default selection
|
||
# - In interactive mode: displays numbered menu and waits for choice
|
||
# - After timeout: auto-applies default selection
|
||
#
|
||
# Arguments:
|
||
# $1 - Prompt message (required)
|
||
# $2 - Default option number, 1-based (optional, default: 1)
|
||
# $3 - Timeout in seconds (optional, default: 60)
|
||
# $4+ - Options to display (required, at least 2)
|
||
#
|
||
# Output:
|
||
# Prints the selected option value to stdout
|
||
#
|
||
# Returns:
|
||
# 0 - Success
|
||
# 1 - No options provided or invalid state
|
||
#
|
||
# Example:
|
||
# choice=$(prompt_select "Select database:" 1 30 "PostgreSQL" "MySQL" "SQLite")
|
||
# echo "Selected: $choice"
|
||
#
|
||
# # With array
|
||
# options=("Option A" "Option B" "Option C")
|
||
# selected=$(prompt_select "Choose:" 2 60 "${options[@]}")
|
||
# ------------------------------------------------------------------------------
|
||
prompt_select() {
|
||
local message="${1:-Select option:}"
|
||
local default="${2:-1}"
|
||
local timeout="${3:-60}"
|
||
shift 3
|
||
|
||
local options=("$@")
|
||
local num_options=${#options[@]}
|
||
|
||
# Validate options
|
||
if [[ $num_options -eq 0 ]]; then
|
||
msg_warn "prompt_select called with no options"
|
||
echo "" >&2
|
||
return 65
|
||
fi
|
||
|
||
# Validate default
|
||
if [[ ! "$default" =~ ^[0-9]+$ ]] || [[ "$default" -lt 1 ]] || [[ "$default" -gt "$num_options" ]]; then
|
||
default=1
|
||
fi
|
||
|
||
# Unattended mode: return default immediately
|
||
if is_unattended; then
|
||
echo "${options[$((default - 1))]}"
|
||
return 0
|
||
fi
|
||
|
||
# Check if running in a TTY
|
||
if [[ ! -t 0 ]]; then
|
||
echo "${options[$((default - 1))]}"
|
||
return 0
|
||
fi
|
||
|
||
# Display menu
|
||
echo -e "${YW}${message}${CL}" >&2
|
||
local i
|
||
for i in "${!options[@]}"; do
|
||
local num=$((i + 1))
|
||
if [[ $num -eq $default ]]; then
|
||
echo -e " ${GN}${num})${CL} ${options[$i]} ${YW}(default)${CL}" >&2
|
||
else
|
||
echo -e " ${GN}${num})${CL} ${options[$i]}" >&2
|
||
fi
|
||
done
|
||
|
||
# Interactive prompt with timeout
|
||
echo -en "${YW}Select [1-${num_options}] (auto-select ${default} in ${timeout}s): ${CL}" >&2
|
||
|
||
local response
|
||
if read -t "$timeout" -r response; then
|
||
if [[ -z "$response" ]]; then
|
||
# Empty response, use default
|
||
echo "${options[$((default - 1))]}"
|
||
elif [[ "$response" =~ ^[0-9]+$ ]] && [[ "$response" -ge 1 ]] && [[ "$response" -le "$num_options" ]]; then
|
||
# Valid selection
|
||
echo "${options[$((response - 1))]}"
|
||
else
|
||
# Invalid input, use default
|
||
echo -e "${YW}Invalid selection, using default: ${options[$((default - 1))]}${CL}" >&2
|
||
echo "${options[$((default - 1))]}"
|
||
fi
|
||
else
|
||
# Timeout occurred
|
||
echo "" >&2 # Newline after timeout
|
||
echo -e "${YW}Timeout - auto-selecting: ${options[$((default - 1))]}${CL}" >&2
|
||
echo "${options[$((default - 1))]}"
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# prompt_password()
|
||
#
|
||
# - Prompts user for password input with hidden characters
|
||
# - In unattended mode: returns default or generates random password
|
||
# - Supports auto-generation of secure passwords
|
||
# - After timeout: generates random password if allowed
|
||
#
|
||
# Arguments:
|
||
# $1 - Prompt message (required)
|
||
# $2 - Default value or "generate" for auto-generation (optional)
|
||
# $3 - Timeout in seconds (optional, default: 60)
|
||
# $4 - Minimum length for validation (optional, default: 0 = no minimum)
|
||
#
|
||
# Output:
|
||
# Prints the password to stdout
|
||
#
|
||
# Example:
|
||
# password=$(prompt_password "Enter password:" "generate" 30 8)
|
||
# echo "Password set"
|
||
#
|
||
# # Require user input (no default)
|
||
# db_pass=$(prompt_password "Database password:" "" 60 12)
|
||
# ------------------------------------------------------------------------------
|
||
prompt_password() {
|
||
local message="${1:-Enter password:}"
|
||
local default="${2:-}"
|
||
local timeout="${3:-60}"
|
||
local min_length="${4:-0}"
|
||
local response
|
||
|
||
# Generate random password if requested
|
||
local generated=""
|
||
if [[ "$default" == "generate" ]]; then
|
||
generated=$(openssl rand -base64 16 2>/dev/null | tr -dc 'a-zA-Z0-9' | head -c 16)
|
||
[[ -z "$generated" ]] && generated=$(head /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 16)
|
||
default="$generated"
|
||
fi
|
||
|
||
# Unattended mode: return default immediately
|
||
if is_unattended; then
|
||
echo "$default"
|
||
return 0
|
||
fi
|
||
|
||
# Check if running in a TTY
|
||
if [[ ! -t 0 ]]; then
|
||
echo "$default"
|
||
return 0
|
||
fi
|
||
|
||
# Build hint
|
||
local hint=""
|
||
if [[ -n "$generated" ]]; then
|
||
hint=" (Enter for auto-generated)"
|
||
elif [[ -n "$default" ]]; then
|
||
hint=" (Enter for default)"
|
||
fi
|
||
[[ "$min_length" -gt 0 ]] && hint="${hint} [min ${min_length} chars]"
|
||
|
||
# Interactive prompt with timeout (silent input)
|
||
echo -en "${YW}${message}${hint} (timeout ${timeout}s): ${CL}" >&2
|
||
|
||
if read -t "$timeout" -rs response; then
|
||
echo "" >&2 # Newline after hidden input
|
||
if [[ -n "$response" ]]; then
|
||
# Validate minimum length
|
||
if [[ "$min_length" -gt 0 ]] && [[ ${#response} -lt "$min_length" ]]; then
|
||
echo -e "${YW}Password too short (min ${min_length}), using default${CL}" >&2
|
||
echo "$default"
|
||
else
|
||
echo "$response"
|
||
fi
|
||
else
|
||
echo "$default"
|
||
fi
|
||
else
|
||
# Timeout occurred
|
||
echo "" >&2 # Newline after timeout
|
||
echo -e "${YW}Timeout - using generated password${CL}" >&2
|
||
echo "$default"
|
||
fi
|
||
}
|
||
|
||
# ==============================================================================
|
||
# SECTION 6: CLEANUP & MAINTENANCE
|
||
# ==============================================================================
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# cleanup_lxc()
|
||
#
|
||
# - Cleans package manager and language caches (safe for installs AND updates)
|
||
# - Supports Alpine (apk), Debian/Ubuntu (apt), Python, Node.js, Go, Rust, Ruby, PHP
|
||
# - Uses fallback error handling to prevent cleanup failures from breaking installs
|
||
# ------------------------------------------------------------------------------
|
||
cleanup_lxc() {
|
||
msg_info "Cleaning up"
|
||
|
||
if is_alpine; then
|
||
$STD apk cache clean || true
|
||
rm -rf /var/cache/apk/*
|
||
else
|
||
$STD apt -y autoremove 2>/dev/null || msg_warn "apt autoremove failed (non-critical)"
|
||
$STD apt -y autoclean 2>/dev/null || msg_warn "apt autoclean failed (non-critical)"
|
||
$STD apt -y clean 2>/dev/null || msg_warn "apt clean failed (non-critical)"
|
||
fi
|
||
|
||
find /tmp /var/tmp -type f -name 'tmp*' -delete 2>/dev/null || true
|
||
find /tmp /var/tmp -type f -name 'tempfile*' -delete 2>/dev/null || true
|
||
|
||
# Python
|
||
if command -v pip &>/dev/null; then
|
||
rm -rf /root/.cache/pip 2>/dev/null || true
|
||
fi
|
||
if command -v uv &>/dev/null; then
|
||
rm -rf /root/.cache/uv 2>/dev/null || true
|
||
fi
|
||
|
||
# Node.js
|
||
if command -v npm &>/dev/null; then
|
||
rm -rf /root/.npm/_cacache /root/.npm/_logs 2>/dev/null || true
|
||
fi
|
||
if command -v yarn &>/dev/null; then
|
||
rm -rf /root/.cache/yarn /root/.yarn/cache 2>/dev/null || true
|
||
fi
|
||
if command -v pnpm &>/dev/null; then
|
||
pnpm store prune &>/dev/null || true
|
||
fi
|
||
|
||
# Go (only build cache, not modules)
|
||
if command -v go &>/dev/null; then
|
||
$STD go clean -cache 2>/dev/null || true
|
||
fi
|
||
|
||
# Rust (only registry cache, not build artifacts)
|
||
if command -v cargo &>/dev/null; then
|
||
rm -rf /root/.cargo/registry/cache /root/.cargo/.package-cache 2>/dev/null || true
|
||
fi
|
||
|
||
# Ruby
|
||
if command -v gem &>/dev/null; then
|
||
rm -rf /root/.gem/cache 2>/dev/null || true
|
||
fi
|
||
|
||
# PHP
|
||
if command -v composer &>/dev/null; then
|
||
rm -rf /root/.composer/cache 2>/dev/null || true
|
||
fi
|
||
|
||
msg_ok "Cleaned"
|
||
|
||
# Send progress ping if available (defined in install.func)
|
||
if declare -f post_progress_to_api &>/dev/null; then
|
||
post_progress_to_api
|
||
fi
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# check_or_create_swap()
|
||
#
|
||
# - Checks if swap is active on system
|
||
# - Offers to create swap file if none exists
|
||
# - Prompts user for swap size in MB
|
||
# - Creates /swapfile with specified size
|
||
# - Activates swap immediately
|
||
# - Returns 0 if swap active or successfully created, 1 if declined/failed
|
||
# ------------------------------------------------------------------------------
|
||
check_or_create_swap() {
|
||
msg_info "Checking for active swap"
|
||
|
||
if swapon --noheadings --show | grep -q 'swap'; then
|
||
msg_ok "Swap is active"
|
||
return 0
|
||
fi
|
||
|
||
msg_error "No active swap detected"
|
||
|
||
if ! prompt_confirm "Do you want to create a swap file?" "n" 60; then
|
||
msg_info "Skipping swap file creation"
|
||
return 1
|
||
fi
|
||
|
||
local swap_size_mb
|
||
swap_size_mb=$(prompt_input "Enter swap size in MB (e.g., 2048 for 2GB):" "2048" 60)
|
||
if ! [[ "$swap_size_mb" =~ ^[0-9]+$ ]]; then
|
||
msg_error "Invalid swap size: '${swap_size_mb}' (must be a number in MB)"
|
||
return 65
|
||
fi
|
||
|
||
local swap_file="/swapfile"
|
||
|
||
msg_info "Creating ${swap_size_mb}MB swap file at $swap_file"
|
||
if ! dd if=/dev/zero of="$swap_file" bs=1M count="$swap_size_mb" status=progress; then
|
||
msg_error "Failed to allocate swap file (dd failed)"
|
||
return 150
|
||
fi
|
||
if ! chmod 600 "$swap_file"; then
|
||
msg_error "Failed to set permissions on $swap_file"
|
||
return 150
|
||
fi
|
||
if ! mkswap "$swap_file"; then
|
||
msg_error "Failed to format swap file (mkswap failed)"
|
||
return 150
|
||
fi
|
||
if ! swapon "$swap_file"; then
|
||
msg_error "Failed to activate swap (swapon failed)"
|
||
return 150
|
||
fi
|
||
msg_ok "Swap file created and activated successfully"
|
||
}
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Loads LOCAL_IP from persistent store or detects if missing.
|
||
#
|
||
# Description:
|
||
# - Loads from /run/local-ip.env or performs runtime lookup
|
||
# ------------------------------------------------------------------------------
|
||
|
||
function get_lxc_ip() {
|
||
local IP_FILE="/run/local-ip.env"
|
||
if [[ -f "$IP_FILE" ]]; then
|
||
# shellcheck disable=SC1090
|
||
source "$IP_FILE"
|
||
fi
|
||
|
||
if [[ -z "${LOCAL_IP:-}" ]]; then
|
||
get_current_ip() {
|
||
local ip
|
||
|
||
# Try direct interface lookup for eth0 FIRST (most reliable for LXC) - IPv4
|
||
ip=$(ip -4 addr show eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1 | head -n1)
|
||
if [[ -n "$ip" && "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||
echo "$ip"
|
||
return 0
|
||
fi
|
||
|
||
# Fallback: Try hostname -I (returns IPv4 first if available)
|
||
if command -v hostname >/dev/null 2>&1; then
|
||
ip=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||
if [[ -n "$ip" && "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||
echo "$ip"
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
# Try routing table with IPv4 targets
|
||
local ipv4_targets=("8.8.8.8" "1.1.1.1" "default")
|
||
for target in "${ipv4_targets[@]}"; do
|
||
if [[ "$target" == "default" ]]; then
|
||
ip=$(ip route get 1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}')
|
||
else
|
||
ip=$(ip route get "$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}')
|
||
fi
|
||
if [[ -n "$ip" ]]; then
|
||
echo "$ip"
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
# IPv6 fallback: Try direct interface lookup for eth0
|
||
ip=$(ip -6 addr show eth0 scope global 2>/dev/null | awk '/inet6 / {print $2}' | cut -d/ -f1 | head -n1)
|
||
if [[ -n "$ip" && "$ip" =~ : ]]; then
|
||
echo "$ip"
|
||
return 0
|
||
fi
|
||
|
||
# IPv6 fallback: Try hostname -I for IPv6
|
||
if command -v hostname >/dev/null 2>&1; then
|
||
ip=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E ':' | head -n1)
|
||
if [[ -n "$ip" && "$ip" =~ : ]]; then
|
||
echo "$ip"
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
# IPv6 fallback: Use routing table with IPv6 targets
|
||
local ipv6_targets=("2001:4860:4860::8888" "2606:4700:4700::1111")
|
||
for target in "${ipv6_targets[@]}"; do
|
||
ip=$(ip -6 route get "$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}')
|
||
if [[ -n "$ip" && "$ip" =~ : ]]; then
|
||
echo "$ip"
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
return 6
|
||
}
|
||
|
||
LOCAL_IP="$(get_current_ip || true)"
|
||
if [[ -z "$LOCAL_IP" ]]; then
|
||
msg_error "Could not determine LOCAL_IP (checked: eth0, hostname -I, ip route, IPv6 targets)"
|
||
return 6
|
||
fi
|
||
fi
|
||
|
||
export LOCAL_IP
|
||
}
|
||
|
||
# ==============================================================================
|
||
# SIGNAL TRAPS
|
||
# ==============================================================================
|
||
|
||
trap 'stop_spinner' EXIT INT TERM
|