Remove obsolete eMMC provisioning scripts and documentation for reTerminal DM4, including udev rules, flash trigger scripts, and related guides.

This commit is contained in:
nearxos
2026-02-18 10:27:23 +02:00
parent d6b09cdd6f
commit 21fc0e8fd2
15 changed files with 337 additions and 38 deletions

View 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 reTerminals **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 SDs 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.

View 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 portals 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.75rem0.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 doesnt 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.

View 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 hosts 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://&lt;LXC-201-IP&gt;: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 youve 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**. |