refactor: Phase 1 code cleanup - constants, logging, and repository organization

This commit is contained in:
2026-03-28 18:54:39 -07:00
parent f1b0ac43d0
commit 6c3848102b
24 changed files with 17078 additions and 50 deletions

2
status/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
dist/

402
status/DEPLOYMENT_GUIDE.md Normal file
View File

@@ -0,0 +1,402 @@
# SAMI-CLOUD Status Dashboard - Deployment Guide
Complete guide for deploying and using the SAMI-CLOUD Status Dashboard with app deployment capabilities.
## Overview
The SAMI-CLOUD Status Dashboard is a web application that:
- Monitors service status in real-time
- Provides weather information
- Allows deploying new apps via a user-friendly interface
- Automatically creates DNS records and Caddy reverse proxy configurations
- Works cross-platform (Windows, Linux, macOS)
## Architecture
```
┌─────────────────────────────────────────────────────────┐
│ User Browser │
│ (https://status.sami) │
└────────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Caddy Server │
│ - Serves static files (index.html, assets) │
│ - Proxies /api/* to Node.js API server │
│ - Provides TLS with internal CA │
└────────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Node.js API Server (port 3001) │
│ - Handles app deployment requests │
│ - Creates DNS records via Technitium API │
│ - Configures Caddy routes via Admin API │
└───────┬─────────────────────────────┬───────────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Caddy Admin API │ │ Technitium DNS │
│ (port 2019) │ │ API (port 5380) │
└──────────────────┘ └──────────────────┘
```
## Prerequisites
1. **Caddy Server** with Admin API enabled
2. **Node.js** v14 or higher
3. **Technitium DNS Server** (optional, for DNS management)
4. **npm** (comes with Node.js)
## Step 1: Configure Caddy
### 1.1 Enable Caddy Admin API
Add to your Caddyfile (global options):
```caddyfile
{
admin localhost:2019 {
origins localhost localhost:2019
}
}
```
### 1.2 Configure Status Dashboard Site
Add this block to your Caddyfile:
```caddyfile
status.sami {
tls internal
# API proxy to Node.js server
handle /api/* {
reverse_proxy localhost:3001
}
# Probe endpoints for service status checks
handle_path /probe/* {
header Content-Type application/json
respond `{"ok":true}` 200
}
# Static site
root * C:/caddy/sites/status
file_server
}
```
### 1.3 Reload Caddy
```bash
caddy reload --config /path/to/Caddyfile
```
## Step 2: Deploy Dashboard Files
### 2.1 Copy Files to Production
Copy the status dashboard files to your Caddy web root:
```bash
# On Windows
xcopy /E /I e:\CaddyCerts\sites\status C:\caddy\sites\status
# On Linux/macOS
cp -r /path/to/development/sites/status /var/www/status
```
### 2.2 Verify File Structure
```
C:/caddy/sites/status/
├── index.html
├── apps.json
├── sw.js
├── assets/
│ ├── fonts/
│ ├── weather/
│ ├── *.png (app logos)
│ └── ...
└── api/
├── caddy-api.js
├── package.json
├── README.md
└── node_modules/
```
## Step 3: Set Up API Server
### 3.1 Install Dependencies
```bash
cd C:/caddy/sites/status/api # or your path
npm install
```
### 3.2 Configure Environment Variables
#### Windows (PowerShell - Persistent)
```powershell
# Set system environment variables
[System.Environment]::SetEnvironmentVariable('CADDY_ADMIN_API', 'http://localhost:2019', 'User')
[System.Environment]::SetEnvironmentVariable('DNS_SERVER_API', 'http://192.168.254.204:5380', 'User')
[System.Environment]::SetEnvironmentVariable('TECHNITIUM_API_TOKEN', 'your_token_here', 'User')
```
#### Linux/macOS (Persistent)
Add to `~/.bashrc` or `~/.zshrc`:
```bash
export CADDY_ADMIN_API=http://localhost:2019
export DNS_SERVER_API=http://192.168.254.204:5380
export TECHNITIUM_API_TOKEN=your_token_here
```
Then reload:
```bash
source ~/.bashrc # or source ~/.zshrc
```
### 3.3 Get Technitium API Token
1. Open Technitium DNS web interface (e.g., `http://dns1.sami:5380`)
2. Log in with admin credentials
3. Go to **Settings → API**
4. Generate a new token or copy existing one
5. Set as `TECHNITIUM_API_TOKEN` environment variable
### 3.4 Test the API Server
```bash
node test-api.js
```
Expected output:
```
Testing SAMI-CLOUD API...
1. Testing health endpoint...
✓ Health check passed
2. Testing API test endpoint...
✓ API test passed
Platform: win32
Caddy Admin API: http://localhost:2019
DNS Server API: http://192.168.254.204:5380
DNS Token: Configured
3. Testing services endpoint...
✓ Services endpoint passed
Found 8 services
Tests complete!
```
## Step 4: Run the API Server
### Option A: Run Directly (for testing)
```bash
node caddy-api.js
```
### Option B: Use PM2 (recommended for production)
```bash
# Install PM2 globally
npm install -g pm2
# Start the API server
pm2 start caddy-api.js --name sami-api
# Save the process list
pm2 save
# Set up PM2 to start on boot
pm2 startup
```
Manage with PM2:
```bash
pm2 status # Check status
pm2 logs sami-api # View logs
pm2 restart sami-api
pm2 stop sami-api
```
### Option C: Windows Service (using PM2)
```bash
npm install -g pm2-windows-service
pm2-service-install -n SAMI-API
```
### Option D: systemd (Linux)
Create `/etc/systemd/system/sami-api.service`:
```ini
[Unit]
Description=SAMI-CLOUD API Server
After=network.target
[Service]
Type=simple
User=caddy
WorkingDirectory=/var/www/status/api
Environment="NODE_ENV=production"
Environment="CADDY_ADMIN_API=http://localhost:2019"
Environment="DNS_SERVER_API=http://192.168.254.204:5380"
Environment="TECHNITIUM_API_TOKEN=your_token"
ExecStart=/usr/bin/node caddy-api.js
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
```
Enable and start:
```bash
sudo systemctl daemon-reload
sudo systemctl enable sami-api
sudo systemctl start sami-api
sudo systemctl status sami-api
```
## Step 5: Verify Everything Works
### 5.1 Access the Dashboard
Open your browser and navigate to:
```
https://status.sami
```
You should see:
- Service status cards
- Weather widget
- Theme toggle
- "📱 App Selector" button
### 5.2 Test App Deployment
1. Click **"📱 App Selector"** button
2. Choose an app template (or use the generic one)
3. Fill in the deployment form:
- **Subdomain**: e.g., `test-app`
- **IP Address**: e.g., `192.168.1.100`
- **Port**: e.g., `8080`
- **DNS Type**: Choose "Private DNS" or "Public DNS"
- **SSL Type**: Choose "Internal CA" or "Public (Let's Encrypt)"
4. Click **"Deploy App"**
If successful, you should see:
- Success message
- New card appears in the grid
- App is accessible at `https://test-app.sami`
## Troubleshooting
### Dashboard not accessible
- Check Caddy is running: `caddy version`
- Check Caddyfile syntax: `caddy validate --config /path/to/Caddyfile`
- View Caddy logs for errors
### API requests failing
- Verify API server is running: `pm2 status` or check process
- Check API logs: `pm2 logs sami-api`
- Test API directly: `curl http://localhost:3001/health`
### DNS records not created
- Verify `TECHNITIUM_API_TOKEN` is set correctly
- Test DNS API manually:
```bash
curl "http://192.168.254.204:5380/api/zones/records/get?token=YOUR_TOKEN&domain=sami"
```
- Check Technitium DNS server is running
### Caddy routes not working
- Verify Caddy Admin API is accessible:
```bash
curl http://localhost:2019/config/
```
- Check Caddy admin API origins in Caddyfile
- View Caddy configuration: `curl http://localhost:2019/config/ | jq .`
### App deployment fails
1. Check API server logs
2. Verify all environment variables are set
3. Test each component individually:
- DNS API connection
- Caddy Admin API connection
4. Check for existing configurations with same subdomain
## Advanced Configuration
### Custom App Templates
Edit the dashboard's JavaScript to add custom app templates with pre-configured settings.
### Multiple DNS Servers
Modify the API to support multiple DNS servers for redundancy.
### Docker Integration
Extend the API to deploy Docker containers automatically before configuring DNS and Caddy.
### Authentication
Add authentication middleware to the API server to protect deployment endpoints.
## Maintenance
### Backup Important Files
Regularly backup:
- `C:/caddy/Caddyfile` (or equivalent)
- `C:/caddy/sites/status/apps.json`
- Custom app data in localStorage (export from browser)
### Update the Dashboard
1. Pull latest changes from development
2. Copy updated files to production
3. Restart API server if needed
4. Clear browser cache or bump cache version in `sw.js`
### Monitor Logs
```bash
# API logs
pm2 logs sami-api
# Caddy logs
caddy logs
# System logs (Linux)
journalctl -u sami-api -f
```
## Security Considerations
1. **API Access**: The API server should only be accessible from localhost or trusted networks
2. **DNS Token**: Keep your Technitium API token secure
3. **Caddy Admin API**: Restrict access to localhost only
4. **CORS**: The API has CORS enabled - restrict origins in production
5. **Input Validation**: The API validates inputs but consider additional security layers
## Support
For issues or questions:
- Check the API README: `api/README.md`
- Review Caddy documentation: https://caddyserver.com/docs/
- Check Technitium DNS docs: https://technitium.com/dns/
## License
MIT

229
status/EMBY_DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,229 @@
# Deploying Emby Server with SAMI-CLOUD Dashboard
Quick guide to deploy Emby media server using the SAMI-CLOUD Status Dashboard.
## Prerequisites
1. Emby Server installed somewhere (Docker, Windows, Linux, etc.)
2. SAMI-CLOUD API server running (`node caddy-api.js`)
3. Know the IP and port where Emby is running
## Option 1: Deploy Existing Emby Server
If you already have Emby running somewhere, use the dashboard to add it:
### Step 1: Gather Information
You'll need:
- **IP Address**: Where Emby is running (e.g., `192.168.254.100`)
- **Port**: Emby's HTTP port (default: `8096`)
### Step 2: Deploy via Dashboard
1. Open the dashboard: `https://status.sami`
2. Click **"📱 App Selector"** button
3. Find **Emby** under the **Media** category
4. Fill in the deployment form:
```
Subdomain: emby
IP Address: 192.168.254.100
Port: 8096
DNS Type: Private DNS (creates DNS record)
SSL Type: Internal CA (local network)
```
5. Click **"Deploy App"**
### Step 3: Access Emby
Your Emby server will be accessible at:
```
https://emby.sami
```
## Option 2: Deploy Emby in Docker
If you don't have Emby yet, here's how to deploy it with Docker:
### Using Docker Compose
Create `docker-compose.yml`:
```yaml
version: '3'
services:
emby:
image: lscr.io/linuxserver/emby:latest
container_name: emby
environment:
- PUID=1000
- PGID=1000
- TZ=America/New_York
volumes:
- ./config:/config
- /path/to/media:/data/media
- /path/to/movies:/data/movies
- /path/to/tvshows:/data/tvshows
ports:
- "8096:8096"
restart: unless-stopped
```
Then:
```bash
docker-compose up -d
```
### Using Docker CLI
```bash
docker run -d \
--name=emby \
-e PUID=1000 \
-e PGID=1000 \
-e TZ=America/New_York \
-p 8096:8096 \
-v /path/to/config:/config \
-v /path/to/media:/data/media \
--restart unless-stopped \
lscr.io/linuxserver/emby:latest
```
### After Docker Deployment
1. Verify Emby is running: `docker ps`
2. Test access: `curl http://localhost:8096`
3. Use the Dashboard App Selector to add reverse proxy:
- IP: `localhost` or `172.17.0.1` (Docker bridge)
- Port: `8096`
## Option 3: Install Emby Directly
### Windows
1. Download Emby Server: https://emby.media/download.html
2. Install and run setup wizard
3. Note the IP and port (default: `8096`)
4. Use Dashboard App Selector to add it
### Linux
```bash
# Ubuntu/Debian
wget https://github.com/MediaBrowser/Emby.Releases/releases/download/4.8.0.62/emby-server-deb_4.8.0.62_amd64.deb
sudo dpkg -i emby-server-deb_4.8.0.62_amd64.deb
# Start service
sudo systemctl start emby-server
sudo systemctl enable emby-server
```
Then add to dashboard with IP and port `8096`.
## Emby Initial Setup
After deploying, complete Emby's setup wizard:
1. Open `https://emby.sami`
2. Choose your language
3. Create admin account
4. Add media libraries (Movies, TV Shows, Music, etc.)
5. Configure metadata providers
6. Set up remote access (if needed)
## Troubleshooting
### Can't access Emby through reverse proxy
Check that Emby allows the proxy:
1. Open Emby Settings → Network
2. Add to **"Allow remote connections from"**:
- Your Caddy server IP
- `127.0.0.1`
3. Ensure **"Enable automatic port mapping"** is off (not needed with reverse proxy)
### Certificate errors
If using Internal CA:
- Ensure you've installed the Caddy root certificate on your devices
- Export from: `https://dns1.sami/config/pki/ca/local/download`
### Performance issues
If streaming is slow:
1. Check network bandwidth
2. Enable hardware transcoding in Emby (Settings → Transcoding)
3. Adjust quality settings in Emby clients
### DNS not resolving
If `emby.sami` doesn't resolve:
```bash
# Test DNS
nslookup emby.sami dns1.sami
# Add manually if needed
# Windows: C:\Windows\System32\drivers\etc\hosts
# Linux/Mac: /etc/hosts
192.168.254.100 emby.sami
```
## Updating Emby Logo
The dashboard now has the official Emby logo. If you want to customize it:
1. Replace: `e:\CaddyCerts\sites\status\assets\emby.png`
2. Copy to production: `C:\caddy\sites\status\assets\emby.png`
3. Clear browser cache
## Advanced: Emby with Custom Settings
You can configure additional Caddy settings for Emby:
```caddyfile
emby.sami {
tls internal
# Increase timeouts for long transcoding operations
reverse_proxy http://192.168.254.100:8096 {
flush_interval -1
transport http {
dial_timeout 30s
response_header_timeout 0
read_timeout 0
}
}
}
```
## Next Steps
After Emby is deployed:
1. **Add Media Libraries** - Configure your movies, TV shows, music
2. **Install Plugins** - Trailers, Trakt, Theme Songs, etc.
3. **Setup Users** - Create accounts for family members
4. **Configure Clients** - Install Emby apps on phones, TVs, streaming devices
5. **Enable Live TV** - If you have TV tuners or IPTV
## Comparing Emby vs Plex vs Jellyfin
| Feature | Emby | Plex | Jellyfin |
|---------|------|------|----------|
| Free features | Most | Some | All |
| Hardware transcoding | Premiere only | Pass only | Free |
| Open source | No | No | Yes |
| Mobile apps | Paid | Free | Free |
| Best for | Power users | Everyone | Self-hosters |
## Resources
- Emby Documentation: https://support.emby.media/
- Emby Forums: https://emby.media/community/
- Docker Hub: https://hub.docker.com/r/emby/embyserver
- LinuxServer.io: https://docs.linuxserver.io/images/docker-emby/
## License
Emby has both free and Premiere (paid) versions. Some features require Emby Premiere subscription.

183
status/QUICK_DEPLOY_EMBY.md Normal file
View File

@@ -0,0 +1,183 @@
# Quick Emby Deployment Guide
## TL;DR - Deploy Emby Right Now
### If you have Emby already running:
1. Open dashboard: `https://status.sami`
2. Click **📱 App Selector**
3. Click **Emby** (under Media category)
4. Enter your Emby server's **IP** and **Port** (default: 8096)
5. Click **Deploy**
6. Access at: `https://emby.sami`
### If you need to install Emby first:
**Quick Docker Deploy:**
```bash
docker run -d \
--name=emby \
-e PUID=1000 \
-e PGID=1000 \
-e TZ=America/New_York \
-p 8096:8096 \
-v ./emby-config:/config \
-v /path/to/movies:/data/movies \
-v /path/to/tvshows:/data/tvshows \
--restart unless-stopped \
lscr.io/linuxserver/emby:latest
```
**Then add to dashboard:**
1. Dashboard → 📱 App Selector → Emby
2. IP: `localhost` or your server IP
3. Port: `8096`
4. Deploy!
## Deployment Form Values
```
Subdomain: emby
IP Address: [your server IP]
Port: 8096
DNS Type: ⦿ Private DNS (recommended for local network)
SSL Type: ⦿ Internal CA (recommended for .sami domain)
```
## What Happens When You Deploy
1. ✅ DNS A record created: `emby.sami → your IP`
2. ✅ Caddy reverse proxy configured
3. ✅ SSL certificate generated (internal CA)
4. ✅ Accessible at: `https://emby.sami`
## Common Deployment Scenarios
### Scenario 1: Emby on Windows PC
```
IP: 192.168.254.100 (your PC's IP)
Port: 8096
```
### Scenario 2: Emby in Docker on same Caddy server
```
IP: localhost or 172.17.0.1
Port: 8096
```
### Scenario 3: Emby on another server (Tailscale IP)
```
IP: 100.xx.xx.xx (Tailscale IP)
Port: 8096
```
### Scenario 4: Emby on NAS
```
IP: 192.168.254.50 (NAS IP)
Port: 8096
```
## Verify Deployment
```bash
# Test DNS
nslookup emby.sami dns1.sami
# Test HTTPS (from Caddy server)
curl -k https://emby.sami
# Check Caddy configuration
curl http://localhost:2019/config/ | jq '.apps.http.servers.srv0.routes[] | select(.["@id"] == "emby.sami")'
# Check if Emby responds
curl http://[your-emby-ip]:8096/emby/System/Info/Public
```
## Quick Fixes
**Emby not accessible after deployment:**
```bash
# Check Emby is running
curl http://[ip]:8096
# Check Caddy route exists
curl http://localhost:2019/id/emby.sami
# Check DNS record
curl "http://192.168.254.204:5380/api/zones/records/get?token=$TECHNITIUM_API_TOKEN&domain=emby.sami"
```
**Remove and redeploy:**
1. Delete the card from dashboard (🗑️ button)
2. Deploy again through App Selector
## Manual Caddy Configuration (Alternative)
If you prefer manual configuration, add to Caddyfile:
```caddyfile
emby.sami {
tls internal
reverse_proxy http://192.168.254.100:8096
}
```
Then:
```bash
caddy reload --config C:\caddy\Caddyfile
```
## Docker-Compose Full Stack
Deploy Emby + monitoring:
```yaml
version: '3.8'
services:
emby:
image: lscr.io/linuxserver/emby:latest
container_name: emby
environment:
- PUID=1000
- PGID=1000
- TZ=America/New_York
volumes:
- ./emby:/config
- /media/movies:/data/movies
- /media/tv:/data/tv
ports:
- "8096:8096"
restart: unless-stopped
```
Deploy:
```bash
docker-compose up -d
```
Then use App Selector to add reverse proxy!
## Next Steps After Deployment
1. **Initial Setup**: Visit `https://emby.sami` and complete wizard
2. **Add Libraries**: Settings → Library → Add Media Library
3. **Install Clients**: Get Emby apps for your devices
4. **Configure Transcoding**: Settings → Transcoding (enable hardware if supported)
5. **Setup Users**: Settings → Users → Add User
## Need Help?
- Full guide: See [EMBY_DEPLOYMENT.md](EMBY_DEPLOYMENT.md)
- API docs: See [api/README.md](api/README.md)
- General setup: See [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md)
## One-Liner Docker + Dashboard Deploy
```bash
# Start Emby
docker run -d --name=emby -p 8096:8096 -e TZ=America/New_York -v ./emby:/config lscr.io/linuxserver/emby:latest
# Then open https://status.sami and use App Selector!
```
That's it! 🎬

214
status/README.md Normal file
View File

@@ -0,0 +1,214 @@
# SAMI-CLOUD Status Dashboard
A modern, cross-platform status dashboard with built-in app deployment capabilities.
## Features
- **Real-time Service Monitoring** - Monitor all your services with live status checks
- **Weather Integration** - Display current weather conditions
- **One-Click App Deployment** - Deploy new apps with automatic DNS and reverse proxy configuration
- **Cross-Platform** - Works on Windows, Linux, and macOS
- **API-Driven** - Uses Caddy Admin API and Technitium DNS API (no scripts required)
- **PWA Support** - Install as a Progressive Web App
- **Responsive Design** - Works on desktop, tablet, and mobile
- **Dark/Light Themes** - Multiple theme options
## Quick Start
### 1. Install Dependencies
```bash
cd api
npm install
```
### 2. Set Environment Variables
```bash
# Windows (PowerShell)
$env:CADDY_ADMIN_API="http://localhost:2019"
$env:DNS_SERVER_API="http://192.168.254.204:5380"
$env:TECHNITIUM_API_TOKEN="your_token_here"
# Linux/macOS
export CADDY_ADMIN_API=http://localhost:2019
export DNS_SERVER_API=http://192.168.254.204:5380
export TECHNITIUM_API_TOKEN=your_token_here
```
### 3. Start the API Server
```bash
# Windows
cd api
start.bat
# Linux/macOS
cd api
./start.sh
```
### 4. Configure Caddy
Add to your Caddyfile:
```caddyfile
status.sami {
tls internal
handle /api/* {
reverse_proxy localhost:3001
}
root * /path/to/sites/status
file_server
}
```
### 5. Access Dashboard
Open your browser to:
```
https://status.sami
```
## Documentation
- **[Deployment Guide](DEPLOYMENT_GUIDE.md)** - Complete deployment instructions
- **[API Documentation](api/README.md)** - API server setup and configuration
- **[Caddyfile](C:/caddy/Caddyfile)** - Production Caddyfile example
## Project Structure
```
status/
├── index.html # Main dashboard page
├── apps.json # Service configuration
├── sw.js # Service worker for PWA
├── assets/ # Images, fonts, and static assets
├── api/ # Node.js API server
│ ├── caddy-api.js # Main API server
│ ├── package.json # Dependencies
│ ├── test-api.js # API test script
│ ├── start.bat # Windows startup script
│ ├── start.sh # Linux/macOS startup script
│ └── README.md # API documentation
├── DEPLOYMENT_GUIDE.md # Complete deployment guide
└── README.md # This file
```
## Key Technologies
- **Frontend**: Vanilla JavaScript, HTML5, CSS3
- **Backend**: Node.js, Express.js
- **Web Server**: Caddy v2
- **DNS**: Technitium DNS Server
- **APIs**:
- Caddy Admin API (reverse proxy configuration)
- Technitium DNS API (DNS record management)
## How App Deployment Works
1. **User Input** - User selects app template and enters configuration (subdomain, IP, port)
2. **DNS Creation** - API creates A record in Technitium DNS
3. **Caddy Configuration** - API adds reverse proxy route via Caddy Admin API
4. **Instant Access** - App is immediately accessible at `https://subdomain.sami`
5. **Automatic Rollback** - If any step fails, previous changes are rolled back
## Features in Detail
### Service Monitoring
- HTTP/HTTPS status checks
- Response time tracking
- Visual status indicators
- Configurable check intervals
### App Deployment
- Pre-configured app templates (Plex, Radarr, Sonarr, etc.)
- Custom app deployment
- Automatic DNS record creation
- Automatic Caddy reverse proxy configuration
- Internal CA or Let's Encrypt SSL
- Private or public DNS options
### Weather Widget
- Current conditions
- Temperature and wind speed
- Location-based
- Configurable via settings
### PWA Support
- Offline functionality
- Install to home screen
- App-like experience
- Service worker caching
## API Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/apps/deploy` | Deploy a new app |
| POST | `/api/apps/delete` | Delete an app |
| GET | `/api/services` | Get list of services |
| GET | `/api/caddy/config` | Get Caddy configuration |
| GET | `/api/caddy/test` | Test API connectivity |
| GET | `/health` | Health check |
## Configuration
### Environment Variables
- `CADDY_ADMIN_API` - Caddy Admin API URL (default: `http://localhost:2019`)
- `DNS_SERVER_API` - Technitium DNS API URL (default: `http://192.168.254.204:5380`)
- `TECHNITIUM_API_TOKEN` - API token for DNS operations (required)
### Service Configuration
Edit `apps.json` to add/remove services:
```json
[
{
"id": "myapp",
"name": "My App",
"logo": "assets/myapp.png",
"url": "https://myapp.sami"
}
]
```
## Browser Support
- Chrome/Edge (latest)
- Firefox (latest)
- Safari (latest)
- Mobile browsers (iOS Safari, Chrome Mobile)
## Security
- CORS enabled (configure for production)
- API access restricted to localhost by default
- Environment-based configuration
- Input validation on all endpoints
- Automatic rollback on deployment failures
## Troubleshooting
See [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md#troubleshooting) for detailed troubleshooting steps.
Common issues:
- **API not accessible**: Check if Node.js server is running
- **DNS not working**: Verify `TECHNITIUM_API_TOKEN` is set
- **Caddy routes not working**: Check Caddy Admin API is enabled
## Contributing
This is a personal project for SAMI-CLOUD infrastructure. Feel free to fork and adapt for your own use.
## License
MIT
## Author
SAMI-CLOUD

File diff suppressed because it is too large Load Diff

View File

@@ -58,6 +58,23 @@
style="width: 100%; padding: 8px 10px; background: var(--bg); color: var(--fg); border: 1px solid var(--border); border-radius: 6px; font-size: 0.85rem; font-family: monospace; box-sizing: border-box;" />
</div>
<!-- Quality Profile (shown for radarr/sonarr only) -->
<div id="svc-creds-quality" style="display: none; margin-bottom: 14px;">
<label class="label-bold">Quality Profile</label>
<p class="hint-micro">Used when requesting via Seerr</p>
<div style="display: flex; gap: 8px; align-items: center;">
<select id="svc-quality-select"
style="flex: 1; padding: 8px 10px; background: var(--bg); color: var(--fg); border: 1px solid var(--border); border-radius: 6px; font-size: 0.85rem; box-sizing: border-box;">
<option value="">-- Enter API key first --</option>
</select>
<button id="svc-quality-fetch" type="button"
style="padding: 8px 12px; font-size: 0.75rem; cursor: pointer; white-space: nowrap;">
Fetch
</button>
</div>
<div id="svc-quality-status" style="font-size: 0.75rem; margin-top: 4px; min-height: 1em;"></div>
</div>
<!-- Per-service Basic Auth (shown for non-external services) -->
<div id="svc-creds-basic" style="display: none; margin-bottom: 14px;">
<label class="label-bold">Service Login</label>
@@ -67,9 +84,12 @@
class="input-creds" />
</div>
<!-- Error message -->
<div id="svc-creds-error" style="display: none; padding: 8px 10px; margin-bottom: 10px; background: color-mix(in srgb, var(--error, #c62828) 12%, transparent); border: 1px solid var(--error, #c62828); border-radius: 6px; font-size: 0.8rem; color: var(--error, #c62828);"></div>
<!-- Buttons -->
<div style="display: flex; gap: 8px; margin-top: 14px;">
<button id="svc-creds-save" style="flex: 1; padding: 9px; background: var(--accent, #8FD6FF); color: #0b0f1a; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 0.85rem;">
<button id="svc-creds-save" class="btn-accent-solid" style="flex: 1; padding: 9px; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 0.85rem;">
Save
</button>
<button id="svc-creds-clear" style="padding: 9px 14px; background: transparent; color: var(--bad-fg, #ff9aa3); border: 1px solid var(--bad-fg, #ff9aa3); border-radius: 6px; cursor: pointer; font-size: 0.85rem; display: none;">
@@ -85,24 +105,50 @@
const modal = document.getElementById('service-creds-modal');
let currentService = null;
const arrServices = ['sonarr', 'radarr', 'prowlarr', 'overseerr'];
const qualityProfileServices = ['sonarr', 'radarr'];
function getServiceUrl(service) {
return service.externalUrl || service.url || '';
}
function showError(msg) {
const el = document.getElementById('svc-creds-error');
el.textContent = msg;
el.style.display = '';
}
function hideError() {
const el = document.getElementById('svc-creds-error');
el.textContent = '';
el.style.display = 'none';
}
window.openServiceCredsModal = async function(service) {
currentService = service;
hideError();
const title = document.getElementById('svc-creds-title');
const desc = document.getElementById('svc-creds-desc');
const seedhostSection = document.getElementById('svc-creds-seedhost');
const apikeySection = document.getElementById('svc-creds-apikey');
const basicSection = document.getElementById('svc-creds-basic');
const qualitySection = document.getElementById('svc-creds-quality');
title.textContent = service.name + ' Credentials';
// Determine which sections to show
const isExt = !!service.isExternal;
const isArr = arrServices.includes(service.id) || arrServices.includes(service.appTemplate);
const hasQuality = qualityProfileServices.includes(service.id) || qualityProfileServices.includes(service.appTemplate);
seedhostSection.style.display = isExt ? '' : 'none';
apikeySection.style.display = isArr ? '' : 'none';
qualitySection.style.display = hasQuality ? '' : 'none';
basicSection.style.display = !isExt ? '' : 'none';
// Reset quality dropdown
const qualSelect = document.getElementById('svc-quality-select');
qualSelect.innerHTML = '<option value="">-- Enter API key first --</option>';
document.getElementById('svc-quality-status').textContent = '';
if (isExt) {
desc.textContent = 'Seedhost credentials auto-login past the HTTP prompt. API key bypasses the app login.';
// Update password placeholder with service name
@@ -160,6 +206,12 @@
} catch (e) { /* ignore */ }
if (document.getElementById('svc-basic-pass')) document.getElementById('svc-basic-pass').value = '';
// Load quality profile for arr services
const svcId = service.id || service.appTemplate;
if (qualityProfileServices.includes(svcId)) {
await loadQualityProfiles(service);
}
if (hasCreds) {
dot.style.background = 'var(--ok-fg, #74dfc4)';
status.style.color = 'var(--ok-fg, #74dfc4)';
@@ -176,14 +228,129 @@
}
}
// Fetch and populate quality profiles dropdown
async function loadQualityProfiles(service) {
const qualSelect = document.getElementById('svc-quality-select');
const qualStatus = document.getElementById('svc-quality-status');
const svcId = service.id || service.appTemplate;
const svcUrl = getServiceUrl(service);
if (!svcUrl) {
qualSelect.innerHTML = '<option value="">-- No service URL --</option>';
return;
}
qualSelect.innerHTML = '<option value="">Loading...</option>';
qualStatus.textContent = '';
try {
const params = new URLSearchParams({ service: svcId, url: svcUrl });
const res = await fetch(`/api/v1/arr/quality-profiles?${params}`);
const data = await res.json();
if (!data.success || !data.profiles?.length) {
qualSelect.innerHTML = '<option value="">-- No profiles found (enter API key and click Fetch) --</option>';
return;
}
qualSelect.innerHTML = '';
for (const p of data.profiles) {
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = p.name;
qualSelect.appendChild(opt);
}
// Pre-select stored profile or best match for "720"
if (data.storedProfileId) {
qualSelect.value = String(data.storedProfileId);
}
if (!qualSelect.value) {
// Try to find a 720p-ish profile
const match720 = data.profiles.find(p => /720/i.test(p.name));
if (match720) qualSelect.value = String(match720.id);
}
if (!qualSelect.value && data.profiles.length) {
qualSelect.value = String(data.profiles[0].id);
}
qualStatus.innerHTML = `<span style="color: var(--ok-fg);">${data.profiles.length} profiles loaded</span>`;
} catch (e) {
qualSelect.innerHTML = '<option value="">-- Failed to load --</option>';
qualStatus.innerHTML = `<span style="color: var(--error, #c62828);">Error: ${e.message}</span>`;
}
}
// Fetch button for quality profiles
document.getElementById('svc-quality-fetch')?.addEventListener('click', async () => {
if (!currentService) return;
const svcId = currentService.id || currentService.appTemplate;
const svcUrl = getServiceUrl(currentService);
const apiKeyInput = document.getElementById('svc-apikey-input');
const apiKey = apiKeyInput?.value.trim();
const qualSelect = document.getElementById('svc-quality-select');
const qualStatus = document.getElementById('svc-quality-status');
if (!svcUrl) {
qualStatus.innerHTML = '<span style="color: var(--error, #c62828);">No service URL available</span>';
return;
}
if (!apiKey || apiKey === '••••••••') {
qualStatus.innerHTML = '<span style="color: var(--error, #c62828);">Enter an API key first</span>';
return;
}
qualSelect.innerHTML = '<option value="">Fetching...</option>';
qualStatus.textContent = '';
try {
const params = new URLSearchParams({ service: svcId, url: svcUrl, apiKey });
const res = await fetch(`/api/v1/arr/quality-profiles?${params}`);
const data = await res.json();
if (!data.success) {
qualSelect.innerHTML = '<option value="">-- Error --</option>';
qualStatus.innerHTML = `<span style="color: var(--error, #c62828);">${data.error || 'Failed to fetch profiles'}</span>`;
return;
}
if (!data.profiles?.length) {
qualSelect.innerHTML = '<option value="">-- No profiles found --</option>';
return;
}
qualSelect.innerHTML = '';
for (const p of data.profiles) {
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = p.name;
qualSelect.appendChild(opt);
}
// Pre-select 720p match
const match720 = data.profiles.find(p => /720/i.test(p.name));
if (match720) qualSelect.value = String(match720.id);
else if (data.profiles.length) qualSelect.value = String(data.profiles[0].id);
qualStatus.innerHTML = `<span style="color: var(--ok-fg);">${data.profiles.length} profiles loaded</span>`;
} catch (e) {
qualSelect.innerHTML = '<option value="">-- Error --</option>';
qualStatus.innerHTML = `<span style="color: var(--error, #c62828);">${e.message}</span>`;
}
});
// Save button
document.getElementById('svc-creds-save')?.addEventListener('click', async () => {
if (!currentService) return;
const saveBtn = document.getElementById('svc-creds-save');
saveBtn.textContent = 'Saving...';
saveBtn.disabled = true;
hideError();
try {
const isArr = arrServices.includes(currentService.id) || arrServices.includes(currentService.appTemplate);
const svcId = currentService.id || currentService.appTemplate;
// Save seedhost creds (shared username + per-service password)
if (currentService.isExternal) {
const user = document.getElementById('svc-seedhost-user').value.trim();
@@ -197,15 +364,58 @@
}
}
// Save API key
// Save API key — for arr services, use the arr credentials endpoint (correct namespace)
const apiKeyInput = document.getElementById('svc-apikey-input');
const apiKey = apiKeyInput.value.trim();
const apiKey = apiKeyInput?.value.trim();
if (apiKey && apiKey !== '••••••••') {
await secureFetch(`/api/v1/services/${currentService.id}/credentials`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ apiKey })
});
if (isArr) {
// Use arr credentials endpoint — validates key, tests connection, stores in arr.* namespace
const svcUrl = getServiceUrl(currentService);
const qualSelect = document.getElementById('svc-quality-select');
const qualityProfileId = qualSelect?.value ? parseInt(qualSelect.value) : undefined;
const qualityProfileName = qualSelect?.selectedOptions?.[0]?.textContent || undefined;
const res = await secureFetch('/api/v1/arr/credentials', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
service: svcId,
apiKey,
url: svcUrl || undefined,
qualityProfileId: qualityProfileId || undefined,
qualityProfileName: qualityProfileName || undefined
})
});
const data = await res.json();
if (!data.success) {
showError(data.error || 'Failed to save API key');
saveBtn.textContent = 'Save';
saveBtn.disabled = false;
return;
}
if (data.connectionTest && !data.connectionTest.success) {
showError(`API key saved but connection test failed: ${data.connectionTest.error}`);
}
} else {
// Non-arr services use the generic endpoint
await secureFetch(`/api/v1/services/${currentService.id}/credentials`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ apiKey })
});
}
} else if (isArr && qualityProfileServices.includes(svcId)) {
// API key unchanged but user may have changed quality profile — save profile only
const qualSelect = document.getElementById('svc-quality-select');
const qualityProfileId = qualSelect?.value ? parseInt(qualSelect.value) : undefined;
const qualityProfileName = qualSelect?.selectedOptions?.[0]?.textContent || undefined;
if (qualityProfileId) {
await secureFetch('/api/v1/arr/quality-profiles', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ service: svcId, qualityProfileId, qualityProfileName })
});
}
}
// Save per-service basic auth
@@ -224,6 +434,7 @@
await loadServiceCreds(currentService);
} catch (e) {
console.error('Failed to save credentials:', e);
showError('Failed to save: ' + (e.message || 'Unknown error'));
}
saveBtn.textContent = 'Save';
saveBtn.disabled = false;
@@ -233,16 +444,24 @@
document.getElementById('svc-creds-clear')?.addEventListener('click', async () => {
if (!currentService) return;
if (!confirm(`Remove stored credentials for ${currentService.name}?`)) return;
hideError();
try {
const svcId = currentService.id || currentService.appTemplate;
const isArr = arrServices.includes(svcId);
if (currentService.isExternal) {
await secureFetch(`/api/v1/seedhost-creds?serviceId=${currentService.id}`, { method: 'DELETE' });
}
// Delete from both namespaces
await secureFetch(`/api/v1/services/${currentService.id}/credentials`, { method: 'DELETE' });
if (isArr) {
await secureFetch(`/api/v1/arr/credentials/${svcId}`, { method: 'DELETE' });
}
const btn = document.getElementById(`creds-btn-${currentService.id}`);
if (btn) btn.classList.remove('has-creds');
await loadServiceCreds(currentService);
} catch (e) {
console.error('Failed to clear credentials:', e);
showError('Failed to clear: ' + (e.message || 'Unknown error'));
}
});

View File

@@ -12,7 +12,7 @@
<!-- Setup Button (not configured state) -->
<div id="totp-setup-section">
<button id="totp-setup-btn" style="width: 100%; padding: 12px; background: var(--accent, #8FD6FF); color: #0b0f1a; border: none; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 0.95rem;">
<button id="totp-setup-btn" class="btn-accent-solid" style="width: 100%; padding: 12px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 0.95rem;">
Generate New Secret
</button>
<div style="display: flex; align-items: center; gap: 8px; margin: 10px 0 0;">
@@ -29,6 +29,7 @@
Import
</button>
</div>
<div id="totp-import-error" style="color: var(--bad-fg, #ff9aa3); font-size: 0.8rem; min-height: 1.2em; margin-top: 6px;"></div>
</div>
</div>
@@ -55,7 +56,7 @@
<div style="display: flex; gap: 8px; margin-top: 8px;">
<input type="text" id="totp-setup-code" maxlength="6" inputmode="numeric" pattern="[0-9]{6}" placeholder="000000"
style="flex: 1; text-align: center; font-size: 1.3rem; padding: 10px; font-family: 'Sami Grotesk', monospace; letter-spacing: 4px; background: var(--bg); color: var(--fg); border: 2px solid var(--border); border-radius: 8px; outline: none;" />
<button id="totp-confirm-setup" style="padding: 10px 20px; background: var(--accent, #8FD6FF); color: #0b0f1a; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
<button id="totp-confirm-setup" class="btn-accent-solid" style="padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
Confirm
</button>
</div>
@@ -191,7 +192,12 @@
// Import existing secret button
document.getElementById('totp-import-btn')?.addEventListener('click', async () => {
const secret = document.getElementById('totp-import-key').value.trim();
if (!secret) return;
const errorEl = document.getElementById('totp-import-error');
errorEl.textContent = '';
if (!secret) {
errorEl.textContent = 'Paste a Base32 secret key first';
return;
}
try {
const res = await secureFetch('/api/v1/totp/setup', {
method: 'POST',
@@ -200,6 +206,7 @@
});
const data = await res.json();
if (data.success) {
errorEl.textContent = '';
document.getElementById('totp-qr-image').src = data.qrCode;
document.getElementById('totp-manual-key').textContent = data.manualKey;
document.getElementById('totp-setup-section').style.display = 'none';
@@ -208,11 +215,10 @@
document.getElementById('totp-setup-error').textContent = '';
document.getElementById('totp-setup-code').focus();
} else {
document.getElementById('totp-import-key').style.borderColor = 'var(--bad-fg)';
setTimeout(() => { document.getElementById('totp-import-key').style.borderColor = ''; }, 2000);
errorEl.textContent = data.error || data.message || 'Import failed';
}
} catch (e) {
console.error('TOTP import failed:', e);
errorEl.textContent = 'Connection error — try refreshing the page';
}
});