Update README and add proprietary license

- Rename to ISP IP Manager
- Add copyright notice and closed-source declaration
- Document webhook improvements (immediate mode, queue management)
- Document user authentication and RBAC features
- Document cache busting for JavaScript files
- Add BunnyCDN setup instructions with regional endpoints
- Add troubleshooting for common webhook issues
- Add proprietary LICENSE file

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Purple
2026-01-18 12:14:59 +00:00
parent 5ead456620
commit d1b28cf0d4
2 changed files with 152 additions and 325 deletions

33
LICENSE Normal file
View File

@@ -0,0 +1,33 @@
PROPRIETARY SOFTWARE LICENSE
Copyright (c) 2025 Purple Computing Ltd. All rights reserved.
This software and associated documentation files (the "Software") are the
proprietary and confidential property of Purple Computing Ltd.
NOTICE: All information contained herein is, and remains the property of
Purple Computing Ltd and its suppliers, if any. The intellectual and
technical concepts contained herein are proprietary to Purple Computing Ltd
and its suppliers and may be covered by UK and Foreign Patents, patents in
process, and are protected by trade secret or copyright law.
RESTRICTIONS:
- You may not copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software without explicit written permission from
Purple Computing Ltd.
- You may not reverse engineer, decompile, or disassemble the Software.
- You may not use the Software for any purpose other than that for which
it was originally licensed.
UNAUTHORIZED COPYING, MODIFICATION, DISTRIBUTION, OR USE OF THIS SOFTWARE
IS STRICTLY PROHIBITED.
For licensing inquiries, contact: info@purplecomputing.com
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
PURPLE COMPUTING LTD BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

444
README.md
View File

@@ -1,25 +1,47 @@
# Geofeed Manager # ISP IP Manager
A complete solution for managing RFC 8805 compliant IP geolocation feeds (geofeeds). This system provides a modern web interface for managing geofeed entries, stores data in MariaDB/MySQL, and automatically exports to BunnyCDN via n8n workflows. A complete solution for managing RFC 8805 compliant IP geolocation feeds (geofeeds). This system provides a modern web interface for managing geofeed entries, stores data in MariaDB/MySQL, and automatically exports to BunnyCDN via n8n workflows.
**Copyright (c) 2025 Purple Computing Ltd. All rights reserved.**
This is proprietary closed-source software. Unauthorized copying, modification, distribution, or use of this software is strictly prohibited.
## Features ## Features
- **Modern Apple-esque UI** - Clean, responsive interface with dark mode support - **Modern Apple-esque UI** - Clean, responsive interface with dark mode support
- **RFC 8805 Compliant** - Generates valid geofeed CSV files per the specification - **RFC 8805 Compliant** - Generates valid geofeed CSV files per the specification
- **Authentication** - Secure login with environment-based credentials - **Role-Based Access Control** - Admin and staff roles with Cloudflare Access integration
- **CRUD Operations** - Create, read, update, and delete geofeed entries - **CRUD Operations** - Create, read, update, and delete geofeed entries
- **Search & Filter** - Find entries by IP prefix, city, region, or country - **Search & Filter** - Find entries by IP prefix, city, region, or country
- **Audit Logging** - Track all changes to your geofeed with detailed history - **Audit Logging** - Track all changes with user attribution (email-based)
- **IP Enrichment** - Automatic ISP, hostname, and security flag data via ipregistry.co - **IP Enrichment** - Automatic ISP, hostname, and security flag data via ipregistry.co
- **Client Logos** - Associate logo images with client shortnames - **Client Logos** - Associate logo images with client shortnames
- **Webhook Integration** - Debounced n8n webhooks for on-demand CDN updates - **Webhook Integration** - Immediate or debounced n8n webhooks for CDN updates
- **Mobile Optimized** - Full mobile Safari support with PWA capabilities - **Mobile Optimized** - Full mobile Safari support with PWA capabilities
- **CSRF Protection** - Secure form submissions - **CSRF Protection** - Secure form submissions
- **Developer Tools** - Database backup/restore, schema sync, and error log viewer - **Developer Tools** - Database backup/restore, schema sync, and error log viewer
- **PTR Record Management** - AWS Route53 integration for checking reverse DNS records - **PTR Record Management** - AWS Route53 integration for checking reverse DNS records
- **Cache Busting** - Automatic JavaScript versioning for reliable deployments
## What's New ## What's New
### Webhook System Improvements
- **Immediate Webhooks** - Set delay to 0 for instant webhook delivery when entries change
- **Queue Management** - Clear pending, failed, or all webhook history from the UI
- **Automatic Processing** - Webhooks now process automatically (no cron required for immediate mode)
- **Queue Status** - View pending, completed, and failed webhooks with HTTP status codes
### User Management & Authentication
- **Cloudflare Access Integration** - Users authenticated via CF Access headers
- **Database-Driven Roles** - Admin users stored in `admin_users` table
- **User Attribution** - Audit log now shows user email instead of IP address
- **Display Names** - Optional display names for admin users
### UI Improvements
- **Client Settings Tab** - Renamed from "Advanced" for clarity
- **Cache Busting** - JavaScript files now include version parameter to prevent stale caches
- **Improved Error Handling** - Better error messages for webhook failures
### Whitelabel Settings ### Whitelabel Settings
- **Custom Branding** - Customize the application with your company name and logo - **Custom Branding** - Customize the application with your company name and logo
- **Webapp Icon** - Set a custom header icon (SVG or PNG) - **Webapp Icon** - Set a custom header icon (SVG or PNG)
@@ -46,7 +68,7 @@ A complete solution for managing RFC 8805 compliant IP geolocation feeds (geofee
### Entry Details Modal ### Entry Details Modal
- Click the info (i) icon on any entry to view full details - Click the info (i) icon on any entry to view full details
- Shows all enrichment data including hostname, ISP, ASN, coordinates - Shows all enrichment data including hostname, ISP, ASN, coordinates
- Displays all 11 security flags with checkmarks (✓) and crosses (✗) - Displays all 11 security flags with checkmarks and crosses
- Shows record timestamps (created, updated, enriched) - Shows record timestamps (created, updated, enriched)
### IP Registry Integration ### IP Registry Integration
@@ -56,26 +78,10 @@ A complete solution for managing RFC 8805 compliant IP geolocation feeds (geofee
- Security flags for: Abuser, Attacker, Bogon, Cloud Provider, Proxy, Relay, Tor, Tor Exit, VPN, Anonymous, Threat - Security flags for: Abuser, Attacker, Bogon, Cloud Provider, Proxy, Relay, Tor, Tor Exit, VPN, Anonymous, Threat
- Manual enrichment option for existing entries - Manual enrichment option for existing entries
### Authentication
- Secure login page with session-based authentication
- Credentials configured via environment variables
- Automatic session timeout after 24 hours
### Webhook System
- On-demand webhook notifications to n8n (replaces hourly polling)
- Debouncing to batch multiple changes and reduce API calls
- Queue status monitoring in the Advanced tab
### UI Improvements
- Dark mode with automatic OS detection
- Mobile Safari optimizations with safe area support
- Client logo management with grid display
- Wider table layout for better data visibility
## Directory Structure ## Directory Structure
``` ```
geofeed-manager/ ip-manager/
├── database/ ├── database/
│ ├── schema.sql # Database schema │ ├── schema.sql # Database schema
│ └── import_csv.php # CSV import utility (CLI) │ └── import_csv.php # CSV import utility (CLI)
@@ -84,7 +90,12 @@ geofeed-manager/
│ ├── api.php # RESTful API endpoints │ ├── api.php # RESTful API endpoints
│ ├── login.php # Authentication page │ ├── login.php # Authentication page
│ ├── index.php # Main web interface │ ├── index.php # Main web interface
│ ├── error.log # PHP error log (auto-created) │ ├── settings.php # Settings & admin interface
│ ├── includes/
│ │ ├── app.js # Main application JavaScript
│ │ ├── auth.php # Authentication & RBAC helpers
│ │ ├── header.php # Page header
│ │ └── footer.php # Page footer with cache-busted JS
│ └── .htaccess # Security rules │ └── .htaccess # Security rules
├── n8n/ ├── n8n/
│ └── geofeed-export-workflow.json # n8n workflow │ └── geofeed-export-workflow.json # n8n workflow
@@ -102,7 +113,7 @@ The application automatically pulls code from the Git repository on startup - no
```env ```env
# Git Repository # Git Repository
GIT_REPO=https://git.prpl.tools/PurpleComputing/geofeed-manager.git GIT_REPO=https://git.prpl.tools/PurpleComputing/ip-manager.git
GIT_BRANCH=main GIT_BRANCH=main
# Database # Database
@@ -111,10 +122,13 @@ DB_NAME=geofeed_manager
DB_USER=geofeed DB_USER=geofeed
DB_PASSWORD=your_secure_password DB_PASSWORD=your_secure_password
# Authentication # Authentication (fallback when Cloudflare Access not available)
AUTH_USERNAME=admin AUTH_USERNAME=admin
AUTH_PASSWORD=your_secure_admin_password AUTH_PASSWORD=your_secure_admin_password
# Admin Users (comma-separated emails for initial seeding)
ADMIN_EMAILS=admin@example.com,user@example.com
# IP Registry (optional - for IP enrichment) # IP Registry (optional - for IP enrichment)
IPREGISTRY_API_KEY=your_ipregistry_api_key IPREGISTRY_API_KEY=your_ipregistry_api_key
@@ -130,9 +144,9 @@ docker compose up -d
3. **Access the web interface** at `http://your-server:8080` 3. **Access the web interface** at `http://your-server:8080`
4. **Login** with your configured credentials (default: admin/changeme) 4. **Login** with your configured credentials or via Cloudflare Access
5. **Import your geofeed** via the Advanced tab in the UI 5. **Import your geofeed** via the Client Settings tab in Settings
### How It Works ### How It Works
@@ -171,20 +185,27 @@ Or in Dokploy, just redeploy the service.
### Authentication ### Authentication
Authentication is required to access the application. Configure credentials via environment variables: The application supports two authentication methods:
```env **1. Cloudflare Access (Recommended)**
AUTH_USERNAME=admin - Users are authenticated via Cloudflare Access headers
AUTH_PASSWORD=your_secure_password - Email is read from `CF-Access-Authenticated-User-Email` header
``` - Roles are determined by the `admin_users` database table
**Default Credentials:** **2. Session-based Authentication (Fallback)**
- Username: `admin` - Used when Cloudflare Access headers are not present
- Password: `changeme` - Configure via environment variables:
```env
AUTH_USERNAME=admin
AUTH_PASSWORD=your_secure_password
```
> **Warning:** Change the default password immediately after deployment! Set `AUTH_PASSWORD` in your `.env` file or environment variables. ### User Roles
The login session expires after 24 hours of inactivity. - **Admin** - Full access to all features including user management and danger zone
- **Staff** - Standard access to geofeed management features
Admin users are stored in the `admin_users` table and can be managed via the Users tab in Settings.
### IP Registry Integration ### IP Registry Integration
@@ -195,140 +216,67 @@ To enable automatic IP enrichment:
```env ```env
IPREGISTRY_API_KEY=your_api_key IPREGISTRY_API_KEY=your_api_key
``` ```
Or configure it in the Advanced tab of the web interface. Or configure it in Settings > n8n Integration tab.
3. Enable auto-enrichment in the Advanced tab 3. Enable auto-enrichment in the settings
When enabled, new IP entries are automatically enriched with:
- Hostname (reverse DNS)
- ISP and organization name
- ASN information
- Connection type
- Timezone and coordinates
- Security flags (proxy, VPN, Tor, threat, etc.)
### AWS Route53 Integration ### AWS Route53 Integration
Configure AWS credentials in the Advanced tab to enable PTR record checking: Configure AWS credentials in the Settings > n8n Integration tab to enable PTR record checking:
1. Create an IAM user in AWS with `route53:ListHostedZones` and `route53:ListResourceRecordSets` permissions 1. Create an IAM user in AWS with Route53 read permissions
2. Generate an access key and secret key for the IAM user 2. Generate an access key and secret key for the IAM user
3. In the Advanced tab, enter: 3. Enter AWS credentials in the settings
- **AWS Access Key ID** - Your IAM access key
- **AWS Secret Access Key** - Your IAM secret key
- **AWS Region** - Select your preferred region (Route53 is global, but a region is required for API signing)
- **Hosted Zone IDs** - Comma-separated list of Route53 hosted zone IDs (e.g., `Z1234567890ABC, Z0987654321DEF`)
4. Click "Test Connection" to verify credentials 4. Click "Test Connection" to verify credentials
5. Go to the PTR tab to view A records and check PTR status 5. Go to the PTR tab to view A records and check PTR status
**IAM Policy Example:**
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"route53:ListHostedZones",
"route53:ListResourceRecordSets",
"route53:GetHostedZone"
],
"Resource": "*"
}
]
}
```
### Webhook Integration ### Webhook Integration
Configure webhooks in the Advanced tab to notify n8n when data changes: Configure webhooks in Settings > n8n Integration tab:
1. Enter your n8n webhook URL 1. Enter your n8n webhook URL
2. Set the debounce delay (1-60 minutes) 2. Set the debounce delay (0 for immediate, 1-60 minutes for batched)
3. Enable webhook notifications 3. Enable webhook notifications
The system batches multiple changes within the debounce window to reduce API calls. **Immediate Mode (delay = 0):**
- Webhooks are sent instantly when entries change
- Each change triggers a separate webhook
- Best for real-time updates
### n8n Workflow Setup **Batched Mode (delay > 0):**
- Multiple changes within the delay window are consolidated
- Reduces API calls for bulk operations
- Webhooks process when the delay expires
1. In n8n, go to **Settings > Environment Variables** and add: ### Webhook Queue Management
- `BUNNY_STORAGE_ZONE` - Your BunnyCDN storage zone name
- `BUNNY_API_KEY` - Your BunnyCDN Storage API key
2. Create MySQL credentials in n8n: The webhook queue can be managed from the n8n Integration tab:
- Go to **Credentials**
- Add new **MySQL** credential
- Configure with your database details
- Note the credential ID
3. Import the workflow: - **View Status** - See pending, completed, and failed webhooks
- Go to **Workflows** - **Clear Pending** - Remove webhooks waiting to be sent
- Click **Import from File** - **Clear Failed** - Remove failed webhook records
- Select `n8n/geofeed-export-workflow.json` - **Clear All** - Remove entire webhook history
4. Update credential references:
- Open the imported workflow
- For each MySQL node, select your MySQL credential
- Save the workflow
5. Activate the workflow - it will trigger via webhook when data changes
## Developer Tools
Access developer tools via the **Developer** tab in the web interface.
### Database Backup & Restore
- **Download Full Backup** - Exports all data as a timestamped JSON file including:
- All geofeed entries with enrichment data
- Application settings
- Client logos
- Last 1000 audit log entries
- **Import Backup** - Restore from a previously exported JSON file
- Validates backup structure before import
- Replaces all existing data
- Shows import summary with counts
### Database Schema Sync
Keep your database schema up-to-date with the latest code:
1. Click **Check for Updates** to compare your database against the repository schema
2. View missing tables, columns, and indexes
3. Click **Apply Schema Updates** to add missing elements
This is useful after pulling new code that includes database changes.
### PHP Error Logs
- View recent PHP errors directly in the browser
- Filter by number of lines (50, 100, 250, 500)
- Color-coded by severity (fatal, error, warning, notice)
- Clear logs with one click
### System Information
View current system status:
- App and PHP versions
- Server software and timezone
- Database statistics (entries, enriched count, settings, audit log size)
- Database size on disk
## API Reference ## API Reference
### Authentication ### Authentication
All API endpoints (except `export` and `webhook_process`) require authentication. All API endpoints (except `export` and `webhook_process`) require authentication.
### Webhook Queue Clear
```
POST api.php?action=webhook_queue_clear
Content-Type: application/json
{
"clear_type": "pending|failed|all",
"csrf_token": "..."
}
```
### List Entries ### List Entries
``` ```
GET api.php?action=list&page=1&limit=25&search=term&country=GB&sort=ip|custom GET api.php?action=list&page=1&limit=25&search=term&country=GB&sort=ip|custom
``` ```
### Get Single Entry
```
GET api.php?action=get&id=123
```
### Create Entry ### Create Entry
``` ```
POST api.php?action=create POST api.php?action=create
@@ -378,148 +326,6 @@ Content-Type: application/json
GET api.php?action=export&format=download GET api.php?action=export&format=download
``` ```
### Get Statistics
```
GET api.php?action=stats
```
### Enrich Single IP
```
POST api.php?action=enrich_ip
Content-Type: application/json
{
"id": 123,
"csrf_token": "..."
}
```
### Enrich All Un-enriched IPs
```
POST api.php?action=enrich_all
Content-Type: application/json
{
"csrf_token": "..."
}
```
### Database Backup
```
GET api.php?action=database_backup
```
Returns a JSON file download with full database backup.
### Database Import
```
POST api.php?action=database_import
Content-Type: application/json
{
"backup_data": { ... },
"csrf_token": "..."
}
```
### Schema Check
```
GET api.php?action=schema_check
```
Returns missing tables, columns, and indexes compared to repository schema.
### Schema Apply
```
POST api.php?action=schema_apply
Content-Type: application/json
{
"csrf_token": "..."
}
```
### Error Logs
```
GET api.php?action=error_logs&lines=100
```
### Clear Error Logs
```
POST api.php?action=error_logs_clear
Content-Type: application/json
{
"csrf_token": "..."
}
```
### System Info
```
GET api.php?action=system_info
```
### AWS Settings
```
GET api.php?action=aws_settings_get
```
Returns current AWS Route53 configuration.
```
POST api.php?action=aws_settings_save
Content-Type: application/json
{
"aws_access_key_id": "AKIAIOSFODNN7EXAMPLE",
"aws_secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"aws_region": "us-east-1",
"aws_hosted_zones": "Z1234567890ABC, Z0987654321DEF",
"csrf_token": "..."
}
```
### Test AWS Connection
```
GET api.php?action=aws_test
```
Tests AWS credentials and returns hosted zone count.
### List AWS Hosted Zones
```
GET api.php?action=aws_zones
```
Returns list of configured hosted zones with their names and IDs.
### List A Records
```
GET api.php?action=aws_records&zone_id=Z1234567890ABC
```
Returns all A records from the specified hosted zone.
### PTR Lookup
```
GET api.php?action=ptr_lookup&ip=192.168.1.1
```
Performs reverse DNS lookup and returns the PTR record for the given IP.
### Get Whitelabel Settings
```
GET api.php?action=whitelabel_get
```
Returns current whitelabel configuration (company name, icon URL, favicon URL, default import URL).
### Save Whitelabel Settings
```
POST api.php?action=whitelabel_save
Content-Type: application/json
{
"company_name": "My Company",
"icon_url": "https://example.com/logo.svg",
"favicon_url": "https://example.com/favicon.ico",
"default_import_url": "https://example.com/geofeed.csv",
"csrf_token": "..."
}
```
## Geofeed Format (RFC 8805) ## Geofeed Format (RFC 8805)
Each line in the exported CSV follows this format: Each line in the exported CSV follows this format:
@@ -530,7 +336,7 @@ ip_prefix,country_code,region_code,city,postal_code
Example: Example:
```csv ```csv
# Geofeed - Generated by Geofeed Manager # Geofeed - Generated by ISP IP Manager
# Format: ip_prefix,country_code,region_code,city,postal_code # Format: ip_prefix,country_code,region_code,city,postal_code
192.168.1.0/24,GB,GB-ENG,London,EC1A 1BB 192.168.1.0/24,GB,GB-ENG,London,EC1A 1BB
10.0.0.0/8,US,US-CA,San Francisco,94105 10.0.0.0/8,US,US-CA,San Francisco,94105
@@ -541,8 +347,12 @@ Example:
1. Create a Storage Zone in BunnyCDN 1. Create a Storage Zone in BunnyCDN
2. Get your Storage API key from the FTP & API Access section 2. Get your Storage API key from the FTP & API Access section
3. The workflow uploads to: `https://storage.bunnycdn.com/{zone}/geofeed.csv` 3. Use the regional endpoint URL (e.g., `uk.storage.bunnycdn.com` for UK)
4. Your public URL will be: `https://{zone}.b-cdn.net/geofeed.csv` 4. Configure the n8n HTTP Request node with:
- Method: PUT
- URL: `https://uk.storage.bunnycdn.com/{zone}/geofeed.csv`
- Headers: `AccessKey: your-storage-password`, `Content-Type: application/octet-stream`
5. Your public URL will be: `https://{zone}.b-cdn.net/geofeed.csv`
## Security Considerations ## Security Considerations
@@ -557,62 +367,46 @@ Example:
## Troubleshooting ## Troubleshooting
### Cannot login ### Cannot login
- Verify AUTH_USERNAME and AUTH_PASSWORD environment variables are set - Verify Cloudflare Access is configured correctly
- Check container logs for authentication errors - Check AUTH_USERNAME and AUTH_PASSWORD environment variables
- Clear browser cookies and try again - Clear browser cookies and try again
### Webhooks not sending
- Ensure webhook URL is configured and enabled
- Set delay to 0 for immediate delivery
- Check the webhook queue status for errors
- Verify n8n workflow is active and in "Production" mode
### Webhook shows as failed
- Check the HTTP response code in the queue status
- 404 - Webhook URL not found or n8n workflow not active
- 401/403 - Authentication issue with the webhook endpoint
### Stale JavaScript (old version running)
- The app includes automatic cache busting via version parameters
- If issues persist, clear browser cache or use incognito mode
- Redeploy the application to trigger a new git pull
### Import fails with "Invalid IP prefix" ### Import fails with "Invalid IP prefix"
Ensure your IP prefixes are in valid CIDR notation (e.g., `192.168.1.0/24`) Ensure your IP prefixes are in valid CIDR notation (e.g., `192.168.1.0/24`)
### IP enrichment not working ### IP enrichment not working
- Verify your ipregistry.co API key is valid - Verify your ipregistry.co API key is valid
- Check that auto-enrichment is enabled in the Advanced tab - Check that auto-enrichment is enabled in Settings
- Review error logs in the Developer tab for API errors - Review error logs in the Developer tab for API errors
### Hostname not showing
- Hostname requires re-enriching entries (the API parameter was added recently)
- Click the globe icon on individual entries to re-enrich them
### n8n workflow fails ### n8n workflow fails
- Check that environment variables are set correctly - Check that environment variables are set correctly
- Verify MySQL credentials are configured - Verify MySQL credentials are configured
- Check BunnyCDN API key permissions - Check BunnyCDN API key permissions
### Web interface shows database error
- Verify database credentials in environment variables
- Ensure the database and tables exist
- Check MySQL/MariaDB is running
- Try running Schema Sync in the Developer tab
### Missing columns after update ### Missing columns after update
- Go to Developer tab - Go to Developer tab
- Click "Check for Updates" - Click "Check for Updates"
- Apply any missing schema changes - Apply any missing schema changes
### Dark mode not working
- Ensure your browser/OS has dark mode enabled
- Try clearing browser cache
### AWS Route53 connection fails
- Verify your AWS Access Key ID and Secret Access Key are correct
- Ensure the IAM user has the required permissions (ListHostedZones, ListResourceRecordSets)
- Check that the hosted zone IDs are correct (format: Z followed by alphanumeric characters)
- Review error logs in the Developer tab for detailed error messages
### PTR records showing as MISSING
- PTR records are managed by your IP provider (e.g., IPXO, your ISP, or cloud provider)
- Contact your IP provider to set up reverse DNS for your IP addresses
- PTR lookups use the server's DNS resolver - results may vary based on DNS propagation
### PTR records showing as MISMATCH
- The PTR record exists but doesn't match the expected hostname
- Verify the PTR is set correctly with your IP provider
- Note: Trailing dots in hostnames are normalized during comparison
## License
MIT License - Feel free to use and modify as needed.
--- ---
Built with care by [Purple Computing](https://purplecomputing.com) **Copyright (c) 2025 Purple Computing Ltd. All rights reserved.**
Built by [Purple Computing](https://purplecomputing.com)