feat(supply-depot): per-app onboarding docs, install fixes, and in-app Docs links
Add NOMAD-specific getting-started docs for all 9 curated Supply Depot apps,
the catalog/install fixes each one surfaced, and a way to reach the docs from
each app card.
Docs:
- New in-app Markdoc page admin/docs/supply-depot-apps.md covering all 9 apps
(Stirling PDF, File Browser, Calibre-Web, IT Tools, Excalidraw, Homebox,
Vaultwarden, Jellyfin, Meshtastic Web): first run/login, where data lives,
and offline behaviour. Registered in docs_service DOC_ORDER.
- Manage > Docs dropdown item linking each app to its section
(/docs/supply-depot-apps#<anchor>): anchor map in constants/supply_depot_docs.ts,
heading anchors via Markdoc {% #id %}, and hash-scroll on the docs page.
Install / catalog fixes:
- Stirling PDF: open straight to the tools (SECURITY_ENABLELOGIN=false; the old
v1 DOCKER_ENABLE_SECURITY flag was dead).
- File Browser: seed a known admin/nomad login (bcrypt) instead of a random
log-only password; scope visibility to content folders via mount selection and
move the DB out of the browsable root.
- Calibre-Web: bundle an empty Calibre library and seed it on install so setup
doesn't dead-end at db config (_runPreinstallActions__CalibreWeb).
- Homebox: swap the archived hay-kot image for the maintained sysadminsmedia fork.
- Vaultwarden: generate a self-signed cert on install and serve HTTPS by default
(_runPreinstallActions__Vaultwarden + ROCKET_TLS + ui_location https:8480), so
the web vault has the secure context it requires.
- Jellyfin: pre-create storage/media/{Movies,TV Shows,Music,Photos} so each
library points at its own subfolder, avoiding the overlapping-path issue that
silently hides content (_runPreinstallActions__Jellyfin).
- Seeder run() now also syncs ui_location for non-modified curated services, so a
catalog link/scheme/port change reaches existing installs on update.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b8674193da
commit
f488f08c96
|
|
@ -84,6 +84,10 @@ RUN echo "{\"version\":\"${VERSION}\"}" > /app/version.json
|
|||
COPY admin/docs /app/docs
|
||||
COPY README.md /app/README.md
|
||||
|
||||
# Empty Calibre library, seeded into storage/books on Calibre-Web install
|
||||
# (see DockerService._runPreinstallActions__CalibreWeb)
|
||||
COPY install/calibre-empty-library/metadata.db /app/assets/calibre/metadata.db
|
||||
|
||||
# Copy entrypoint script and ensure it's executable
|
||||
COPY install/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
|
|
|||
|
|
@ -5,12 +5,19 @@ import { inject } from '@adonisjs/core'
|
|||
import transmit from '@adonisjs/transmit/services/main'
|
||||
import { doResumableDownloadWithRetry } from '../utils/downloads.js'
|
||||
import { join } from 'path'
|
||||
import { ZIM_STORAGE_PATH } from '../utils/fs.js'
|
||||
import {
|
||||
ZIM_STORAGE_PATH,
|
||||
BOOKS_STORAGE_PATH,
|
||||
CALIBRE_EMPTY_LIBRARY_ASSET_PATH,
|
||||
VAULTWARDEN_STORAGE_PATH,
|
||||
MEDIA_STORAGE_PATH,
|
||||
JELLYFIN_MEDIA_SUBFOLDERS,
|
||||
} from '../utils/fs.js'
|
||||
import { KiwixLibraryService } from './kiwix_library_service.js'
|
||||
import { SERVICE_NAMES } from '../../constants/service_names.js'
|
||||
import { exec } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { readFile, mkdir, copyFile, chown, chmod, access } from 'node:fs/promises'
|
||||
import KVStore from '#models/kv_store'
|
||||
import { BROADCAST_CHANNELS } from '../../constants/broadcast.js'
|
||||
import { KIWIX_LIBRARY_CMD } from '../../constants/kiwix.js'
|
||||
|
|
@ -510,6 +517,33 @@ export class DockerService {
|
|||
)
|
||||
}
|
||||
|
||||
if (service.service_name === SERVICE_NAMES.CALIBREWEB) {
|
||||
await this._runPreinstallActions__CalibreWeb()
|
||||
this._broadcast(
|
||||
service.service_name,
|
||||
'preinstall-complete',
|
||||
`Pre-install actions for Calibre-Web completed successfully.`
|
||||
)
|
||||
}
|
||||
|
||||
if (service.service_name === SERVICE_NAMES.VAULTWARDEN) {
|
||||
await this._runPreinstallActions__Vaultwarden()
|
||||
this._broadcast(
|
||||
service.service_name,
|
||||
'preinstall-complete',
|
||||
`Pre-install actions for Vaultwarden completed successfully.`
|
||||
)
|
||||
}
|
||||
|
||||
if (service.service_name === SERVICE_NAMES.JELLYFIN) {
|
||||
await this._runPreinstallActions__Jellyfin()
|
||||
this._broadcast(
|
||||
service.service_name,
|
||||
'preinstall-complete',
|
||||
`Pre-install actions for Jellyfin completed successfully.`
|
||||
)
|
||||
}
|
||||
|
||||
// GPU-aware configuration for Ollama
|
||||
let finalImage = service.container_image
|
||||
let gpuHostConfig = containerConfig?.HostConfig || {}
|
||||
|
|
@ -783,6 +817,176 @@ export class DockerService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calibre-Web cannot create a library from scratch — without an existing Calibre database it
|
||||
* dead-ends at the "Database Configuration" page. Seed an empty library (bundled in the admin
|
||||
* image) into storage/books so the user just points Calibre-Web at /books and starts adding
|
||||
* books. Only seeds when no metadata.db already exists, so an existing library is never clobbered.
|
||||
* Ownership is handed to the container user (PUID/PGID 1000 in the seeder) so Calibre-Web can
|
||||
* write to the library and create book folders on upload.
|
||||
*/
|
||||
private async _runPreinstallActions__CalibreWeb(): Promise<void> {
|
||||
// Keep in sync with the PUID/PGID set on the Calibre-Web service in the seeder.
|
||||
const CALIBRE_WEB_UID = 1000
|
||||
const CALIBRE_WEB_GID = 1000
|
||||
|
||||
const booksDir = join(process.cwd(), BOOKS_STORAGE_PATH)
|
||||
const metadataPath = join(booksDir, 'metadata.db')
|
||||
const assetPath = join(process.cwd(), CALIBRE_EMPTY_LIBRARY_ASSET_PATH)
|
||||
|
||||
this._broadcast(
|
||||
SERVICE_NAMES.CALIBREWEB,
|
||||
'preinstall',
|
||||
`Running pre-install actions for Calibre-Web...`
|
||||
)
|
||||
|
||||
try {
|
||||
await mkdir(booksDir, { recursive: true })
|
||||
|
||||
// Don't clobber an existing library — only seed when there's no metadata.db yet.
|
||||
const alreadyHasLibrary = await access(metadataPath)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
if (alreadyHasLibrary) {
|
||||
this._broadcast(
|
||||
SERVICE_NAMES.CALIBREWEB,
|
||||
'preinstall',
|
||||
`Existing Calibre library found in books folder — leaving it as-is.`
|
||||
)
|
||||
} else {
|
||||
await copyFile(assetPath, metadataPath)
|
||||
this._broadcast(
|
||||
SERVICE_NAMES.CALIBREWEB,
|
||||
'preinstall',
|
||||
`Seeded an empty Calibre library into the books folder.`
|
||||
)
|
||||
}
|
||||
|
||||
// Hand the books folder + library to the Calibre-Web container user so it can read/write
|
||||
// the library and create book folders on upload (Docker creates bind dirs as root otherwise).
|
||||
await chown(booksDir, CALIBRE_WEB_UID, CALIBRE_WEB_GID)
|
||||
await chown(metadataPath, CALIBRE_WEB_UID, CALIBRE_WEB_GID)
|
||||
} catch (error: any) {
|
||||
this._broadcast(
|
||||
SERVICE_NAMES.CALIBREWEB,
|
||||
'preinstall-error',
|
||||
`Failed to prepare the Calibre library: ${error.message}`
|
||||
)
|
||||
throw new Error(`Pre-install action failed: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vaultwarden's web vault refuses to run without a secure context — over plain HTTP it shows
|
||||
* "You need to enable HTTPS!" and you can't register or unlock. We give it a self-signed TLS
|
||||
* cert in its /data volume so it serves HTTPS out of the box (paired with the ROCKET_TLS env and
|
||||
* the `https:8480` ui_location set in the seeder). Users click through a one-time browser warning;
|
||||
* after that the vault is fully functional offline. Self-signed is the only option on a LAN
|
||||
* appliance with no public DNS to get a trusted cert for.
|
||||
*/
|
||||
private async _runPreinstallActions__Vaultwarden(): Promise<void> {
|
||||
const dataDir = join(process.cwd(), VAULTWARDEN_STORAGE_PATH)
|
||||
const certPath = join(dataDir, 'cert.pem')
|
||||
const keyPath = join(dataDir, 'key.pem')
|
||||
|
||||
this._broadcast(
|
||||
SERVICE_NAMES.VAULTWARDEN,
|
||||
'preinstall',
|
||||
`Running pre-install actions for Vaultwarden...`
|
||||
)
|
||||
|
||||
try {
|
||||
await mkdir(dataDir, { recursive: true })
|
||||
|
||||
// Don't regenerate if a cert is already present — keeps the same cert across reinstalls so
|
||||
// users don't get a fresh browser warning every time, and never clobbers a cert an admin
|
||||
// may have swapped in themselves.
|
||||
const alreadyHasCert = await Promise.all([
|
||||
access(certPath)
|
||||
.then(() => true)
|
||||
.catch(() => false),
|
||||
access(keyPath)
|
||||
.then(() => true)
|
||||
.catch(() => false),
|
||||
]).then(([c, k]) => c && k)
|
||||
|
||||
if (alreadyHasCert) {
|
||||
this._broadcast(
|
||||
SERVICE_NAMES.VAULTWARDEN,
|
||||
'preinstall',
|
||||
`Existing Vaultwarden TLS certificate found — leaving it as-is.`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// 10-year self-signed cert. CN/SAN are cosmetic for a self-signed cert (the browser warns
|
||||
// regardless), but a SAN keeps it structurally valid for clients that require one.
|
||||
const execAsync = promisify(exec)
|
||||
await execAsync(
|
||||
`openssl req -x509 -newkey rsa:2048 -nodes ` +
|
||||
`-keyout "${keyPath}" -out "${certPath}" -days 3650 ` +
|
||||
`-subj "/CN=Project NOMAD Vaultwarden" ` +
|
||||
`-addext "subjectAltName=DNS:nomad,DNS:localhost"`
|
||||
)
|
||||
|
||||
// Lock the private key down; the cert can stay world-readable. Vaultwarden runs as root and
|
||||
// reads both from /data, so no chown is needed (admin writes them as root too).
|
||||
await chmod(keyPath, 0o600)
|
||||
await chmod(certPath, 0o644)
|
||||
|
||||
this._broadcast(
|
||||
SERVICE_NAMES.VAULTWARDEN,
|
||||
'preinstall',
|
||||
`Generated a self-signed HTTPS certificate for Vaultwarden.`
|
||||
)
|
||||
} catch (error: any) {
|
||||
this._broadcast(
|
||||
SERVICE_NAMES.VAULTWARDEN,
|
||||
'preinstall-error',
|
||||
`Failed to prepare the Vaultwarden certificate: ${error.message}`
|
||||
)
|
||||
throw new Error(`Pre-install action failed: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Jellyfin works best when each library points at its own subfolder. Pointing one library at the
|
||||
* whole media root and another at a subfolder inside it makes Jellyfin report a "duplicate path"
|
||||
* and silently drop the nested library's content. To steer users toward the one-folder-per-type
|
||||
* layout (and match the documentation), we pre-create the suggested subfolders under the shared
|
||||
* media root so they're ready to pick in the setup wizard. Idempotent: only missing folders are
|
||||
* created (mkdir is a no-op on existing ones) and nothing the user added is ever touched.
|
||||
*/
|
||||
private async _runPreinstallActions__Jellyfin(): Promise<void> {
|
||||
const mediaDir = join(process.cwd(), MEDIA_STORAGE_PATH)
|
||||
|
||||
this._broadcast(
|
||||
SERVICE_NAMES.JELLYFIN,
|
||||
'preinstall',
|
||||
`Running pre-install actions for Jellyfin...`
|
||||
)
|
||||
|
||||
try {
|
||||
await mkdir(mediaDir, { recursive: true })
|
||||
for (const name of JELLYFIN_MEDIA_SUBFOLDERS) {
|
||||
await mkdir(join(mediaDir, name), { recursive: true })
|
||||
}
|
||||
this._broadcast(
|
||||
SERVICE_NAMES.JELLYFIN,
|
||||
'preinstall',
|
||||
`Prepared media folders: ${JELLYFIN_MEDIA_SUBFOLDERS.join(', ')}.`
|
||||
)
|
||||
} catch (error: any) {
|
||||
this._broadcast(
|
||||
SERVICE_NAMES.JELLYFIN,
|
||||
'preinstall-error',
|
||||
`Failed to prepare the Jellyfin media folders: ${error.message}`
|
||||
)
|
||||
throw new Error(`Pre-install action failed: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async _cleanupFailedInstallation(serviceName: string): Promise<void> {
|
||||
try {
|
||||
const service = await Service.query().where('service_name', serviceName).first()
|
||||
|
|
|
|||
|
|
@ -12,10 +12,11 @@ export class DocsService {
|
|||
'home': 1,
|
||||
'getting-started': 2,
|
||||
'use-cases': 3,
|
||||
'community-add-ons': 4,
|
||||
'faq': 5,
|
||||
'about': 6,
|
||||
'release-notes': 7,
|
||||
'supply-depot-apps': 4,
|
||||
'community-add-ons': 5,
|
||||
'faq': 6,
|
||||
'about': 7,
|
||||
'release-notes': 8,
|
||||
}
|
||||
|
||||
async getDocs() {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,17 @@ import { LSBlockDevice, NomadDiskInfoRaw } from '../../types/system.js'
|
|||
|
||||
export const ZIM_STORAGE_PATH = '/storage/zim'
|
||||
export const KIWIX_LIBRARY_XML_PATH = '/storage/zim/kiwix-library.xml'
|
||||
export const BOOKS_STORAGE_PATH = '/storage/books'
|
||||
// Shared media root (Jellyfin reads it as /media; File Browser shows it as "media"). Per-type
|
||||
// subfolders are pre-created on Jellyfin install — see _runPreinstallActions__Jellyfin.
|
||||
export const MEDIA_STORAGE_PATH = '/storage/media'
|
||||
export const JELLYFIN_MEDIA_SUBFOLDERS = ['Movies', 'TV Shows', 'Music', 'Photos']
|
||||
// Empty Calibre library bundled into the admin image (see install/calibre-empty-library/).
|
||||
// Seeded into storage/books on Calibre-Web install so it doesn't dead-end at db config.
|
||||
export const CALIBRE_EMPTY_LIBRARY_ASSET_PATH = 'assets/calibre/metadata.db'
|
||||
// Vaultwarden's /data volume. A self-signed TLS cert is generated here on install so the
|
||||
// web vault has the secure context (HTTPS) it requires — see _runPreinstallActions__Vaultwarden.
|
||||
export const VAULTWARDEN_STORAGE_PATH = '/storage/vaultwarden'
|
||||
|
||||
export async function listDirectoryContents(path: string): Promise<FileEntry[]> {
|
||||
const entries = await readdir(path, { withFileTypes: true })
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import { SERVICE_NAMES } from './service_names.js'
|
||||
|
||||
// In-app docs page (admin/docs/supply-depot-apps.md) served at /docs/supply-depot-apps.
|
||||
export const SUPPLY_DEPOT_DOC_PAGE = 'supply-depot-apps'
|
||||
|
||||
// Maps a Supply Depot service to its section anchor on that page. Only services listed here get a
|
||||
// "Docs" item in the Manage dropdown, so the link never points at a section that doesn't exist yet.
|
||||
// Each anchor MUST match the heading id set in the .md file (e.g. `## Vaultwarden {% #vaultwarden %}`).
|
||||
// Add an entry here the moment that app's section is written.
|
||||
export const SUPPLY_DEPOT_DOC_ANCHORS: Record<string, string> = {
|
||||
[SERVICE_NAMES.STIRLING_PDF]: 'stirling-pdf',
|
||||
[SERVICE_NAMES.FILEBROWSER]: 'file-browser',
|
||||
[SERVICE_NAMES.CALIBREWEB]: 'calibre-web',
|
||||
[SERVICE_NAMES.IT_TOOLS]: 'it-tools',
|
||||
[SERVICE_NAMES.EXCALIDRAW]: 'excalidraw',
|
||||
[SERVICE_NAMES.HOMEBOX]: 'homebox',
|
||||
[SERVICE_NAMES.VAULTWARDEN]: 'vaultwarden',
|
||||
[SERVICE_NAMES.JELLYFIN]: 'jellyfin',
|
||||
[SERVICE_NAMES.MESHTASTIC_WEB]: 'meshtastic-web',
|
||||
}
|
||||
|
||||
// Returns the in-app docs link for a service, or null if it has no documentation section.
|
||||
export function getSupplyDepotDocLink(serviceName: string): string | null {
|
||||
const anchor = SUPPLY_DEPOT_DOC_ANCHORS[serviceName]
|
||||
return anchor ? `/docs/${SUPPLY_DEPOT_DOC_PAGE}#${anchor}` : null
|
||||
}
|
||||
|
|
@ -206,7 +206,11 @@ export default class ServiceSeeder extends BaseSeeder {
|
|||
],
|
||||
},
|
||||
ExposedPorts: { '8080/tcp': {} },
|
||||
Env: ['DOCKER_ENABLE_SECURITY=false', 'LANGS=en_GB'],
|
||||
// Stirling v2 ignores the old v1 `DOCKER_ENABLE_SECURITY` flag and ships with
|
||||
// `security.enableLogin: true` in settings.yml, so it boots behind a login wall.
|
||||
// For a single-user offline appliance we open it straight to the tools. Users who
|
||||
// want a login can flip this to `true` via Manage > Edit (env overrides settings.yml).
|
||||
Env: ['SECURITY_ENABLELOGIN=false', 'LANGS=en_GB'],
|
||||
}),
|
||||
ui_location: '8400',
|
||||
installed: false,
|
||||
|
|
@ -225,17 +229,44 @@ export default class ServiceSeeder extends BaseSeeder {
|
|||
icon: 'IconFolderOpen',
|
||||
container_image: 'filebrowser/filebrowser:v2',
|
||||
source_repo: 'https://github.com/filebrowser/filebrowser',
|
||||
// Stores the database alongside the files it serves so a single volume covers everything.
|
||||
// User: root — host directories auto-created by Docker are owned root:root 755; FileBrowser's
|
||||
// image runs as UID 1000 by default which can't write to them (DooD: we can't chown on host).
|
||||
container_command: '--root /srv --database /srv/.filebrowser.db',
|
||||
// Browsable root is storage/filebrowser/files (persistent, so files created at the top level
|
||||
// survive updates), with the user-facing content folders mounted in beneath it. We deliberately
|
||||
// do NOT mount the sensitive/app-internal folders (vaultwarden, ollama, qdrant, logs, other
|
||||
// apps' config + *.db). They simply aren't present in the container, so they can't be browsed,
|
||||
// downloaded, or deleted — this is the guardrail. FileBrowser's own rules feature would need a
|
||||
// wrapper script / imported config shipped into the container, which the catalog model doesn't
|
||||
// support, so mount selection is how we scope visibility. To expose another content folder,
|
||||
// add a `${STORAGE}/<folder>:/srv/<folder>` bind here.
|
||||
// The DB lives in storage/filebrowser/db (mounted at /db, a SIBLING of the root, not under it)
|
||||
// so FileBrowser's own .filebrowser.db never shows up in the user's file listing. User: root so
|
||||
// it can read/write folders owned by other UIDs.
|
||||
container_command: '--root /srv --database /db/.filebrowser.db',
|
||||
container_config: JSON.stringify({
|
||||
HostConfig: {
|
||||
RestartPolicy: { Name: 'unless-stopped' },
|
||||
PortBindings: { '80/tcp': [{ HostPort: '8410' }] },
|
||||
Binds: [`${ServiceSeeder.NOMAD_STORAGE_ABS_PATH}/filebrowser:/srv`],
|
||||
Binds: [
|
||||
`${ServiceSeeder.NOMAD_STORAGE_ABS_PATH}/filebrowser/files:/srv`,
|
||||
`${ServiceSeeder.NOMAD_STORAGE_ABS_PATH}/filebrowser/db:/db`,
|
||||
`${ServiceSeeder.NOMAD_STORAGE_ABS_PATH}/books:/srv/books`,
|
||||
`${ServiceSeeder.NOMAD_STORAGE_ABS_PATH}/maps:/srv/maps`,
|
||||
`${ServiceSeeder.NOMAD_STORAGE_ABS_PATH}/media:/srv/media`,
|
||||
`${ServiceSeeder.NOMAD_STORAGE_ABS_PATH}/kb_uploads:/srv/kb_uploads`,
|
||||
`${ServiceSeeder.NOMAD_STORAGE_ABS_PATH}/zim:/srv/zim`,
|
||||
],
|
||||
},
|
||||
ExposedPorts: { '80/tcp': {} },
|
||||
// Without an initial password FileBrowser generates a random one and prints it only to
|
||||
// the container logs, which a non-technical user can't reach. Seed a known admin/nomad
|
||||
// login on first run instead (only applies when the DB doesn't exist yet); the docs tell
|
||||
// users to change it. FB_NOAUTH / --noauth don't work on this image (v2.63.x), so a login
|
||||
// stays, which is the safer default anyway for a read/write/delete file manager.
|
||||
// NOTE: FB_PASSWORD must be a bcrypt hash, not plaintext. The value below is the hash of
|
||||
// "nomad" (generated via `filebrowser hash nomad`). Login is admin / nomad.
|
||||
Env: [
|
||||
'FB_USERNAME=admin',
|
||||
'FB_PASSWORD=$2a$10$Dvu3XTiLxvPTzvdOKu6y6.AmadN6Zt0ddLwK.8MQ.RCIQWunWBQXa',
|
||||
],
|
||||
User: 'root'
|
||||
}),
|
||||
ui_location: '8410',
|
||||
|
|
@ -386,8 +417,11 @@ export default class ServiceSeeder extends BaseSeeder {
|
|||
display_order: 25,
|
||||
description: 'Home inventory and asset management — track everything you own',
|
||||
icon: 'IconBox',
|
||||
container_image: 'ghcr.io/hay-kot/homebox:latest',
|
||||
source_repo: 'https://github.com/hay-kot/homebox',
|
||||
// Maintained fork. The original hay-kot/homebox was archived June 2024;
|
||||
// sysadminsmedia is the official continuation (drop-in: same 7745 port + /data volume,
|
||||
// migrates an existing DB forward, telemetry off by default).
|
||||
container_image: 'ghcr.io/sysadminsmedia/homebox:latest',
|
||||
source_repo: 'https://github.com/sysadminsmedia/homebox',
|
||||
container_command: null,
|
||||
container_config: JSON.stringify({
|
||||
HostConfig: {
|
||||
|
|
@ -422,9 +456,16 @@ export default class ServiceSeeder extends BaseSeeder {
|
|||
Binds: [`${ServiceSeeder.NOMAD_STORAGE_ABS_PATH}/vaultwarden:/data`],
|
||||
},
|
||||
ExposedPorts: { '80/tcp': {} },
|
||||
Env: ['WEBSOCKET_ENABLED=true'],
|
||||
// ROCKET_TLS points at the self-signed cert generated on install by
|
||||
// DockerService._runPreinstallActions__Vaultwarden. Vaultwarden's web vault needs a secure
|
||||
// context (HTTPS) or it refuses to register/unlock, so it ships HTTPS-on-by-default.
|
||||
Env: [
|
||||
'WEBSOCKET_ENABLED=true',
|
||||
'ROCKET_TLS={certs="/data/cert.pem",key="/data/key.pem"}',
|
||||
],
|
||||
}),
|
||||
ui_location: '8480',
|
||||
// https: prefix tells getServiceLink to build an https:// Open link on this port.
|
||||
ui_location: 'https:8480',
|
||||
installed: false,
|
||||
installation_status: 'idle',
|
||||
is_dependency_service: false,
|
||||
|
|
@ -482,9 +523,11 @@ export default class ServiceSeeder extends BaseSeeder {
|
|||
await Service.createMany([...newServices])
|
||||
}
|
||||
|
||||
// Keep container_config, container_command, and metadata in sync for curated services.
|
||||
// Custom services are user-defined and must never be overwritten. User-modified curated
|
||||
// services (a user edited their config) are likewise left alone so the edit survives reboots.
|
||||
// Keep curated services in sync with the catalog. Custom services are user-defined and must
|
||||
// never be overwritten. User-modified curated services (a user edited their config) are
|
||||
// likewise left alone so the edit survives reboots. ui_location is synced too so a catalog
|
||||
// change to an app's link/scheme/port (e.g. Vaultwarden moving to https:8480, or a corrected
|
||||
// internal port) reaches existing non-modified installs on update, not just fresh ones.
|
||||
for (const service of ServiceSeeder.DEFAULT_SERVICES) {
|
||||
const existing = existingServiceMap.get(service.service_name)
|
||||
if (existing && !existing.is_custom && !existing.is_user_modified) {
|
||||
|
|
@ -493,6 +536,7 @@ export default class ServiceSeeder extends BaseSeeder {
|
|||
container_command: service.container_command ?? null,
|
||||
metadata: (service as any).metadata ?? null,
|
||||
category: service.category,
|
||||
ui_location: service.ui_location,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,212 @@
|
|||
# Supply Depot Apps
|
||||
|
||||
The Supply Depot is where you install extra apps onto your NOMAD beyond the built-in tools. Each app runs in its own container on your NOMAD, fully offline, and shows up with an **Open** button once it finishes installing.
|
||||
|
||||
This page covers what you need to know to get up and running with each app *on NOMAD specifically*: whether you log in, what the default credentials are, where your files end up, and anything you need to have on hand first. It does not cover how to use the apps themselves. Each app is its own open-source project with its own documentation, and we link out to that for every one.
|
||||
|
||||
A quick note on logins: some of these apps have their own accounts, separate from your NOMAD login. Where an app asks you to sign in, we tell you the starting credentials and whether you should change them.
|
||||
|
||||
---
|
||||
|
||||
## Stirling PDF {% #stirling-pdf %}
|
||||
|
||||
A full toolbox for working with PDFs, all on your own hardware. Merge and split files, convert to and from PDF, compress, rotate, add or remove passwords, OCR scanned documents so they're searchable, sign, stamp, and redact. There are over 50 tools in here, and because it runs locally, none of your documents ever leave your NOMAD.
|
||||
|
||||
**Official site:** [stirlingpdf.com](https://stirlingpdf.com) · **Source:** [github.com/Stirling-Tools/Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF)
|
||||
|
||||
**First time you open it:** It opens straight to the tools, no login required. We set Stirling up to skip its login screen, since on a NOMAD it's a personal tool on your own network and a password wall just gets in the way. You'll see "Guest" in the bottom-left corner, which is normal.
|
||||
|
||||
**Heads up, it's a slow starter:** Stirling PDF is a big Java application. After you install it, give it 30 to 60 seconds to finish starting up before it loads cleanly. It also wants a good chunk of memory (around a gigabyte), so it's happier on a NOMAD with room to spare.
|
||||
|
||||
**Want a password on it?** If you'd rather Stirling require a login (say a few people share your NOMAD and you want this app locked down), you can turn its login back on from NOMAD:
|
||||
|
||||
1. On the Supply Depot page, find Stirling PDF and click **Manage > Edit**.
|
||||
2. Under **Environment Variables**, change `SECURITY_ENABLELOGIN=false` to `SECURITY_ENABLELOGIN=true`.
|
||||
3. Save. NOMAD rebuilds the app, and the login screen comes back.
|
||||
|
||||
The first time you sign in after that, use username `admin` and password `stirling`. Stirling will make you set your own password right away. Note that this is the only way to flip the login back on: Stirling's own settings menu is locked behind being logged in, so once login is off you turn it on from NOMAD's Edit screen, not from inside Stirling.
|
||||
|
||||
**Your data:** Your settings live in the `storage/stirling-pdf` folder on your NOMAD. The PDFs you work on are uploaded for the operation and downloaded back to your own device. Stirling isn't a long-term library, so it's not holding onto your documents.
|
||||
|
||||
**Where your PDFs come from (and why you don't see your NOMAD's files):** Stirling works on files from whatever device you're using, your laptop, phone, or tablet. You click "Open from computer," pick a PDF, work on it, then download the result back to that device. Stirling can't reach into files stored elsewhere on your NOMAD, so it won't show you your books folder, your Knowledge Base documents, or anything sitting in File Browser. If the PDF you want is already on your NOMAD, download it from wherever it lives first (File Browser, for example), then open that copy in Stirling. It's one extra step, but it's also why your files stay exactly where you put them instead of getting pulled into another app.
|
||||
|
||||
**Works offline:** All of the PDF tools run locally on your NOMAD, so the toolbox itself works fully offline. A few side features reach out to the internet and won't do anything when you're disconnected: the "Google Drive" import option, and the links in the footer (Survey, Discord, GitHub). None of those matter for actually working on PDFs. The one core feature with an online piece is OCR, which reads text out of scanned pages: it ships with English already installed, and adding other languages is the only part that would need a connection.
|
||||
|
||||
## File Browser {% #file-browser %}
|
||||
|
||||
A web-based file manager for your NOMAD. Browse folders, upload and download files, create folders, rename, move, and delete, all from your browser with nothing to install on your computer. It's a handy way to move files on and off the device or tidy things up without dropping into a command line.
|
||||
|
||||
**Official site:** [filebrowser.org](https://filebrowser.org) · **Source:** [github.com/filebrowser/filebrowser](https://github.com/filebrowser/filebrowser)
|
||||
|
||||
**First time you open it:** You'll get a login screen. Sign in with username `admin` and password `nomad`. **Change that password right away.** It's the same default on every NOMAD, so until you change it, anyone on your network who knows it could get in. Click the settings gear, open your profile settings, and set a new password.
|
||||
|
||||
Unlike most of the apps here, File Browser keeps its login on purpose. It can rename and delete real files on your NOMAD, so a password is the right call even on your own network.
|
||||
|
||||
**What you can see:** File Browser shows you your NOMAD's content folders in one place:
|
||||
|
||||
- **books** - e-books, including anything you want Calibre-Web to read
|
||||
- **maps** - downloaded map data
|
||||
- **media** - video, music, and photos, including anything you want Jellyfin to serve
|
||||
- **zim** - downloaded offline content like Wikipedia and other reference libraries
|
||||
- **kb_uploads** - documents you've added to the Knowledge Base
|
||||
|
||||
You can upload, download, rename, move, and delete inside these, and anything you drop in the top level is saved too. The behind-the-scenes folders that the apps actually run on (things like the AI models, the search index, and the password vault) are deliberately kept out of File Browser, so you can't browse or delete them by accident.
|
||||
|
||||
> **A word on deleting:** what you delete here is really gone, there's no recycle bin. The content is mostly replaceable (you can re-download a map or a Wikipedia library), but if you delete a book or a video you added yourself, that copy is gone. Delete with the same care you would on your own computer.
|
||||
|
||||
**Works offline:** Fully offline. File Browser runs entirely on your NOMAD and doesn't reach out to the internet for anything, so it works exactly the same connected or not.
|
||||
|
||||
## Calibre-Web {% #calibre-web %}
|
||||
|
||||
A web-based reader and library manager for your e-book collection. Read books right in your browser, organize them by author, series, and tags, and send them to a Kindle or other e-reader. It pairs with the books folder on your NOMAD, so your whole library lives on the device and goes wherever it goes.
|
||||
|
||||
**Official site:** [github.com/janeczku/calibre-web](https://github.com/janeczku/calibre-web)
|
||||
|
||||
**First time you open it:** Calibre-Web needs a library to point at, and NOMAD sets up an empty one for you during install, so you won't get stuck on a setup error. Here's the one-time flow:
|
||||
|
||||
1. Open Calibre-Web. It lands on a **Database Configuration** screen.
|
||||
2. In the **Location of Calibre Database** box, type `/books` and click **Save**. You'll see "Database Settings updated" and your (empty) library opens.
|
||||
3. That's it for setup. Your library is ready to fill.
|
||||
|
||||
If it asks you to sign in at any point, the default login is `admin` / `admin123`. **Change that password** once you're in (click `admin` in the top right, then Edit). It's the same default on every NOMAD.
|
||||
|
||||
**Adding books:** Uploading through the web page is turned off by default. To turn it on, go to **Admin** (top right) and edit the basic configuration to allow uploads, then you'll get an Upload button. You can also drop e-book files straight into the books folder using File Browser, then use Calibre-Web's "scan" to pick them up.
|
||||
|
||||
**Your data:** Your library lives in the `books` folder on your NOMAD (the same `books` you see in File Browser). Every book you add is stored there, so backing up that one folder backs up your whole collection.
|
||||
|
||||
**Works offline:** Reading and managing your library works fully offline. The one feature that reaches out to the internet is "fetch metadata," which pulls book covers and descriptions from online sources. That part won't do anything when you're offline, but it doesn't affect reading or organizing the books you already have.
|
||||
|
||||
## IT Tools {% #it-tools %}
|
||||
|
||||
A collection of over 100 small utilities you'd otherwise go hunting for online: hash generators, base64 and URL encoders, JSON and SQL formatters, UUID generators, a QR code maker, color converters, and a lot more. It all runs locally on your NOMAD, so you can use it with no internet connection.
|
||||
|
||||
**Official site:** [it-tools.tech](https://it-tools.tech) · **Source:** [github.com/CorentinTh/it-tools](https://github.com/CorentinTh/it-tools)
|
||||
|
||||
**First time you open it:** It opens straight to the tools. No login, no account, no setup. Pick a tool from the sidebar and use it.
|
||||
|
||||
**Your data:** There's nothing to manage. IT Tools doesn't store anything on your NOMAD between sessions, so there are no files, no library to set up, and no credentials to keep track of. It's the simplest app in the Supply Depot.
|
||||
|
||||
**Works offline:** Every tool runs right in your browser against the copy on your NOMAD. Nothing here reaches out to the internet, so all of it keeps working when you're offline.
|
||||
|
||||
## Excalidraw {% #excalidraw %}
|
||||
|
||||
A virtual whiteboard for quick, hand-drawn-style diagrams and sketches. Draw boxes, arrows, and freehand shapes, drop in text and images, and lay out a flowchart, a network diagram, or a rough idea in seconds. The whole thing has a friendly, sketched-on-a-napkin look, and it runs right in your browser.
|
||||
|
||||
**Official site:** [excalidraw.com](https://excalidraw.com) · **Source:** [github.com/excalidraw/excalidraw](https://github.com/excalidraw/excalidraw)
|
||||
|
||||
**First time you open it:** It opens straight to a blank canvas. No login, no account, no setup. Pick a shape from the toolbar and start drawing. You'll see a short welcome note reminding you that your work is saved in your browser, which leads to the one thing worth understanding about Excalidraw on NOMAD.
|
||||
|
||||
**Where your drawings live (read this part):** This version of Excalidraw has no storage on your NOMAD. Your drawing is saved inside the web browser you're using, on that one device. A few things follow from that:
|
||||
|
||||
- Your drawing is **not shared between devices**. What you draw on your laptop won't show up when you open Excalidraw on your phone, because each browser keeps its own copy.
|
||||
- If you **clear your browser data**, or use a private/incognito window, the drawing is gone. There's no copy on the NOMAD to fall back on.
|
||||
- So **save your work to a file.** Use the menu (top-left) to **Save to...** an `.excalidraw` file, and put it somewhere safe, for example your media or documents folder via File Browser. To pick it back up later, use **Open** and load that file. This is the only way to keep a drawing for the long term or move it to another device.
|
||||
|
||||
**Your data:** Because everything stays in your browser, there are no NOMAD folders or credentials to manage for Excalidraw. The files you save are wherever you choose to put them.
|
||||
|
||||
**Works offline:** The whiteboard itself works offline, you can draw, edit, and save files with no internet. Three things to know:
|
||||
|
||||
- **The signature hand-drawn font comes from the internet.** When your NOMAD is offline, Excalidraw can't fetch it and falls back to a plain font, so your diagrams look a little less sketchy. Your drawings themselves are completely unaffected, only the on-screen font changes.
|
||||
- **Excalidraw sends anonymous usage analytics when your NOMAD is online.** The app's makers include basic page-view tracking (through a service called Simple Analytics) that records that the app was opened. It doesn't see your drawings, and it can't reach anything when your NOMAD is offline, but we want you to know it's there since NOMAD is otherwise built to keep to itself.
|
||||
- **A few buttons are cloud features that don't work on NOMAD.** "Live collaboration," "Sign up," and "Excalidraw+" all point to the makers' paid online service and need the internet. They're not part of your offline whiteboard, so you can ignore them. The same goes for the shape **Library** browser, which pulls from an online gallery.
|
||||
|
||||
## Homebox {% #homebox %}
|
||||
|
||||
A home inventory system for keeping track of everything you own. Catalog your belongings into locations and labels, attach photos, record serial numbers, purchase prices, warranty dates, and receipts, and find anything with a search. It's a genuinely useful tool for insurance records, warranty tracking, and knowing what you have and where it is.
|
||||
|
||||
**Official site:** [homebox.software](https://homebox.software) · **Source:** [github.com/sysadminsmedia/homebox](https://github.com/sysadminsmedia/homebox)
|
||||
|
||||
**First time you open it:** Homebox lands on a login screen, but you don't have an account yet, so you create one. Click **Register**, then fill in:
|
||||
|
||||
- **your email** (used as your username to log in),
|
||||
- **your name**,
|
||||
- **a password** (Homebox shows a strength meter and won't let you register until the password is strong enough, so use a real one).
|
||||
|
||||
Click **Register**, then log in with that email and password. The first account you create is the **owner** of this Homebox. There are no default credentials to change, the account is yours from the start.
|
||||
|
||||
**Sharing your NOMAD with others?** By default Homebox lets anyone who can reach it create their own account. That's fine if it's just you, or if you trust everyone on your network. If you'd rather lock it down so no one else can register after you've made your account:
|
||||
|
||||
1. Create your owner account first (above).
|
||||
2. On the Supply Depot page, find Homebox and click **Manage > Edit**.
|
||||
3. Under **Environment Variables**, add `HBOX_OPTIONS_ALLOW_REGISTRATION=false`.
|
||||
4. Save. NOMAD rebuilds the app, and the Register button stops creating new accounts. You can still log in normally.
|
||||
|
||||
**Your data:** Everything Homebox stores lives in one folder on your NOMAD, `storage/homebox`, as a single database file (plus any photos and receipts you attach). Backing up that one folder backs up your entire inventory.
|
||||
|
||||
**Works offline:** Fully offline. Homebox runs entirely on your NOMAD, keeps all your data locally, and has no usage tracking, so it works exactly the same whether your NOMAD is connected or not. The links in its header (GitHub, Discord, the project website) need the internet, but they're just shortcuts to the project's pages and have nothing to do with your inventory.
|
||||
|
||||
## Vaultwarden {% #vaultwarden %}
|
||||
|
||||
A private password manager that runs on your own NOMAD. It's compatible with Bitwarden, so you can store logins, secure notes, and card details in an encrypted vault, and use the official Bitwarden browser extensions and phone apps to access it, all pointed at your NOMAD instead of someone else's cloud.
|
||||
|
||||
**Official site:** [bitwarden.com](https://bitwarden.com) (for the apps and extensions) · **Source:** [github.com/dani-garcia/vaultwarden](https://github.com/dani-garcia/vaultwarden)
|
||||
|
||||
**First time you open it, you'll see a security warning. That's expected, here's why:** A password manager will only run over a secure (HTTPS) connection, so NOMAD sets Vaultwarden up with HTTPS automatically. Because your NOMAD is your own private device and not a public website, it uses a self-signed security certificate, and browsers show a warning the first time they see one. It looks alarming but it's normal for a device on your own network. To get past it once:
|
||||
|
||||
1. Click **Open** on the Vaultwarden card. Your browser shows something like *"Your connection is not private"* or *"Not secure."*
|
||||
2. Click **Advanced**, then **Proceed to (your NOMAD's address)**. (On some browsers the button says "Continue" or "Accept the Risk.")
|
||||
3. You'll land on the Vaultwarden vault. Your browser remembers your choice, so you won't see the warning again on that device.
|
||||
|
||||
**Creating your vault:** On the login page, click **Create account**, then set your **email** and a **master password**.
|
||||
|
||||
> **Your master password cannot be recovered.** Vaultwarden has no "forgot password" email and no reset, by design, because it never sees your password. If you forget it, the vault and everything in it is locked for good. Choose something strong that you won't lose, and consider writing it down somewhere physically safe.
|
||||
|
||||
**Sharing your NOMAD with others?** By default anyone who can reach Vaultwarden can create their own account (each account is separate and encrypted). If you'd rather no one else can register after you've set yours up:
|
||||
|
||||
1. Create your own account first.
|
||||
2. On the Supply Depot page, find Vaultwarden and click **Manage > Edit**.
|
||||
3. Under **Environment Variables**, add `SIGNUPS_ALLOWED=false`.
|
||||
4. Save. NOMAD rebuilds the app and new sign-ups are turned off; existing accounts keep working.
|
||||
|
||||
**Using it from your phone and browser:** Install the official Bitwarden app or browser extension, and on its login screen choose **self-hosted** (or "Server URL") and enter `https://(your NOMAD's address):8480`. Note that some phone apps are stricter about self-signed certificates and may refuse to connect; the web vault you open from NOMAD always works.
|
||||
|
||||
**Your data:** Your encrypted vault lives in the `storage/vaultwarden` folder on your NOMAD. Backing up that folder backs up everything. (The built-in admin panel is turned off unless you set an admin token, which most people don't need.)
|
||||
|
||||
**Works offline:** Fully offline and private. Vaultwarden runs entirely on your NOMAD, stores your vault locally, and phones home to nobody. The Bitwarden apps and extensions also keep a local copy of your vault, so they can read your passwords even when your NOMAD or your phone is offline.
|
||||
|
||||
## Jellyfin {% #jellyfin %}
|
||||
|
||||
Your own media server. Point Jellyfin at a folder of movies, TV shows, music, and photos on your NOMAD, and it organizes everything with artwork and details and streams it to a web browser, phone, tablet, smart TV, or the Jellyfin apps. It's a private, offline alternative to the big streaming services for media you already own.
|
||||
|
||||
**Official site:** [jellyfin.org](https://jellyfin.org) · **Source:** [github.com/jellyfin/jellyfin](https://github.com/jellyfin/jellyfin)
|
||||
|
||||
**First time you open it, you'll go through a setup wizard.** It's a few quick screens:
|
||||
|
||||
1. **Language** - pick your display language and click Next.
|
||||
2. **Create your admin account** - enter a username and password. This is the main account that controls the server, so give it a real password and keep track of it. (You can add more users, including limited ones for kids, later from the Dashboard.)
|
||||
3. **Add your media** - click **Add Media Library** and pick a content type. To make this easy, NOMAD has already created a matching folder for each type inside your media folder, so you just point each library at the one that fits:
|
||||
- **Movies** library → the `Movies` folder
|
||||
- **Shows** library → the `TV Shows` folder
|
||||
- **Music** library → the `Music` folder
|
||||
- **Photos** library → the `Photos` folder
|
||||
|
||||
**Point each library at its own folder, not at the whole `media` folder.** This matters: if you point one library at `media` itself (which contains all the others) and another library at, say, `Music` inside it, Jellyfin sees the same files claimed twice, calls it a "duplicate path," and your music silently won't show up. One folder per library keeps everything tidy and working. You can also skip this step and add libraries later from the Dashboard.
|
||||
4. **Metadata, remote access, finish** - accept the defaults on the remaining screens and finish. Then sign in with the account you just made.
|
||||
|
||||
**Getting your media in:** Put your files in the matching subfolder of the **media** folder on your NOMAD (the same `media` folder you see in File Browser): movies in **Movies**, series in **TV Shows**, music in **Music** (a folder per album works great), pictures in **Photos**. The easiest workflow is to upload files with File Browser (or drop them in however you like), then in Jellyfin click **Scan Library** to pick them up. Jellyfin reads sub-folders, so a whole album folder dropped into **Music** comes in as one album. It also works best when files are named clearly (for example `Movie Name (2020).mp4`), which helps it match the right artwork and details.
|
||||
|
||||
**Your data:** Your media lives in `storage/media`. Jellyfin's own settings, user accounts, and the artwork it downloads live in `storage/jellyfin`. Your media files are never modified, Jellyfin only reads them.
|
||||
|
||||
**Works offline:** Streaming your own media works fully offline, that's the whole point. The one piece that uses the internet is **fetching metadata**: when Jellyfin adds a movie or show, it tries to download a cover image, description, and cast info from online databases. Offline, it can't do that, so items show up with plain names and no artwork, but they still play perfectly. Once you're back online, a library scan fills in the missing artwork.
|
||||
|
||||
> **A note on playback performance:** Jellyfin plays most files effortlessly, but if a video's format isn't supported by your device, Jellyfin has to convert it on the fly ("transcoding"), which is heavy work for the processor. NOMAD doesn't set up graphics-card acceleration for this by default, so very large or high-resolution videos may stutter on a modest NOMAD. Playing files in a widely-supported format (like MP4/H.264) avoids transcoding and plays smoothest.
|
||||
|
||||
## Meshtastic Web {% #meshtastic-web %}
|
||||
|
||||
A browser-based control panel for [Meshtastic](https://meshtastic.org) devices. Meshtastic is off-grid, long-range radio messaging: small, inexpensive LoRa radios that form their own mesh network and send text messages and GPS locations for miles with no cell service, no internet, and no fees. This app is how you configure those radios and read and send messages from a full-size screen.
|
||||
|
||||
**Official site:** [meshtastic.org](https://meshtastic.org) · **Source:** [github.com/meshtastic/web](https://github.com/meshtastic/web)
|
||||
|
||||
**You need a Meshtastic radio to use this.** This app is just the control panel. On its own it opens to a "No devices connected" screen, because the actual work happens on a physical Meshtastic device (and the network of other radios it talks to). If you don't have one yet, the app won't do much.
|
||||
|
||||
**First time you open it:** It opens straight in, no login. Click **New Connection** and you'll see three ways to connect to your radio:
|
||||
|
||||
- **HTTP** - connect to a radio that's already joined to your Wi-Fi, by typing its IP address. **This is the method to use on NOMAD** (see below).
|
||||
- **Bluetooth** - pair with a nearby radio over Bluetooth.
|
||||
- **Serial** - connect to a radio plugged into a USB port.
|
||||
|
||||
**The NOMAD-specific catch (Bluetooth and Serial need HTTPS):** Browsers only allow a website to use Bluetooth or USB when the page is loaded over a secure (HTTPS) connection. NOMAD serves Meshtastic Web over plain HTTP, so on NOMAD the **Bluetooth and Serial options won't connect**, your browser blocks them. The one that works is **HTTP**: put your Meshtastic radio on the same Wi-Fi network (Meshtastic radios can join Wi-Fi), then connect to it here by its IP address. If you specifically need to pair over USB or Bluetooth, do that from the official Meshtastic phone app or the Meshtastic website instead.
|
||||
|
||||
**Your data:** There's nothing to set up or store on your NOMAD for this app. Your radio's settings live on the radio itself, and this app's preferences live in your browser. There's no NOMAD folder to manage.
|
||||
|
||||
**Works offline:** Fully offline, which is the entire point of Meshtastic. The app is served from your NOMAD, and talking to your radios happens over your local network or radio, never the internet. The only online bits are the links in the footer (Vercel, legal), which don't matter for using your mesh.
|
||||
|
|
@ -1,8 +1,22 @@
|
|||
import { Head } from '@inertiajs/react'
|
||||
import { useEffect } from 'react'
|
||||
import MarkdocRenderer from '~/components/MarkdocRenderer'
|
||||
import DocsLayout from '~/layouts/DocsLayout'
|
||||
|
||||
export default function Show({ content }: { content: any; }) {
|
||||
// Deep-link support: when arriving at /docs/<slug>#<anchor> (e.g. from a Supply Depot app's
|
||||
// "Docs" menu item), scroll to that section. The content renders synchronously from props, so a
|
||||
// double rAF lets the headings paint before we look one up.
|
||||
useEffect(() => {
|
||||
const hash = decodeURIComponent(window.location.hash.replace(/^#/, ''))
|
||||
if (!hash) return
|
||||
requestAnimationFrame(() =>
|
||||
requestAnimationFrame(() => {
|
||||
document.getElementById(hash)?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
})
|
||||
)
|
||||
}, [content])
|
||||
|
||||
return (
|
||||
<DocsLayout>
|
||||
<Head title={'Documentation'} />
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Head } from '@inertiajs/react'
|
|||
import { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
IconAlertTriangle,
|
||||
IconBook,
|
||||
IconBox,
|
||||
IconBrandDocker,
|
||||
IconChartBar,
|
||||
|
|
@ -30,6 +31,7 @@ import useErrorNotification from '~/hooks/useErrorNotification'
|
|||
import useServiceInstallationActivity from '~/hooks/useServiceInstallationActivity'
|
||||
import { ServiceSlim } from '../../types/services'
|
||||
import { getServiceLink } from '~/lib/navigation'
|
||||
import { getSupplyDepotDocLink } from '../../constants/supply_depot_docs'
|
||||
import api from '~/lib/api'
|
||||
import { toTitleCase } from '../../app/utils/misc'
|
||||
|
||||
|
|
@ -626,6 +628,9 @@ function AppCard({
|
|||
const uiIsPath = !!service.ui_location && service.ui_location.startsWith('/')
|
||||
const uiIsHttps = /^https:/.test(service.ui_location || '')
|
||||
const uiPort = service.ui_location && !uiIsPath ? service.ui_location.replace(/^https?:/, '') : null
|
||||
// Per-app documentation link (in-app docs page, anchored to this app's section). Null for apps
|
||||
// without a doc section (custom apps, undocumented catalog apps) so the Docs item is hidden.
|
||||
const docLink = getSupplyDepotDocLink(service.service_name)
|
||||
|
||||
function toggleDropdown(e: React.MouseEvent) {
|
||||
e.stopPropagation()
|
||||
|
|
@ -759,6 +764,18 @@ function AppCard({
|
|||
|
||||
{isDropdownOpen && (
|
||||
<div className="absolute right-0 bottom-full mb-1 w-44 bg-surface-primary border border-surface-secondary rounded-lg shadow-xl z-20 overflow-hidden">
|
||||
{docLink && (
|
||||
<a
|
||||
href={docLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="flex items-center gap-2 w-full px-3 py-2 text-xs transition-colors text-left cursor-pointer text-text-primary hover:bg-surface-secondary"
|
||||
>
|
||||
<IconBook className="h-4 w-4" />
|
||||
Docs
|
||||
</a>
|
||||
)}
|
||||
{isStopped && (
|
||||
<DropdownItem icon={<IconPlayerPlay className="h-4 w-4" />} label="Start" onClick={onStart} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
# Empty Calibre library (Calibre-Web seed)
|
||||
|
||||
`metadata.db` is an empty Calibre library database, generated once with
|
||||
`calibredb --with-library <dir> list` (calibre 9.9).
|
||||
|
||||
Calibre-Web cannot create a library from scratch. On a fresh NOMAD it would dead-end
|
||||
at the "Database Configuration" page asking for an existing Calibre database. To avoid
|
||||
that, the Calibre-Web pre-install action seeds this file into `storage/books` (only when
|
||||
no `metadata.db` is already there) and hands ownership to the container's user, so the
|
||||
user just points Calibre-Web at `/books` once and starts adding books.
|
||||
|
||||
This file is bundled into the admin image via the Dockerfile and copied at install time
|
||||
by `DockerService._runPreinstallActions__CalibreWeb()`. To regenerate it:
|
||||
|
||||
```sh
|
||||
docker run --rm -v "$PWD/lib:/books" --entrypoint bash lscr.io/linuxserver/calibre:latest \
|
||||
-c "calibredb --with-library /books list"
|
||||
# then copy lib/metadata.db here
|
||||
```
|
||||
Binary file not shown.
Loading…
Reference in New Issue