Remove obsolete eMMC provisioning scripts and documentation for reTerminal DM4, including udev rules, flash trigger scripts, and related guides.
This commit is contained in:
166
chromium-setup/emmc-provisioning/docs/EMMC-PROVISIONING-GUIDE.md
Normal file
166
chromium-setup/emmc-provisioning/docs/EMMC-PROVISIONING-GUIDE.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# Automatic eMMC provisioning for reTerminal DM4 (CM4)
|
||||
|
||||
This guide covers:
|
||||
|
||||
1. **Auto-flash**: When the reTerminal is switched to boot mode (eMMC disable jumper) and connected via USB to a provisioning host, the host automatically deploys a golden image to the CM4 eMMC.
|
||||
2. **Backup**: When a device is detected (USB or network), the dashboard asks you to choose **Backup** or **Deploy**. Backup saves the device eMMC to a timestamped file in `backups/`.
|
||||
3. **Network**: If the device boots over the network and runs the **provisioning client** (see `network-client/`), it registers with the dashboard and appears as "Device (Network)"; you then choose Backup or Deploy. Deploy streams the golden image to the device; Backup uploads the device eMMC to the server.
|
||||
4. **Cloud-init**: The golden image includes cloud-init so each device configures itself on first boot (hostname, network, packages, kiosk setup).
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Auto-flash when reTerminal is in boot mode
|
||||
|
||||
### How it works
|
||||
|
||||
- reTerminal has an **eMMC disable** jumper (see reTerminal docs; often “J2” or “nRPIBOOT”). When the jumper is fitted, the CM4 boots in **USB device mode** and waits for `rpiboot` from the host.
|
||||
- You connect the reTerminal’s **USB slave** port to a **provisioning PC** (Linux).
|
||||
- **udev** detects the Raspberry Pi Foundation USB device (vendor `2b8e`) and runs a trigger script.
|
||||
- The trigger starts a **flash job** that:
|
||||
1. Runs **rpiboot** (from the `usbboot` project). The CM4 then exposes its eMMC as a USB mass-storage device.
|
||||
2. After `rpiboot` exits, finds the new block device (eMMC) and writes your **golden image** to it with `dd`.
|
||||
- You remove the jumper and power cycle; the reTerminal boots from eMMC and runs **cloud-init** on first boot.
|
||||
|
||||
### Provisioning host setup (Linux)
|
||||
|
||||
#### 1. Build and install usbboot (rpiboot)
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y libusb-1.0-0-dev
|
||||
git clone --depth=1 https://github.com/raspberrypi/usbboot
|
||||
cd usbboot
|
||||
make
|
||||
sudo mkdir -p /opt/usbboot
|
||||
sudo cp rpiboot /opt/usbboot/
|
||||
```
|
||||
|
||||
#### 2. Create golden image and config directory
|
||||
|
||||
- Build your golden image (see Part 2) and place it where the script will find it, e.g.:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/lib/cm4-provisioning
|
||||
sudo cp /path/to/your/golden-reterminal.img /var/lib/cm4-provisioning/golden.img
|
||||
```
|
||||
|
||||
- Or use a different path and set `GOLDEN_IMAGE` when installing the script (see below).
|
||||
|
||||
#### 3. Install the auto-flash script and trigger
|
||||
|
||||
```bash
|
||||
# From this repo (chromium-setup/emmc-provisioning/host/)
|
||||
cd chromium-setup/emmc-provisioning/host
|
||||
|
||||
sudo mkdir -p /opt/cm4-provisioning
|
||||
sudo cp flash-emmc-on-connect.sh /opt/cm4-provisioning/
|
||||
sudo chmod +x /opt/cm4-provisioning/flash-emmc-on-connect.sh
|
||||
|
||||
# Optional: override paths via environment (create env file)
|
||||
echo 'GOLDEN_IMAGE=/var/lib/cm4-provisioning/golden.img' | sudo tee /opt/cm4-provisioning/env
|
||||
echo 'RPIBOOT_DIR=/opt/usbboot' | sudo tee -a /opt/cm4-provisioning/env
|
||||
echo 'EMMC_SIZE_BYTES=8589934592' | sudo tee -a /opt/cm4-provisioning/env # 8GB; use 17179869184 for 16GB
|
||||
|
||||
sudo cp cm4-flash-trigger.sh /usr/local/bin/
|
||||
sudo chmod +x /usr/local/bin/cm4-flash-trigger.sh
|
||||
```
|
||||
|
||||
If your golden image path or rpiboot path is different, set `GOLDEN_IMAGE`, `RPIBOOT_DIR`, and optionally `EMMC_SIZE_BYTES` in `/opt/cm4-provisioning/env` and source it from the script, or pass them into the systemd-run call in the trigger (e.g. by making the trigger source the env file and export variables before `systemd-run`).
|
||||
|
||||
#### 4. Install udev rule
|
||||
|
||||
```bash
|
||||
# From emmc-provisioning/host/
|
||||
sudo cp 90-cm4-boot-mode.rules /etc/udev/rules.d/
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger
|
||||
```
|
||||
|
||||
#### 5. Enable provisioning (safety)
|
||||
|
||||
Provisioning runs only if the “enabled” file exists:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /etc/cm4-provisioning
|
||||
sudo touch /etc/cm4-provisioning/enabled
|
||||
```
|
||||
|
||||
To disable auto-flash, remove that file: `sudo rm /etc/cm4-provisioning/enabled`.
|
||||
|
||||
#### 6. Optional: pass environment into the flash job
|
||||
|
||||
If you use `/opt/cm4-provisioning/env`, update the trigger so the flash script sees those variables. For example change `/usr/local/bin/cm4-flash-trigger.sh` to:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -a
|
||||
[[ -f /opt/cm4-provisioning/env ]] && source /opt/cm4-provisioning/env
|
||||
set +a
|
||||
export GOLDEN_IMAGE RPIBOOT_DIR EMMC_SIZE_BYTES
|
||||
FLASH_SCRIPT="${CM4_FLASH_SCRIPT:-/opt/cm4-provisioning/flash-emmc-on-connect.sh}"
|
||||
exec systemd-run --no-block --unit=cm4-flash-once --property=Environment="GOLDEN_IMAGE=$GOLDEN_IMAGE" ...
|
||||
```
|
||||
|
||||
Or keep it simple and edit the defaults inside `flash-emmc-on-connect.sh` (e.g. `GOLDEN_IMAGE`, `RPIBOOT_DIR`, `EMMC_SIZE_BYTES`).
|
||||
|
||||
### Usage
|
||||
|
||||
1. Fit the **eMMC disable** jumper on the reTerminal.
|
||||
2. Connect the reTerminal **USB slave** port to the provisioning PC.
|
||||
3. Power the reTerminal (or apply power after USB).
|
||||
4. On the host, `rpiboot` will run automatically; when it exits, the script will `dd` the golden image to the eMMC. Watch logs: `journalctl -u cm4-flash-once -f` or `journalctl -t cm4-flash -f`.
|
||||
5. When done, remove the jumper and power cycle the reTerminal. It will boot from eMMC; cloud-init will run on first boot.
|
||||
|
||||
---
|
||||
|
||||
## Part 2: Golden image with cloud-init
|
||||
|
||||
Raspberry Pi OS (recent versions) supports **cloud-init** using the **NoCloud** datasource: it reads `user-data`, `meta-data`, and optionally `network-config` from the **boot** (FAT32) partition.
|
||||
|
||||
### Creating the golden image
|
||||
|
||||
1. **Flash Raspberry Pi OS** (or your base image) to a spare SD card or a loop file.
|
||||
2. **Mount the boot partition** (first partition, FAT32). On the image file it might be at an offset; use `losetup -P` or mount the SD’s partition.
|
||||
3. **Add cloud-init NoCloud files** on the boot partition (same level as `config.txt`, not in a subfolder for default NoCloud):
|
||||
- `user-data` – main config (packages, runcmd, etc.)
|
||||
- `meta-data` – optional (instance-id, local-hostname)
|
||||
- `network-config` – optional (network config in netplan format)
|
||||
|
||||
You can use the examples in this repo:
|
||||
|
||||
```bash
|
||||
# After mounting boot partition at e.g. /mnt/boot
|
||||
# (On Raspberry Pi OS, boot is often /boot/firmware on the running system, or the first FAT partition of the image)
|
||||
cp emmc-provisioning/cloud-init/user-data /mnt/boot/
|
||||
cp emmc-provisioning/cloud-init/meta-data /mnt/boot/
|
||||
cp emmc-provisioning/cloud-init/network-config /mnt/boot/
|
||||
```
|
||||
|
||||
4. **Customise** `user-data` and `network-config` (hostname, WiFi, packages, Chromium kiosk, etc.).
|
||||
5. **Copy your kiosk/Chromium scripts** into the image rootfs if needed (e.g. under `/home/pi/` or `/opt/`) and reference them from `user-data` `runcmd` or a systemd unit.
|
||||
6. **Unmount**, then create a **golden image** from the SD or loop device (e.g. `dd` or `dd` of the whole block device). Use that as `golden.img` on the provisioning host.
|
||||
|
||||
### Cloud-init file locations on the Pi
|
||||
|
||||
- **NoCloud**: Boot partition root – `user-data`, `meta-data`, `network-config`.
|
||||
- Some images expect them in a subfolder `cloud-init/` or on a separate vfat partition labeled `cidata`; check your OS docs. Standard Raspberry Pi OS NoCloud uses the boot partition root.
|
||||
|
||||
### Per-device config (optional)
|
||||
|
||||
NoCloud can also use a **seed** partition or **config drive**. For per-device hostname/settings you can:
|
||||
|
||||
- Use **meta-data** `instance-id` and `local-hostname` and generate different `meta-data` per device when imaging (e.g. script that writes `meta-data` before flashing), or
|
||||
- Use a first-boot script that calls a provisioning server (e.g. by serial number) and applies device-specific config; cloud-init can launch that script from `runcmd`.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Step | Action |
|
||||
|------|--------|
|
||||
| 1 | Build `usbboot`, install `rpiboot` on provisioning host. |
|
||||
| 2 | Create golden image with cloud-init `user-data`, `meta-data`, `network-config` on boot partition. |
|
||||
| 3 | Install `flash-emmc-on-connect.sh`, `cm4-flash-trigger.sh`, and udev rule; set `GOLDEN_IMAGE` and enable file. |
|
||||
| 4 | Put reTerminal in boot mode (jumper), connect USB to host; image is written automatically. |
|
||||
| 5 | Remove jumper, power cycle; device boots from eMMC and cloud-init runs on first boot. |
|
||||
|
||||
This gives you automatic deployment of the golden image to eMMC when the reTerminal is in boot mode, plus first-boot configuration via cloud-init.
|
||||
292
chromium-setup/emmc-provisioning/docs/PORTAL_STYLING_GUIDE.md
Normal file
292
chromium-setup/emmc-provisioning/docs/PORTAL_STYLING_GUIDE.md
Normal file
@@ -0,0 +1,292 @@
|
||||
# Portal Styling Guide (Template)
|
||||
|
||||
Use this document when building new portals so they match the visual and UX style of the reference portal (e.g. FreePBX / TM VOIP Extensions Portal). Replace placeholders like `[Portal Name]` with your portal’s name where relevant.
|
||||
|
||||
---
|
||||
|
||||
## 1. Design philosophy
|
||||
|
||||
- **Dark theme:** Dark backgrounds with light text; no light-mode variant in this guide.
|
||||
- **Accent:** Single accent (teal/cyan gradient) for primary actions, links, and highlights.
|
||||
- **Clarity:** Clear hierarchy (cards, sections, labels), consistent spacing, readable typography.
|
||||
- **Consistency:** Same tokens, components, and patterns across all pages (login, app, modals).
|
||||
|
||||
---
|
||||
|
||||
## 2. Design tokens (CSS variables)
|
||||
|
||||
Define these in `:root` (or in a shared CSS file) and use them everywhere instead of hard-coded colors.
|
||||
|
||||
### Colors
|
||||
|
||||
| Token | Value | Usage |
|
||||
|-------|--------|--------|
|
||||
| `--bg-primary` | `#0a0e14` | Page background |
|
||||
| `--bg-secondary` | `#11151c` | Header, secondary surfaces |
|
||||
| `--bg-tertiary` | `#1a1f2b` | Inputs, table header, hover states |
|
||||
| `--bg-card` | `#151a24` | Cards, modals |
|
||||
| `--accent-primary` | `#00d4aa` | Primary accent (teal) |
|
||||
| `--accent-secondary` | `#00b894` | Accent variant, secondary accent |
|
||||
| `--accent-glow` | `rgba(0, 212, 170, 0.15)` | Focus rings, subtle highlights |
|
||||
| `--text-primary` | `#e6e8eb` | Main text |
|
||||
| `--text-secondary` | `#8b949e` | Labels, secondary text |
|
||||
| `--text-muted` | `#5c6370` | Placeholders, disabled, hints |
|
||||
| `--border-color` | `#2d333b` | Borders (cards, inputs, tables) |
|
||||
| `--danger` | `#ff6b6b` | Errors, delete, destructive actions |
|
||||
| `--danger-glow` | `rgba(255, 107, 107, 0.15)` | Danger focus/hover background |
|
||||
| `--warning` | `#ffd93d` | Warnings |
|
||||
| `--success` | `#00d4aa` | Success (can match accent) |
|
||||
| `--gradient-accent` | `linear-gradient(135deg, #00d4aa 0%, #00b894 50%, #00cec9 100%)` | Primary buttons, logo text |
|
||||
|
||||
### Example `:root` block
|
||||
|
||||
```css
|
||||
:root {
|
||||
--bg-primary: #0a0e14;
|
||||
--bg-secondary: #11151c;
|
||||
--bg-tertiary: #1a1f2b;
|
||||
--bg-card: #151a24;
|
||||
--accent-primary: #00d4aa;
|
||||
--accent-secondary: #00b894;
|
||||
--accent-glow: rgba(0, 212, 170, 0.15);
|
||||
--text-primary: #e6e8eb;
|
||||
--text-secondary: #8b949e;
|
||||
--text-muted: #5c6370;
|
||||
--border-color: #2d333b;
|
||||
--danger: #ff6b6b;
|
||||
--danger-glow: rgba(255, 107, 107, 0.15);
|
||||
--warning: #ffd93d;
|
||||
--success: #00d4aa;
|
||||
--gradient-accent: linear-gradient(135deg, #00d4aa 0%, #00b894 50%, #00cec9 100%);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Typography
|
||||
|
||||
- **Body / UI font:** `'Outfit', -apple-system, BlinkMacSystemFont, sans-serif`
|
||||
- **Monospace (data, code, IDs):** `'JetBrains Mono', monospace`
|
||||
|
||||
Load from Google Fonts:
|
||||
|
||||
```html
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
```
|
||||
|
||||
- **Body:** `color: var(--text-primary);` `line-height: 1.6;`
|
||||
- **Labels:** `font-size: 0.85rem;` `font-weight: 500;` `color: var(--text-secondary);` optional `text-transform: uppercase;` `letter-spacing: 0.5px;`
|
||||
- **Card/section titles:** `font-size: 1.1rem;` `font-weight: 600;`
|
||||
- **Table header:** `font-size: 0.75rem–0.8rem;` `font-weight: 600;` `text-transform: uppercase;` `letter-spacing: 0.5px;` `color: var(--text-secondary);`
|
||||
- **Table body:** `font-size: 0.9rem;` monospace for IDs/codes
|
||||
|
||||
---
|
||||
|
||||
## 4. Page layout
|
||||
|
||||
### Global
|
||||
|
||||
- **Reset:** `* { margin: 0; padding: 0; box-sizing: border-box; }`
|
||||
- **Body:** `background: var(--bg-primary);` `color: var(--text-primary);` `min-height: 100vh;` `font-family: 'Outfit', ...`
|
||||
|
||||
### Background treatment (optional)
|
||||
|
||||
Subtle gradient overlay for depth:
|
||||
|
||||
```css
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(circle at 20% 20%, rgba(0, 212, 170, 0.03) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 80%, rgba(0, 184, 148, 0.03) 0%, transparent 50%),
|
||||
linear-gradient(180deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
```
|
||||
|
||||
### Header (fixed)
|
||||
|
||||
- **Container:** `background: var(--bg-secondary);` `border-bottom: 1px solid var(--border-color);` `position: fixed; top: 0; left: 0; right: 0; z-index: 1000;` optional `backdrop-filter: blur(10px);`
|
||||
- **Top row:** Logo left; status/user/actions right; `padding: 1rem 2rem;` `display: flex; align-items: center; justify-content: space-between;`
|
||||
- **Tabs row:** Under the top row; `background: var(--bg-tertiary);` `padding: 0.5rem 2rem;` `border-top: 1px solid var(--border-color);` horizontal flex, gap, overflow-x auto for small screens
|
||||
|
||||
### Main content
|
||||
|
||||
- **Container:** `max-width: 1400px;` `margin: 0 auto;` `padding: 2rem;` `padding-top: calc(2rem + 140px);` (offset for fixed header + tabs). On mobile reduce padding and increase top offset if header stacks.
|
||||
|
||||
---
|
||||
|
||||
## 5. Logo
|
||||
|
||||
- **Wrapper:** flex, `align-items: center;` `gap: 0.75rem;`
|
||||
- **Icon:** Square (e.g. 40×40px), `background: var(--gradient-accent);` `border-radius: 10px;` optional `box-shadow: 0 4px 20px var(--accent-glow);` emoji or icon inside.
|
||||
- **Title (h1):** `font-size: 1.5rem;` `font-weight: 600;` `background: var(--gradient-accent);` `-webkit-background-clip: text;` `background-clip: text;` `-webkit-text-fill-color: transparent;`
|
||||
|
||||
---
|
||||
|
||||
## 6. Tabs (main navigation)
|
||||
|
||||
- **Tab button (default):** `padding: 0.75rem 1.5rem;` `background: transparent;` `border: none;` `color: var(--text-secondary);` `font-size: 0.95rem;` `font-weight: 500;` `border-radius: 8px;` flex with icon + label, `gap: 0.5rem;`
|
||||
- **Hover:** `color: var(--text-primary);` `background: var(--bg-tertiary);`
|
||||
- **Active:** `background: var(--gradient-accent);` `color: var(--bg-primary);`
|
||||
- **Tab content:** `display: none;` by default; `.tab-content.active { display: block; }` optional fade-in animation.
|
||||
|
||||
---
|
||||
|
||||
## 7. Cards
|
||||
|
||||
- **Base:** `background: var(--bg-card);` `border: 1px solid var(--border-color);` `border-radius: 16px;` `padding: 1.5rem;` `margin-bottom: 1.5rem;`
|
||||
- **Card header:** flex, `justify-content: space-between;` `align-items: center;` `margin-bottom: 1.5rem;` `padding-bottom: 1rem;` `border-bottom: 1px solid var(--border-color);`
|
||||
- **Card title:** `font-size: 1.1rem;` `font-weight: 600;` flex with icon + text, `gap: 0.5rem;`
|
||||
|
||||
---
|
||||
|
||||
## 8. Buttons
|
||||
|
||||
- **Base:** `padding: 0.75rem 1.5rem;` `border-radius: 8px;` `font-size: 0.9rem;` `font-weight: 500;` inline-flex, `align-items: center;` `gap: 0.5rem;` `transition: all 0.2s ease;`
|
||||
- **Primary:** `background: var(--gradient-accent);` `color: var(--bg-primary);` `border: none;` hover: slight `translateY(-2px);` `box-shadow: 0 4px 20px var(--accent-glow);`
|
||||
- **Secondary:** `background: var(--bg-tertiary);` `border: 1px solid var(--border-color);` `color: var(--text-primary);` hover: `border-color: var(--accent-primary);`
|
||||
- **Danger:** `background: transparent;` `border: 1px solid var(--danger);` `color: var(--danger);` hover: `background: var(--danger);` `color: white;`
|
||||
- **Disabled:** `opacity: 0.5;` `cursor: not-allowed;`
|
||||
- **Icon-only (small):** e.g. 28×28px, `border-radius: 6px;` same semantic colors (e.g. `.btn-remove` with `--danger`).
|
||||
|
||||
---
|
||||
|
||||
## 9. Forms
|
||||
|
||||
- **Grid:** `display: grid;` `grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));` `gap: 1rem;`
|
||||
- **Form group:** flex column, `gap: 0.5rem;`
|
||||
- **Label:** `font-size: 0.85rem;` `font-weight: 500;` `color: var(--text-secondary);` optional uppercase + letter-spacing
|
||||
- **Input / select:** `padding: 0.75rem 1rem;` `background: var(--bg-tertiary);` `border: 1px solid var(--border-color);` `border-radius: 8px;` `color: var(--text-primary);` `font-size: 0.9rem;` monospace for IDs/codes
|
||||
- **Focus:** `outline: none;` `border-color: var(--accent-primary);` `box-shadow: 0 0 0 3px var(--accent-glow);`
|
||||
- **Placeholder:** `color: var(--text-muted);`
|
||||
- **Read-only:** `background: var(--bg-secondary);` `cursor: default;`
|
||||
- **Checkbox:** `accent-color: var(--accent-primary);` (or custom size e.g. 18×18px)
|
||||
- **Password field:** wrapper with toggle button; input `padding-right: 3rem;` so toggle doesn’t overlap text.
|
||||
|
||||
---
|
||||
|
||||
## 10. Tables
|
||||
|
||||
- **Container:** `overflow-x: auto;` `border-radius: 12px;` `border: 1px solid var(--border-color);`
|
||||
- **Table:** `width: 100%;` `border-collapse: collapse;`
|
||||
- **th / td:** `padding: 0.6rem 0.75rem;` `text-align: left;` `border-bottom: 1px solid var(--border-color);` `color: var(--text-primary);`
|
||||
- **th:** `background: var(--bg-tertiary);` `font-size: 0.75rem;` `font-weight: 600;` `text-transform: uppercase;` `letter-spacing: 0.5px;`
|
||||
- **tbody tr hover:** `background-color: var(--bg-tertiary);`
|
||||
- **Last row:** `tr:last-child td { border-bottom: none; }`
|
||||
- **Data cells:** `font-family: 'JetBrains Mono', monospace;` `font-size: 0.9rem;`
|
||||
- **Actions column:** right-aligned; min-width for action buttons; `.table-actions { display: flex; gap: 1rem; flex-wrap: wrap; }`
|
||||
- **Data table variant:** `.data-table` with alternating row background (e.g. `nth-child(even)` subtle `rgba(255,255,255,0.02)`) and same hover.
|
||||
|
||||
---
|
||||
|
||||
## 11. Badges and status
|
||||
|
||||
- **Status badge (e.g. connection):** flex, `align-items: center;` `gap: 0.5rem;` `padding: 0.5rem 1rem;` `background: var(--bg-tertiary);` `border-radius: 20px;` `font-size: 0.85rem;` `border: 1px solid var(--border-color);`
|
||||
- **Status dot:** 8×8px circle; `.connected { background: var(--success); }` `.error { background: var(--danger); }` optional pulse animation
|
||||
- **Pill badge (e.g. extension ID):** `padding: 0.25rem 0.75rem;` `background: var(--accent-glow);` `color: var(--accent-primary);` `border-radius: 20px;` `font-weight: 500;`
|
||||
- **Tech badge (e.g. PJSIP/SIP):** small, `border-radius: 4px;` `font-size: 0.75rem;` `font-weight: 600;` `text-transform: uppercase;` distinct colors per type (e.g. PJSIP blue, SIP purple)
|
||||
|
||||
---
|
||||
|
||||
## 12. Search and filters
|
||||
|
||||
- **Search box:** wrapper relative; input `padding: 0.6rem 1rem 0.6rem 2.5rem;` `width: 250px;` same colors as form inputs; optional `::before` search icon (e.g. 🔍) `left: 0.75rem;`
|
||||
- **Filter checkbox:** inline-flex, `align-items: center;` `gap: 0.4rem;` `color: var(--text-secondary);` `accent-color: var(--accent-primary);`
|
||||
|
||||
---
|
||||
|
||||
## 13. Empty and loading states
|
||||
|
||||
- **Empty state:** `text-align: center;` `padding: 3rem;` `color: var(--text-muted);`
|
||||
- **No results:** same idea, `padding: 2rem;`
|
||||
- **Loading:** flex center, `padding: 2rem;` spinner (e.g. 32×32px border, `border-top-color: var(--accent-primary);` `animation: spin 1s linear infinite;`)
|
||||
|
||||
---
|
||||
|
||||
## 14. Modals
|
||||
|
||||
- **Overlay:** `position: fixed;` `inset: 0;` `background: rgba(0,0,0,0.7);` `z-index: 1000;` flex center; `display: none;` `.active { display: flex; }`
|
||||
- **Dialog:** `background: var(--bg-card);` `border: 1px solid var(--border-color);` `border-radius: 16px;` `padding: 2rem;` `max-width: 500px;` `width: 90%;` optional scale-in animation
|
||||
- **Modal header:** flex, `align-items: center;` `gap: 1rem;` `margin-bottom: 1.5rem;`
|
||||
- **Modal icon:** e.g. 48×48px, `background: var(--accent-glow);` `border-radius: 12px;` centered content
|
||||
- **Modal title:** `font-size: 1.25rem;` `font-weight: 600;`
|
||||
- **Detail sections:** `margin-bottom: 2rem;` section title `font-size: 1.1rem;` `color: var(--accent-primary);` `border-bottom: 1px solid var(--border-color);` `padding-bottom: 0.5rem;`
|
||||
- **Details grid:** `grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));` `gap: 1rem;` each item: label (small, secondary) + value (primary)
|
||||
|
||||
---
|
||||
|
||||
## 15. Toasts (notifications)
|
||||
|
||||
- **Container:** `position: fixed;` `top: 1rem;` `right: 1rem;` `z-index: 1100;` flex column, `gap: 0.5rem;`
|
||||
- **Toast:** `padding: 1rem 1.5rem;` `background: var(--bg-card);` `border: 1px solid var(--border-color);` `border-radius: 8px;` flex, `align-items: center;` `gap: 0.75rem;` `max-width: 400px;` slide-in animation
|
||||
- **Success:** `border-left: 3px solid var(--success);`
|
||||
- **Error:** `border-left: 3px solid var(--danger);`
|
||||
|
||||
---
|
||||
|
||||
## 16. Pagination
|
||||
|
||||
- **Wrapper:** flex, `justify-content: center;` `align-items: center;` `gap: 0.5rem;` `padding: 1rem;` `border-top: 1px solid var(--border-color);`
|
||||
- **Button:** `padding: 0.5rem 0.75rem;` `background: var(--bg-tertiary);` `border: 1px solid var(--border-color);` `border-radius: 6px;` `color: var(--text-primary);` `font-size: 0.85rem;` `min-width: 36px;`
|
||||
- **Hover:** `background: var(--accent-glow);` `border-color: var(--accent-primary);` `color: var(--accent-primary);`
|
||||
- **Active page:** `background: var(--accent-primary);` `color: var(--bg-primary);`
|
||||
- **Disabled:** `opacity: 0.4;` `cursor: not-allowed;`
|
||||
- **Info text:** `color: var(--text-secondary);` `font-size: 0.85rem;` between prev/next
|
||||
|
||||
---
|
||||
|
||||
## 17. Login page
|
||||
|
||||
- **Layout:** full viewport, flex center; `padding: 2rem;`
|
||||
- **Card:** same tokens as app cards; `max-width: 400px;` `padding: 2.5rem;` `border-radius: 16px;` `box-shadow: 0 8px 32px rgba(0,0,0,0.3);`
|
||||
- **Logo:** centered; icon (e.g. 64×64px) with gradient + glow; title with gradient text
|
||||
- **Form:** same form-group and input styles as app; full-width primary submit button
|
||||
- **Error message:** `color: var(--danger);` `font-size: 0.9rem;` above or below form
|
||||
- Use the same `:root` variables and body background so login and app feel like one product.
|
||||
|
||||
---
|
||||
|
||||
## 18. Responsive
|
||||
|
||||
- **Breakpoint:** e.g. `@media (max-width: 768px)`
|
||||
- **Header top:** `flex-direction: column;` `gap: 1rem;` `text-align: center;` reduce padding
|
||||
- **Tabs:** reduce horizontal padding; allow horizontal scroll if needed
|
||||
- **Container:** reduce padding; increase `padding-top` if header height grows (e.g. `calc(1rem + 180px)`)
|
||||
- **Form grid:** `grid-template-columns: 1fr;` for single column on small screens
|
||||
|
||||
---
|
||||
|
||||
## 19. Checklist for a new portal
|
||||
|
||||
- [ ] Copy or recreate the `:root` design tokens.
|
||||
- [ ] Load **Outfit** and **JetBrains Mono** (or same weights).
|
||||
- [ ] Use the same header structure: logo + status/user + actions, then tabs.
|
||||
- [ ] Use `.card`, `.card-header`, `.card-title` for sections.
|
||||
- [ ] Use `.btn`, `.btn-primary`, `.btn-secondary`, `.btn-danger` for actions.
|
||||
- [ ] Use `.form-grid`, `.form-group`, and input/select styles for forms.
|
||||
- [ ] Use `.table-container`, table, `.data-table` and th/td styles for lists.
|
||||
- [ ] Use same modal overlay/dialog and toast styles.
|
||||
- [ ] Use same empty, loading, and error states.
|
||||
- [ ] Apply same login page layout and token usage.
|
||||
- [ ] Test at 768px width for basic responsive behavior.
|
||||
- [ ] Replace [Portal Name] and any product-specific labels in this guide for your portal.
|
||||
|
||||
---
|
||||
|
||||
## 20. Reference files (this repo)
|
||||
|
||||
| File | Purpose |
|
||||
|------|--------|
|
||||
| `static/css/main.css` | Full implementation of tokens, layout, components |
|
||||
| `app/templates/base.html` | App shell: fonts, header, tabs, container, toasts |
|
||||
| `app/templates/login.html` | Login layout and inline tokens (can be moved to main.css) |
|
||||
| `app/templates/tabs/_dashboard.html` | Example: cards, stats, table container |
|
||||
| `app/templates/tabs/_users.html` | Example: card, form-grid, form-group, buttons, table |
|
||||
|
||||
For a new portal, you can copy `main.css` and adapt it (e.g. change `:root` if you need a different accent), then build your base template and pages to use the same class names and structure described above.
|
||||
166
chromium-setup/emmc-provisioning/docs/PROXMOX-LXC-DEPLOYMENT.md
Normal file
166
chromium-setup/emmc-provisioning/docs/PROXMOX-LXC-DEPLOYMENT.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# CM4 eMMC provisioning on Proxmox (LXC + host)
|
||||
|
||||
The auto-flash **runs on the Proxmox host** (where the USB device appears). The **LXC** holds the same scripts and shares the **golden image** directory with the host so you can manage the image from the container.
|
||||
|
||||
## What is deployed
|
||||
|
||||
| Where | What |
|
||||
|-------|-----|
|
||||
| **Proxmox host** | udev rule, trigger script, flash script, rpiboot (after you run the install script), `/var/lib/cm4-provisioning/` (golden image dir), `/etc/cm4-provisioning/enabled` |
|
||||
| **LXC 201 (cm4-provisioning)** | Same scripts in `/opt/cm4-provisioning/`, same env; `/var/lib/cm4-provisioning/` is a **bind mount** from the host (shared storage for the golden image) |
|
||||
|
||||
When you plug the reTerminal in boot mode into the **host**, udev on the host runs the flash (rpiboot + dd). The golden image is read from `/var/lib/cm4-provisioning/golden.img` on the host (same path visible in the LXC).
|
||||
|
||||
---
|
||||
|
||||
## Deployment that was done
|
||||
|
||||
1. **LXC 201** created on Proxmox `10.130.60.224`:
|
||||
- Hostname: `cm4-provisioning`
|
||||
- Debian 12, 1 GB RAM, 8 GB rootfs
|
||||
- Bind mount: host `/var/lib/cm4-provisioning` → container `/var/lib/cm4-provisioning`
|
||||
|
||||
2. **On the host**:
|
||||
- `/opt/cm4-provisioning/flash-emmc-on-connect.sh` – flash script
|
||||
- `/usr/local/bin/cm4-flash-trigger.sh` – started by udev
|
||||
- `/etc/udev/rules.d/90-cm4-boot-mode.rules` – run trigger when USB vendor `2b8e` is added
|
||||
- `/opt/cm4-provisioning/env` – `GOLDEN_IMAGE`, `RPIBOOT_DIR`, `EMMC_SIZE_BYTES`
|
||||
- `/etc/cm4-provisioning/enabled` – safety switch (remove to disable auto-flash)
|
||||
|
||||
3. **Inside LXC 201**:
|
||||
- Same scripts in `/opt/cm4-provisioning/` and env (for reference/backup)
|
||||
- Golden image path: `/var/lib/cm4-provisioning/golden.img` (bind-mounted from host)
|
||||
- **Dashboard** (optional): Flask app in `/opt/cm4-provisioning/dashboard/` to monitor deployment and show connection steps; see below.
|
||||
|
||||
4. **usbboot (rpiboot)** was **not** built on the host (no outbound DNS during deploy). You must install it when the host has internet.
|
||||
|
||||
---
|
||||
|
||||
## What you need to do
|
||||
|
||||
### 1. Build and install rpiboot on the Proxmox host (when it has internet)
|
||||
|
||||
On your machine (repo already synced to the host):
|
||||
|
||||
```bash
|
||||
# From your repo
|
||||
scp chromium-setup/emmc-provisioning/scripts/install-usbboot-on-host.sh root@10.130.60.224:/tmp/
|
||||
ssh root@10.130.60.224 "bash /tmp/install-usbboot-on-host.sh"
|
||||
```
|
||||
|
||||
Or on the host (if the deploy folder is still there):
|
||||
|
||||
```bash
|
||||
ssh root@10.130.60.224
|
||||
bash /tmp/emmc-provisioning-deploy/scripts/install-usbboot-on-host.sh
|
||||
```
|
||||
|
||||
This installs dependencies, clones usbboot, builds it, and copies `rpiboot` to `/opt/usbboot/`.
|
||||
|
||||
### 2. Enable root SSH and add your SSH key to LXC 201
|
||||
|
||||
No root password is set by default. To log in as root over SSH:
|
||||
|
||||
- **Option A – Use the setup script (recommended):** From your machine (with SSH key and optional password):
|
||||
|
||||
```bash
|
||||
# Add your default SSH key (~/.ssh/id_ed25519.pub or id_rsa.pub) and enable root SSH
|
||||
./chromium-setup/emmc-provisioning/scripts/setup-lxc-ssh.sh root@10.130.60.224
|
||||
|
||||
# Or specify key file and set root password
|
||||
ROOT_PASSWORD='YourPassword' ./chromium-setup/emmc-provisioning/scripts/setup-lxc-ssh.sh root@10.130.60.224 ~/.ssh/id_ed25519.pub
|
||||
```
|
||||
|
||||
Then connect with `ssh root@<LXC-IP>` (script prints the IP). Get the IP anytime with:
|
||||
`ssh root@10.130.60.224 "pct exec 201 -- hostname -I"`
|
||||
|
||||
- **Option B – Manual:**
|
||||
`ssh root@10.130.60.224` then `pct exec 201 -- bash` to get a shell in the container. Run `apt-get install -y openssh-server`, edit `/etc/ssh/sshd_config` to set `PermitRootLogin yes`, run `passwd` to set root password, add your key to `/root/.ssh/authorized_keys`, and restart `ssh`.
|
||||
|
||||
### 3. Put the golden image on the host (or in the LXC)
|
||||
|
||||
The image must be at **`/var/lib/cm4-provisioning/golden.img`** on the **host**. Because that directory is bind-mounted into the LXC, you can use either:
|
||||
|
||||
- **From the host:**
|
||||
```bash
|
||||
scp your-golden.img root@10.130.60.224:/var/lib/cm4-provisioning/golden.img
|
||||
```
|
||||
|
||||
- **From the LXC** (e.g. after copying the image into the container elsewhere first):
|
||||
```bash
|
||||
pct exec 201 -- ls -la /var/lib/cm4-provisioning/
|
||||
# Copy to that path inside the container; it's the same as the host path.
|
||||
```
|
||||
|
||||
### 4. Run the provisioning dashboard (optional)
|
||||
|
||||
The dashboard shows **connection steps** and **live deployment status** (idle / connecting / flashing / done / error) and a recent flash log. It reads the same `status.json` and `flash.log` that the host’s flash script writes (via the bind-mounted `/var/lib/cm4-provisioning`).
|
||||
|
||||
**Inside LXC 201:**
|
||||
|
||||
```bash
|
||||
# Copy dashboard into the container (from host, if you have the repo there)
|
||||
# Or from your workstation:
|
||||
# rsync -a chromium-setup/emmc-provisioning/dashboard/ root@10.130.60.224:/tmp/dashboard/
|
||||
# ssh root@10.130.60.224 "pct push 201 /tmp/dashboard/app.py /opt/cm4-provisioning/dashboard/ && pct push 201 /tmp/dashboard/cm4-dashboard.service /opt/cm4-provisioning/dashboard/ && pct exec 201 -- mkdir -p /opt/cm4-provisioning/dashboard/templates && ..."
|
||||
|
||||
# Inside the LXC (pct exec 201 -- bash):
|
||||
apt-get update && apt-get install -y python3-flask
|
||||
mkdir -p /opt/cm4-provisioning/dashboard/templates
|
||||
# Copy app.py, templates/index.html, cm4-dashboard.service into the container (see dashboard/README.md)
|
||||
|
||||
cp /opt/cm4-provisioning/dashboard/cm4-dashboard.service /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now cm4-dashboard
|
||||
```
|
||||
|
||||
Then open **http://<LXC-201-IP>:5000** (get the IP with `pct exec 201 -- hostname -I`). If the LXC is on a private network, set up port forwarding on the Proxmox host or use a reverse proxy so you can reach the dashboard from your browser.
|
||||
|
||||
### 5. Optional: disable or enable auto-flash
|
||||
|
||||
- **Disable:**
|
||||
`ssh root@10.130.60.224 "rm /etc/cm4-provisioning/enabled"`
|
||||
|
||||
- **Enable again:**
|
||||
`ssh root@10.130.60.224 "touch /etc/cm4-provisioning/enabled"`
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
1. Place the reTerminal in **boot mode** (eMMC disable jumper).
|
||||
2. Connect its **USB slave** port to the **Proxmox host** (not to the LXC).
|
||||
3. Power the reTerminal (or connect after power).
|
||||
4. On the host, udev will run the trigger and then the flash script (rpiboot, then dd). Watch logs:
|
||||
```bash
|
||||
ssh root@10.130.60.224 "journalctl -u cm4-flash-once -f"
|
||||
# or
|
||||
ssh root@10.130.60.224 "journalctl -t cm4-flash -f"
|
||||
```
|
||||
5. When flashing finishes, remove the jumper and power cycle the reTerminal so it boots from eMMC.
|
||||
|
||||
---
|
||||
|
||||
## Redeploy / update scripts
|
||||
|
||||
From your repo (e.g. after changing scripts):
|
||||
|
||||
```bash
|
||||
./chromium-setup/emmc-provisioning/scripts/deploy-to-proxmox.sh root@10.130.60.224
|
||||
```
|
||||
|
||||
That script syncs the repo to the host and reinstalls scripts on both the host and LXC 201. It does **not** overwrite `/opt/cm4-provisioning/env` or `/etc/cm4-provisioning/enabled` if you’ve changed them; adjust the script if you want that. It also does **not** build usbboot; run `install-usbboot-on-host.sh` on the host when needed.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Item | Location |
|
||||
|------|----------|
|
||||
| LXC | 201, hostname `cm4-provisioning`, Proxmox `10.130.60.224` |
|
||||
| Golden image | `/var/lib/cm4-provisioning/golden.img` (host and LXC see the same file) |
|
||||
| Flash runs on | Proxmox **host** (udev + rpiboot + dd) |
|
||||
| Build rpiboot on host | Run `scripts/install-usbboot-on-host.sh` on the host when it has internet |
|
||||
| Dashboard | Flask app in LXC at `http://<LXC-IP>:5000`; switch Flash/Backup mode, list and download backups; see **dashboard/README.md** and section 3 above |
|
||||
| Backups | Saved under `/var/lib/cm4-provisioning/backups/`. When a device is detected (USB or network), choose **Backup** or **Deploy** in the dashboard. |
|
||||
| Network deploy/backup | Network-booted devices run **network-client/provisioning-client.sh** and register with the dashboard; they then appear under "Device detected (Network)" and you choose Backup or Deploy. See **network-client/README.md**. |
|
||||
Reference in New Issue
Block a user