Update documentation: git repo info, schedules, restore, WHM pages
- Add Repository section with SSH/HTTP/Web UI URLs - Rewrite Schedules section: decoupled from remotes, hourly/daily/weekly/monthly/custom types - Add schedule types table with cron patterns - Add mailbox restore to commands and restore workflows - Update commands list with full schedule CRUD and comma-separated remotes - Update WHM plugin docs: restore page, schedule toggles, Runner.pm - Update file layout: schedules.d/, rclone.sh, schedule.conf.example - Add uninstall instructions - Remove empty Production Server heading Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
14
CLAUDE.md
14
CLAUDE.md
@@ -553,7 +553,19 @@ cd whm/gniza-whm/assets && npm install && npm run build:css
|
|||||||
|
|
||||||
### Deploying changes to a server
|
### Deploying changes to a server
|
||||||
|
|
||||||
Use the install script for fresh installs or upgrades:
|
Commit and push to git, then run the install script on the target server:
|
||||||
```bash
|
```bash
|
||||||
|
# From dev machine: push changes
|
||||||
|
git add -A && git commit -m "description" && git push
|
||||||
|
|
||||||
|
# On target server: install/upgrade
|
||||||
curl -sSL http://192.168.100.100:3001/shukivaknin/gniza/raw/branch/main/scripts/install.sh | bash
|
curl -sSL http://192.168.100.100:3001/shukivaknin/gniza/raw/branch/main/scripts/install.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Repository
|
||||||
|
|
||||||
|
| | URL |
|
||||||
|
|---|-----|
|
||||||
|
| **Git (SSH)** | `ssh://git@192.168.100.100:2222/shukivaknin/gniza.git` |
|
||||||
|
| **Git (HTTP)** | `http://192.168.100.100:3001/shukivaknin/gniza.git` |
|
||||||
|
| **Web UI** | http://192.168.100.100:3001/shukivaknin/gniza |
|
||||||
|
|||||||
179
README.md
179
README.md
@@ -4,6 +4,14 @@ cPanel Backup, Restore & Disaster Recovery tool.
|
|||||||
|
|
||||||
Uses `pkgacct --nocompress --skiphomedir` for account backups, gzips SQL files individually, and transfers everything (including homedirs) to remote destinations with incremental snapshots. Supports three remote types: **SSH** (rsync with hardlink-based `--link-dest`), **Amazon S3** / S3-compatible (via rclone), and **Google Drive** (via rclone). Supports multiple remote destinations with independent schedules and retention policies.
|
Uses `pkgacct --nocompress --skiphomedir` for account backups, gzips SQL files individually, and transfers everything (including homedirs) to remote destinations with incremental snapshots. Supports three remote types: **SSH** (rsync with hardlink-based `--link-dest`), **Amazon S3** / S3-compatible (via rclone), and **Google Drive** (via rclone). Supports multiple remote destinations with independent schedules and retention policies.
|
||||||
|
|
||||||
|
## Repository
|
||||||
|
|
||||||
|
| | URL |
|
||||||
|
|---|-----|
|
||||||
|
| **Git (SSH)** | `ssh://git@192.168.100.100:2222/shukivaknin/gniza.git` |
|
||||||
|
| **Git (HTTP)** | `http://192.168.100.100:3001/shukivaknin/gniza.git` |
|
||||||
|
| **Web UI** | http://192.168.100.100:3001/shukivaknin/gniza |
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -18,11 +26,20 @@ cd gniza
|
|||||||
sudo bash scripts/install.sh
|
sudo bash scripts/install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To uninstall:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash /usr/local/gniza/scripts/uninstall.sh # from installed copy
|
||||||
|
# or
|
||||||
|
sudo bash scripts/uninstall.sh # from repo clone
|
||||||
|
```
|
||||||
|
|
||||||
|
The uninstall script removes the CLI, WHM plugin, and symlink. Config (`/etc/gniza/`), logs (`/var/log/gniza/`), and cron entries are left for manual cleanup.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Interactive setup (creates config + first remote + optional schedule)
|
||||||
# Interactive setup (creates config + first remote)
|
|
||||||
sudo gniza init
|
sudo gniza init
|
||||||
|
|
||||||
# Add additional remote destinations
|
# Add additional remote destinations
|
||||||
@@ -34,24 +51,30 @@ sudo gniza backup --dry-run
|
|||||||
# Run backup
|
# Run backup
|
||||||
sudo gniza backup
|
sudo gniza backup
|
||||||
|
|
||||||
# Install cron schedules for all remotes
|
# Back up to specific remotes
|
||||||
sudo gniza schedule install
|
sudo gniza backup --remote=nas,offsite
|
||||||
```
|
```
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
```
|
```
|
||||||
gniza backup [--account=NAME] [--remote=NAME] [--dry-run]
|
gniza backup [--account=NAME] [--remote=NAME[,NAME2]] [--dry-run]
|
||||||
gniza restore account <name> [--remote=NAME] [--timestamp=TS] [--force]
|
gniza restore account <name> --remote=NAME [--timestamp=TS] [--force]
|
||||||
gniza restore files <name> [--remote=NAME] [--path=subpath] [--timestamp=TS]
|
gniza restore files <name> --remote=NAME [--path=subpath] [--timestamp=TS]
|
||||||
gniza restore database <name> <dbname> [--remote=NAME] [--timestamp=TS]
|
gniza restore database <name> <dbname> --remote=NAME [--timestamp=TS]
|
||||||
gniza restore server [--remote=NAME] [--timestamp=TS]
|
gniza restore mailbox <name> <email@domain> --remote=NAME [--timestamp=TS]
|
||||||
|
gniza restore server --remote=NAME [--timestamp=TS]
|
||||||
gniza list [--account=NAME] [--remote=NAME]
|
gniza list [--account=NAME] [--remote=NAME]
|
||||||
gniza verify [--account=NAME] [--remote=NAME]
|
gniza verify [--account=NAME] [--remote=NAME]
|
||||||
gniza status
|
gniza status
|
||||||
gniza remote list
|
gniza remote list
|
||||||
gniza remote delete <name>
|
gniza remote delete <name>
|
||||||
gniza schedule {install|show|remove}
|
gniza schedule add <name>
|
||||||
|
gniza schedule delete <name>
|
||||||
|
gniza schedule list
|
||||||
|
gniza schedule install
|
||||||
|
gniza schedule show
|
||||||
|
gniza schedule remove
|
||||||
gniza init
|
gniza init
|
||||||
gniza init remote <name>
|
gniza init remote <name>
|
||||||
gniza version
|
gniza version
|
||||||
@@ -63,7 +86,7 @@ gniza help
|
|||||||
| Option | Description |
|
| Option | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `--config=PATH` | Alternate config file (default: `/etc/gniza/gniza.conf`) |
|
| `--config=PATH` | Alternate config file (default: `/etc/gniza/gniza.conf`) |
|
||||||
| `--remote=NAME` | Target a specific remote from `/etc/gniza/remotes.d/` |
|
| `--remote=NAME[,NAME2]` | Target specific remote(s) from `/etc/gniza/remotes.d/` (comma-separated) |
|
||||||
| `--debug` | Enable debug logging |
|
| `--debug` | Enable debug logging |
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
@@ -100,7 +123,7 @@ See `etc/gniza.conf.example` for the full template.
|
|||||||
|
|
||||||
### Remote Destinations
|
### Remote Destinations
|
||||||
|
|
||||||
Back up to one or more destinations with independent schedules, retention policies, and bandwidth limits. Supports SSH, Amazon S3 (and S3-compatible services like MinIO, Wasabi, Backblaze B2), and Google Drive. Remote destinations are configured as individual files in `/etc/gniza/remotes.d/`.
|
Back up to one or more destinations with independent retention policies and bandwidth limits. Supports SSH, Amazon S3 (and S3-compatible services like MinIO, Wasabi, Backblaze B2), and Google Drive. Remote destinations are configured as individual files in `/etc/gniza/remotes.d/`.
|
||||||
|
|
||||||
#### Setup
|
#### Setup
|
||||||
|
|
||||||
@@ -160,14 +183,15 @@ See `etc/remote.conf.example` for the full template.
|
|||||||
|
|
||||||
#### Usage
|
#### Usage
|
||||||
|
|
||||||
Without `--remote`, backup/list/verify operate on **all** configured remotes. Restore requires `--remote` to specify the source.
|
Without `--remote`, backup/list/verify operate on **all** configured remotes. Restore always requires `--remote` to specify the source.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Back up to all remotes
|
# Back up to all remotes
|
||||||
sudo gniza backup
|
sudo gniza backup
|
||||||
|
|
||||||
# Back up to a specific remote
|
# Back up to specific remote(s)
|
||||||
sudo gniza backup --remote=nas
|
sudo gniza backup --remote=nas
|
||||||
|
sudo gniza backup --remote=nas,offsite
|
||||||
|
|
||||||
# List snapshots on a specific remote
|
# List snapshots on a specific remote
|
||||||
sudo gniza list --remote=offsite
|
sudo gniza list --remote=offsite
|
||||||
@@ -176,23 +200,59 @@ sudo gniza list --remote=offsite
|
|||||||
sudo gniza restore account johndoe --remote=nas
|
sudo gniza restore account johndoe --remote=nas
|
||||||
```
|
```
|
||||||
|
|
||||||
## Schedule Management
|
### Schedules
|
||||||
|
|
||||||
Manage cron entries based on each remote's `SCHEDULE` settings. Each remote gets a tagged cron entry for clean install/remove.
|
Schedules are **decoupled from remotes**. Each schedule lives in `/etc/gniza/schedules.d/<name>.conf` and defines when backups run and which remotes to target. This allows multiple schedules targeting different sets of remotes.
|
||||||
|
|
||||||
|
#### Schedule Config Format
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo gniza schedule install # Install cron entries for all remotes
|
SCHEDULE="daily" # hourly, daily, weekly, monthly, custom
|
||||||
sudo gniza schedule show # Show current gniza cron entries
|
SCHEDULE_TIME="02:00" # HH:MM (24-hour)
|
||||||
sudo gniza schedule remove # Remove all gniza cron entries
|
SCHEDULE_DAY="" # hours between backups (1-23) for hourly
|
||||||
|
# day-of-week (0=Sun..6=Sat) for weekly
|
||||||
|
# day-of-month (1-28) for monthly
|
||||||
|
SCHEDULE_CRON="" # Full cron expression for SCHEDULE=custom
|
||||||
|
REMOTES="" # Comma-separated remote names (empty = all)
|
||||||
```
|
```
|
||||||
|
|
||||||
Example crontab entries:
|
#### Managing Schedules
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Interactive schedule creation
|
||||||
|
sudo gniza schedule add nightly
|
||||||
|
|
||||||
|
# List configured schedules
|
||||||
|
sudo gniza schedule list
|
||||||
|
|
||||||
|
# Delete a schedule
|
||||||
|
sudo gniza schedule delete nightly
|
||||||
|
|
||||||
|
# Install all schedules to crontab
|
||||||
|
sudo gniza schedule install
|
||||||
|
|
||||||
|
# Show current gniza cron entries
|
||||||
|
sudo gniza schedule show
|
||||||
|
|
||||||
|
# Remove all gniza cron entries
|
||||||
|
sudo gniza schedule remove
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Schedule Types
|
||||||
|
|
||||||
|
| Type | SCHEDULE_DAY | Cron Pattern | Example |
|
||||||
|
|------|-------------|--------------|---------|
|
||||||
|
| `hourly` | Hours interval (1-23) | `<min> */<interval> * * *` | Every 2 hours at :15 → `15 */2 * * *` |
|
||||||
|
| `daily` | — | `<min> <hour> * * *` | Daily at 02:00 → `0 2 * * *` |
|
||||||
|
| `weekly` | Day-of-week (0-6) | `<min> <hour> * * <dow>` | Sundays at 03:00 → `0 3 * * 0` |
|
||||||
|
| `monthly` | Day-of-month (1-28) | `<min> <hour> <dom> * *` | 1st at 02:00 → `0 2 1 * *` |
|
||||||
|
| `custom` | — | `SCHEDULE_CRON` (5-field) | `*/30 * * * *` |
|
||||||
|
|
||||||
|
Each schedule gets a tagged cron entry for clean install/remove:
|
||||||
|
|
||||||
```
|
```
|
||||||
# gniza:nas
|
# gniza:nightly
|
||||||
0 2 * * * /usr/local/bin/gniza backup --remote=nas >> /var/log/gniza/cron-nas.log 2>&1
|
0 2 * * * /usr/local/bin/gniza backup --remote=nas,offsite >> /var/log/gniza/cron-nightly.log 2>&1
|
||||||
# gniza:offsite
|
|
||||||
0 3 * * 0 /usr/local/bin/gniza backup --remote=offsite >> /var/log/gniza/cron-offsite.log 2>&1
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Remote Directory Structure
|
## Remote Directory Structure
|
||||||
@@ -239,7 +299,7 @@ All rsync operations use `--rsync-path="rsync --fake-super"` to preserve file ow
|
|||||||
## Backup Flow
|
## Backup Flow
|
||||||
|
|
||||||
1. Load main config (TEMP_DIR, LOG_DIR, accounts, notifications)
|
1. Load main config (TEMP_DIR, LOG_DIR, accounts, notifications)
|
||||||
2. Resolve target remotes (`--remote=NAME` or all from `remotes.d/`)
|
2. Resolve target remotes (`--remote=NAME[,NAME2]` or all from `remotes.d/`)
|
||||||
3. Test connectivity to all targets upfront (SSH or rclone by type)
|
3. Test connectivity to all targets upfront (SSH or rclone by type)
|
||||||
4. For each account:
|
4. For each account:
|
||||||
- `pkgacct` ONCE
|
- `pkgacct` ONCE
|
||||||
@@ -258,12 +318,13 @@ If one remote fails for an account, other remotes still receive the backup.
|
|||||||
|
|
||||||
## Restore Workflows
|
## Restore Workflows
|
||||||
|
|
||||||
| Workflow | Description |
|
| Workflow | Command | Description |
|
||||||
|----------|-------------|
|
|----------|---------|-------------|
|
||||||
| **Full account** | Downloads pkgacct data, decompresses SQL, runs `/scripts/restorepkg`, rsyncs homedir, fixes ownership |
|
| **Full account** | `restore account <user>` | Downloads pkgacct data, decompresses SQL, runs `/scripts/restorepkg`, rsyncs homedir, fixes ownership |
|
||||||
| **Selective files** | Rsyncs specific path from remote homedir backup |
|
| **Selective files** | `restore files <user> --path=...` | Rsyncs specific path from remote homedir backup |
|
||||||
| **Single database** | Downloads SQL dump + grants, imports via `mysql` |
|
| **Single database** | `restore database <user> <dbname>` | Downloads SQL dump + grants, imports via `mysql` |
|
||||||
| **Full server rebuild** | Restores all accounts found on the remote |
|
| **Mailbox** | `restore mailbox <user> <email@domain>` | Restores individual email account from `mail/<domain>/<mailbox>/` |
|
||||||
|
| **Full server** | `restore server` | Restores all accounts found on the remote |
|
||||||
|
|
||||||
All restore commands require `--remote=NAME` to specify the source.
|
All restore commands require `--remote=NAME` to specify the source.
|
||||||
|
|
||||||
@@ -288,6 +349,7 @@ All restore commands require `--remote=NAME` to specify the source.
|
|||||||
│ ├── config.sh # Config loading and validation
|
│ ├── config.sh # Config loading and validation
|
||||||
│ ├── locking.sh # flock-based concurrency control
|
│ ├── locking.sh # flock-based concurrency control
|
||||||
│ ├── ssh.sh # SSH connectivity, remote_exec, rsync SSH cmd
|
│ ├── ssh.sh # SSH connectivity, remote_exec, rsync SSH cmd
|
||||||
|
│ ├── rclone.sh # Rclone transport layer for S3/GDrive
|
||||||
│ ├── accounts.sh # cPanel account discovery and filtering
|
│ ├── accounts.sh # cPanel account discovery and filtering
|
||||||
│ ├── pkgacct.sh # pkgacct execution, SQL gzip, temp cleanup
|
│ ├── pkgacct.sh # pkgacct execution, SQL gzip, temp cleanup
|
||||||
│ ├── snapshot.sh # Timestamp naming, list/resolve snapshots
|
│ ├── snapshot.sh # Timestamp naming, list/resolve snapshots
|
||||||
@@ -295,28 +357,32 @@ All restore commands require `--remote=NAME` to specify the source.
|
|||||||
│ ├── retention.sh # Prune old snapshots beyond RETENTION_COUNT
|
│ ├── retention.sh # Prune old snapshots beyond RETENTION_COUNT
|
||||||
│ ├── verify.sh # Remote backup integrity checks
|
│ ├── verify.sh # Remote backup integrity checks
|
||||||
│ ├── notify.sh # Email notifications
|
│ ├── notify.sh # Email notifications
|
||||||
│ ├── restore.sh # Full account, files, database, server restore
|
│ ├── restore.sh # Full account, files, database, mailbox, server restore
|
||||||
│ ├── remotes.sh # Remote discovery and context switching
|
│ ├── remotes.sh # Remote discovery and context switching
|
||||||
│ └── schedule.sh # Cron management for multi-remote schedules
|
│ └── schedule.sh # Cron management for decoupled schedules
|
||||||
└── etc/
|
└── etc/
|
||||||
├── gniza.conf.example # Main config template
|
├── gniza.conf.example # Main config template
|
||||||
└── remote.conf.example # Remote destination template
|
├── remote.conf.example # Remote destination template
|
||||||
|
└── schedule.conf.example # Schedule template
|
||||||
|
|
||||||
/etc/gniza/ # Runtime configuration
|
/etc/gniza/ # Runtime configuration
|
||||||
├── gniza.conf # Main config
|
├── gniza.conf # Main config
|
||||||
└── remotes.d/ # Remote destination configs
|
├── remotes.d/ # Remote destination configs
|
||||||
├── nas.conf
|
│ ├── nas.conf
|
||||||
└── offsite.conf
|
│ └── offsite.conf
|
||||||
|
└── schedules.d/ # Schedule configs
|
||||||
|
├── nightly.conf
|
||||||
|
└── weekly-offsite.conf
|
||||||
|
|
||||||
/var/log/gniza/ # Log files
|
/var/log/gniza/ # Log files
|
||||||
├── gniza-20260303-020000.log # Per-run logs
|
├── gniza-20260303-020000.log # Per-run logs
|
||||||
├── cron-nas.log # Per-remote cron output
|
├── cron-nightly.log # Per-schedule cron output
|
||||||
└── cron-offsite.log
|
└── cron-weekly-offsite.log
|
||||||
```
|
```
|
||||||
|
|
||||||
## WHM Plugin
|
## WHM Plugin
|
||||||
|
|
||||||
gniza includes a WHM plugin for managing backups through the cPanel/WHM web interface.
|
gniza includes a WHM plugin for managing backups through the cPanel/WHM web interface. All pages use **Tailwind CSS v4** with **DaisyUI v5** for styling.
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
@@ -330,7 +396,7 @@ When gniza is not yet configured (no remotes in `/etc/gniza/remotes.d/`), the da
|
|||||||
|
|
||||||
1. **SSH Key** — Detects existing keys in `/root/.ssh/` (`id_ed25519`, `id_rsa`, `id_ecdsa`, `id_dsa`). Lets you select one or enter a custom path. Shows `ssh-keygen` and `ssh-copy-id` commands for creating new keys.
|
1. **SSH Key** — Detects existing keys in `/root/.ssh/` (`id_ed25519`, `id_rsa`, `id_ecdsa`, `id_dsa`). Lets you select one or enter a custom path. Shows `ssh-keygen` and `ssh-copy-id` commands for creating new keys.
|
||||||
|
|
||||||
2. **Remote Destination** — Configure the first remote: name, host, port, user, SSH key (pre-filled from step 1), base path, bandwidth limit, and retention count. Creates a config file in `/etc/gniza/remotes.d/`.
|
2. **Remote Destination** — Configure the first remote: name, type (SSH/S3/GDrive), connection details, base path, bandwidth limit, and retention count. Tests the connection before saving. Creates a config file in `/etc/gniza/remotes.d/`.
|
||||||
|
|
||||||
3. **Schedule** — Optionally set a backup schedule (hourly/daily/weekly/monthly/custom) for the new remote. Installs the cron entry automatically. Can be skipped.
|
3. **Schedule** — Optionally set a backup schedule (hourly/daily/weekly/monthly/custom) for the new remote. Installs the cron entry automatically. Can be skipped.
|
||||||
|
|
||||||
@@ -341,16 +407,11 @@ The wizard is also accessible anytime from the dashboard quick links ("Run Setup
|
|||||||
| Page | URL | Description |
|
| Page | URL | Description |
|
||||||
|------|-----|-------------|
|
|------|-----|-------------|
|
||||||
| Dashboard | `index.cgi` | Overview, remote listing, cron status, quick links |
|
| Dashboard | `index.cgi` | Overview, remote listing, cron status, quick links |
|
||||||
|
| Remotes | `remotes.cgi` | Add/edit/delete remote destinations (SSH/S3/GDrive) with connection testing |
|
||||||
|
| Schedules | `schedules.cgi` | Add/edit/delete schedules, per-schedule cron toggle |
|
||||||
|
| Restore | `restore.cgi` | Restore workflow: select account, remote, snapshot, then restore type (full/files/database/mailbox) |
|
||||||
| Settings | `settings.cgi` | Edit main config (`/etc/gniza/gniza.conf`) |
|
| Settings | `settings.cgi` | Edit main config (`/etc/gniza/gniza.conf`) |
|
||||||
| Remotes | `remotes.cgi` | Add/edit/delete remote destinations |
|
| Setup Wizard | `setup.cgi` | Guided initial configuration (3 steps) |
|
||||||
| Schedules | `schedules.cgi` | View and manage cron schedules |
|
|
||||||
| Setup Wizard | `setup.cgi` | Guided initial configuration |
|
|
||||||
|
|
||||||
### SSH Key Guidance
|
|
||||||
|
|
||||||
When adding a new remote (via Remotes > Add Remote), an SSH key guidance block is displayed above the form showing:
|
|
||||||
- Detected existing keys on the server
|
|
||||||
- Commands to generate a new key and copy it to the remote
|
|
||||||
|
|
||||||
### Plugin File Layout
|
### Plugin File Layout
|
||||||
|
|
||||||
@@ -361,19 +422,23 @@ whm/
|
|||||||
├── index.cgi # Dashboard
|
├── index.cgi # Dashboard
|
||||||
├── setup.cgi # Setup wizard (3 steps)
|
├── setup.cgi # Setup wizard (3 steps)
|
||||||
├── settings.cgi # Main config editor
|
├── settings.cgi # Main config editor
|
||||||
├── remotes.cgi # Remote CRUD
|
├── remotes.cgi # Remote CRUD (SSH/S3/GDrive)
|
||||||
├── schedules.cgi # Cron management
|
├── schedules.cgi # Schedule CRUD + cron toggles
|
||||||
|
├── restore.cgi # Restore workflow (account → remote → snapshot → type)
|
||||||
├── assets/
|
├── assets/
|
||||||
│ └── gniza-whm.css # Shared styles
|
│ ├── gniza-whm.css # Built Tailwind/DaisyUI CSS (committed)
|
||||||
|
│ └── src/
|
||||||
|
│ ├── input.css # Tailwind v4 entry point
|
||||||
|
│ ├── safelist.html # Class safelist for Tailwind scanner
|
||||||
|
│ └── package.json # Build toolchain
|
||||||
└── lib/GnizaWHM/
|
└── lib/GnizaWHM/
|
||||||
├── Config.pm # Config parser/writer (pure Perl)
|
├── Config.pm # Config parser/writer (pure Perl)
|
||||||
├── Validator.pm # Input validation
|
├── Validator.pm # Input validation
|
||||||
├── Cron.pm # Cron read + allowlisted gniza commands
|
├── Cron.pm # Cron read + per-schedule install/remove
|
||||||
└── UI.pm # Navigation, flash, CSRF, HTML helpers
|
├── Runner.pm # Pattern-based safe CLI command runner
|
||||||
|
└── UI.pm # Navigation, flash, CSRF, HTML helpers, CSS delivery
|
||||||
```
|
```
|
||||||
|
|
||||||
## Production Server
|
|
||||||
|
|
||||||
## Running Tests
|
## Running Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
Reference in New Issue
Block a user