Add a web-based user interface to Subsurface that leverages the existing C++ core logic without reimplementing it. The web UI will be read-only initially and support both single-user (local) and multi-tenant (cloud server) deployment models.
- Reuse 80k+ LOC of existing C++/Qt core logic
- Avoid the scalability problems of the current HTML export (which generates hundreds of MB to GB of data)
- Transfer only the data needed for the current view to the browser (typically 1-5 PNGs plus a few KB of metadata)
- Support users with anywhere from <100 to 5000+ dives
- Work in both standalone and multi-tenant cloud deployment scenarios
┌─────────────────┐
│ Web Browser │
│ (User's) │
└────────┬────────┘
│ HTTPS
↓
┌─────────────────┐
│ Flask Web App │
│ (Python) │
└────────┬────────┘
│ subprocess/CLI
↓
┌─────────────────┐
│ subsurface-cli │
│ CLI Tool │
│ (C++/Qt) │
└────────┬────────┘
│
↓
┌─────────────────┐
│ Cloud Storage │
│ (Git Repo) │
└─────────────────┘
CLI Tool Approach (Option B from discussion):
- Stateless operations - no Qt event loop complications
- Clean separation between web layer (Python) and core logic (C++)
- Reuses existing C++ code (largely) without modification
- Simple to debug (can test CLI tool independently)
- Scales well for multi-tenant (process per request or process pooling)
- CLI tool may have other use cases beyond web UI
Trade-offs:
- Some overhead from process spawning (mitigated by keeping processes warm or using fast startup)
- Need to pass data via JSON serialization
Dives and trips are identified using their git storage path, which provides stable, human-readable identifiers that persist across sessions.
Subsurface stores dives in a git repository with the following hierarchy:
{year}/{month}/{trip-directory}/{dive-directory}/Dive-{number}
Example:
2026/01/09-Kona/11-Sun-09=51=53/Dive-731
│ │ │ │ │
│ │ │ │ └─ Dive file (dive number 731)
│ │ │ └─ Dive directory: day-weekday-time
│ │ └─ Trip directory: day-TripName (trip started on 9th)
│ └─ Month
└─ Year
Format details:
- Time uses
=as separator instead of:(Windows compatibility, per existing git storage) - Weekday is 3-letter English abbreviation (Sun, Mon, Tue, Wed, Thu, Fri, Sat)
- Trip names are sanitized (letters only, max 15 chars) with uniqueness suffix if needed
Trip ID: {year}/{month}/{day}-{TripName}
- Example:
2026/01/09-Kona - URL-encoded for use in URLs:
2026%2F01%2F09-Kona
Dive ID: {year}/{month}/{dive-directory} (the timestamp portion)
- Example:
2026/01/11-Sun-09=51=53 - This uniquely identifies a dive by its exact start time
- The dive number is metadata, not part of the identifier
/trip/2026/01/09-Kona # Trip detail page
/dive/2026/01/11-Sun-09=51=53 # Dive detail page
/api/trip/2026/01/09-Kona # Trip JSON API
/api/dive/2026/01/11-Sun-09=51=53 # Dive JSON API
/api/profile/2026/01/11-Sun-09=51=53/0 # Profile image (dc_index=0)
The CLI tool accepts dive and trip references using the --dive-ref and --trip-ref parameters:
subsurface-cli get-dive --dive-ref="2026/01/11-Sun-09=51=53"
subsurface-cli get-trip --trip-ref="2026/01/09-Kona"
subsurface-cli get-profile --dive-ref="2026/01/11-Sun-09=51=53" --dc-index=0The CLI tool resolves these path-based references to internal dive structures using the same parsing logic as load-git.cpp:
- Parse year, month, day from the path components
- Parse hour, minute, second from the time portion (after the weekday)
- Construct timestamp via
utc_mktime() - Look up dive by timestamp match
This approach ensures:
- Stability: IDs don't change unless the dive's start time changes
- Human readability: Users can understand what dive they're looking at from the URL
- Bookmarkability: URLs can be saved and shared
- Git alignment: Uses the same structure as the underlying storage
A C++ command-line tool that exposes Subsurface's core functionality via a JSON-based interface. Takes commands via arguments, returns results via JSON to stdout, with optional binary files (PNGs) written to temp locations.
The tool reads configuration from a file (not command-line args for security):
Config file location:
- Local use:
~/.config/subsurface-cli/config.json(or platform equivalent) - Multi-tenant: Per-request temporary file created by Flask, path specified via
--configflag
Config file format:
{
"repo_path": "/path/to/git/repo/clone",
"temp_dir": "/tmp/subsurface-cli",
"cache_ttl_seconds": 300,
"userid": "user@example.com"
}Field descriptions:
repo_path: Path to the user's git repository (required)temp_dir: Directory for temporary files like profile PNGs (default: system temp)cache_ttl_seconds: How long to cache git sync status (default: 300)userid: User identifier for logging purposes (optional, used in multi-tenant mode)
Purpose: Return overview statistics plus a paginated list of top-level items (dives or trips).
Usage:
subsurface-cli --config=/path/to/config.json list-dives \
--start=0 \
--count=50Arguments:
--start: Zero-based index into the top-level list (default: 0)--count: Number of top-level items to return (default: 50, max: 500)
Output (JSON to stdout):
{
"status": "success",
"total_dives": 1247,
"total_trips": 42,
"statistics": {
"total_dive_time_minutes": 89450,
"max_depth_meters": 87.3,
"avg_depth_meters": 23.4
},
"items": [
{
"type": "trip",
"trip_ref": "2024/06/15-Bonaire",
"description": "Bonaire 2024",
"date_start": "2024-06-15",
"date_end": "2024-06-22",
"location": "Bonaire",
"dive_count": 18
},
{
"type": "dive",
"dive_ref": "2024/06/10-Mon-14=30=00",
"dive_number": 1247,
"date": "2024-06-10",
"time": "14:30:00",
"location": "Blue Heron Bridge",
"duration_minutes": 67,
"max_depth_meters": 12.4,
"buddy": "Jane Smith",
"gases": ["Air"],
"sac_rate": 15.2,
"dc_count": 2
}
]
}Error handling:
{
"status": "error",
"error_code": "AUTH_FAILED",
"message": "Authentication token invalid or expired"
}Error codes:
AUTH_FAILED: Authentication/authorization failedREPO_NOT_FOUND: Git repository not found at specified pathINVALID_RANGE: Start/count parameters out of boundsSYNC_ERROR: Failed to sync with cloud storage
Purpose: Return detailed information about a specific trip including all its dives.
Usage:
subsurface-cli --config=/path/to/config.json get-trip \
--trip-ref="2024/06/15-Bonaire" \
--dive-start=0 \
--dive-count=20Arguments:
--trip-ref: Trip reference path (required), e.g., "2024/06/15-Bonaire"--dive-start: Zero-based index into the trip's dive list (default: 0)--dive-count: Number of dives to return (default: 100, max: 500)
Output (JSON to stdout):
{
"status": "success",
"trip": {
"trip_ref": "2024/06/15-Bonaire",
"description": "Bonaire 2024",
"date_start": "2024-06-15",
"date_end": "2024-06-22",
"location": "Bonaire",
"notes": "Amazing shore diving...",
"dive_count": 18
},
"dives": [
{
"dive_ref": "2024/06/15-Sat-10=30=00",
"dive_number": 1247,
"date": "2024-06-15",
"time": "10:30:00",
"location": "1000 Steps",
"duration_minutes": 67,
"max_depth_meters": 31.2,
"buddy": "Jane Smith",
"gases": ["EAN32"],
"sac_rate": 14.8,
"dc_count": 2
}
]
}Purpose: Return comprehensive information about a specific dive, including all metadata but NOT the profile images (those are requested separately).
Usage:
subsurface-cli --config=/path/to/config.json get-dive \
--dive-ref="2024/06/15-Sat-10=30=00"Arguments:
--dive-ref: Dive reference path (required), e.g., "2024/06/15-Sat-10=30=00"
Output (JSON to stdout):
{
"status": "success",
"dive": {
"dive_ref": "2024/06/15-Sat-10=30=00",
"dive_number": 1247,
"date": "2024-06-15",
"time": "10:30:00",
"duration_minutes": 67,
"max_depth_meters": 31.2,
"avg_depth_meters": 18.7,
"trip_ref": "2024/06/15-Bonaire",
"location": {
"name": "1000 Steps",
"gps": "12.1234,-68.2345"
},
"buddy": "Jane Smith",
"diveguide": "Local Guide",
"suit": "3mm wetsuit",
"tags": ["shore", "reef"],
"rating": 4,
"visibility": 4,
"air_temp_celsius": 28,
"water_temp_celsius": 27,
"gases": [
{
"name": "EAN32",
"o2_percent": 32,
"he_percent": 0
}
],
"cylinders": [
{
"description": "AL80",
"start_pressure_bar": 200,
"end_pressure_bar": 50,
"gas_name": "EAN32"
}
],
"weights": [
{
"description": "integrated",
"weight_kg": 4
}
],
"sac_rate": 14.8,
"notes": "Beautiful reef dive...",
"dive_computers": [
{
"dc_index": 0,
"model": "Shearwater Perdix",
"device_id": "12345678",
"sample_count": 2010
},
{
"dc_index": 1,
"model": "Suunto D5",
"device_id": "87654321",
"sample_count": 2015
}
]
}
}Purpose: Generate and return a profile image for a specific dive computer.
Usage:
subsurface-cli --config=/path/to/config.json get-profile \
--dive-ref="2024/06/15-Sat-10=30=00" \
--dc-index=0 \
--width=1024 \
--height=768Arguments:
--dive-ref: Dive reference path (required)--dc-index: Dive computer index (default: 0)--width: Image width in pixels (default: 1024, max: 4096)--height: Image height in pixels (default: 768, max: 4096)
Output (JSON to stdout):
{
"status": "success",
"profile": {
"dive_ref": "2024/06/15-Sat-10=30=00",
"dc_index": 0,
"dc_model": "Shearwater Perdix",
"width": 1024,
"height": 768,
"file_path": "/tmp/subsurface-cli/profile_20240615_103000_0_a3f2b9.png",
"file_size_bytes": 145234
}
}Notes:
- PNG file is written to temp directory specified in config
- Filename includes date, time, dc_index, and random hash to avoid collisions
- Flask app is responsible for serving the file and cleaning it up
Purpose: Return aggregate statistics across all dives.
Usage:
subsurface-cli --config=/path/to/config.json get-statsOutput (JSON to stdout):
{
"status": "success",
"statistics": {
"total_dives": 1247,
"total_dive_time_minutes": 89450,
"total_dive_time_human": "62 days 2 hours",
"max_depth_meters": 87.3,
"avg_depth_meters": 23.4,
"max_duration_minutes": 187,
"avg_duration_minutes": 71.7,
"date_first_dive": "2010-03-15",
"date_last_dive": "2024-12-20",
"total_trips": 42,
"countries_visited": 23,
"dive_sites_visited": 387
}
}Authentication is handled differently depending on deployment mode:
No authentication is required. The CLI tool trusts the config file, which points to the user's local git repository. Security relies on file system permissions.
Authentication is handled by the existing flask-auth-proxy application, which runs alongside this new Flask app. The flow is:
Internet (HTTPS)
↓
Apache :443 (SSL termination)
↓
flask-auth-proxy :5000 (handles login, validates credentials against MySQL)
↓
Apache proxies authenticated requests with X-Authenticated-User header
↓
New Flask WebUI App :5001
↓
subsurface-cli (invoked with per-user config)
↓
Git Repo at /var/www/git/{username}/
Key points:
- The existing
flask-auth-proxyhandles all login/logout and session management - Apache adds an
X-Authenticated-Userheader to authenticated requests - The new Flask app reads this header to identify the user
- The Flask app derives the git repo path:
/var/www/git/{username}/ - The Flask app creates a per-request config file for the CLI tool
- The CLI tool does NOT validate tokens - it trusts the config file
- Security is enforced by the fact that only the Flask app (running locally) can invoke the CLI
Flask app authentication handling:
from flask import request, g, abort
import tempfile
import json
@app.before_request
def authenticate():
# Read user identity from Apache proxy header
username = request.headers.get('X-Authenticated-User')
if not username:
abort(401, "Authentication required")
g.username = username
g.repo_path = f"/var/www/git/{username}/"
# Create per-request config for CLI tool
g.cli_config = create_cli_config(g.username, g.repo_path)
def create_cli_config(username: str, repo_path: str) -> str:
"""Create a temporary config file for this request."""
config = {
"repo_path": repo_path,
"temp_dir": "/tmp/subsurface-cli",
"cache_ttl_seconds": 300,
"userid": username
}
# Write to temp file (cleaned up after request)
fd, path = tempfile.mkstemp(suffix='.json', prefix='subsurface-cli-')
with os.fdopen(fd, 'w') as f:
json.dump(config, f)
return pathSecurity considerations:
- The CLI tool is only invoked by the Flask app, never directly by users
- The Flask app runs on localhost, not exposed to the internet
- Apache ensures all requests have valid authentication before proxying
- The
X-Authenticated-Userheader can only be set by Apache (stripped from client requests) - Each user can only access their own git repository at
/var/www/git/{username}/ - Path traversal is prevented by the Flask app constructing the repo path, not accepting it from users
The CLI tool handles syncing with cloud storage using a cached TTL strategy:
- On each invocation: Check if local repo needs sync based on last sync time
- Sync decision:
- Read last sync timestamp from cache file (
{repo_path}/.subsurface-cli-sync) - If last sync was within
cache_ttl_seconds(default: 300 = 5 minutes), skip sync - Otherwise, perform git pull from cloud storage
- Read last sync timestamp from cache file (
- Sync implementation:
- Use existing C++ cloud sync code from
git-access.cpp - Handles authentication, merge conflicts, etc.
- Use existing C++ cloud sync code from
- Error handling:
- If sync fails, return error to Flask app
- Do not serve stale data on sync failure
Manual refresh: The Flask app provides an /api/refresh endpoint that forces a sync regardless of TTL:
@app.route('/api/refresh', methods=['POST'])
def force_refresh():
"""Force a git sync, bypassing the cache TTL."""
# Delete the sync cache file to force refresh
sync_file = os.path.join(g.repo_path, '.subsurface-cli-sync')
if os.path.exists(sync_file):
os.unlink(sync_file)
# Call any CLI command to trigger sync
return g.cli.get_stats()Cache file format ({repo_path}/.subsurface-cli-sync):
{
"last_sync": "2024-01-15T10:30:00Z",
"last_commit": "a3f2b9c1d4e5f6..."
}Existing C++ code to leverage:
- Cloud storage sync/pull: Already exists, needs minimal wrapper
- PNG generation:
void exportProfile(ProfileScene &profile, const struct dive &dive, const QString &filename, bool diveinfo)is a static function in backend-shared/exportfuncs.cpp -- this may need some modification as it is print focused, but most of what we need is there - Data structures: Use existing dive/trip structures, just serialize to JSON
- Filtering: Exists but not needed for v1 (just use start/count pagination)
New C++ code needed:
- CLI argument parsing: Use Qt's QCommandLineParser or similar
- JSON serialization: Convert C++ dive/trip structures to JSON
- potentially use Qt's QJsonDocument/QJsonObject
- Pagination logic: Extract subset of dive list based on start/count
- Config file handling: Read JSON config file
- Token validation: Read and validate token file
- Main function: Route commands to appropriate handlers
Error handling:
- All errors output JSON with
status: "error" - Use appropriate exit codes (0 = success, non-zero = error)
- Log errors to stderr (Flask can capture and log these)
Provide the HTTP interface for the web UI. Handle user sessions, route requests, call the CLI tool, serve results, and manage temporary files.
- Python 3.10+
- Flask 3.x
- Flask-CORS (for API endpoints if needed)
- Gunicorn or similar WSGI server for production
Flask App Structure:
├── app.py # Main Flask application
├── config.py # Configuration management
├── auth.py # Authentication/session handling
├── subsurface_cli.py # Wrapper for calling CLI tool
├── routes/
│ ├── __init__.py
│ ├── dive_list.py # Route handlers for dive list
│ ├── dive_detail.py # Route handlers for dive details
│ └── api.py # JSON API endpoints
├── templates/
│ ├── base.html # Base template
│ ├── dive_list.html # Dive list view
│ └── dive_detail.html # Dive detail view
├── static/
│ ├── css/
│ ├── js/
│ └── images/
└── requirements.txt
GET / # Redirect to /dives
GET /dives # Dive list page (paginated)
GET /dives?start=0&count=50 # Dive list with pagination
GET /trip/<path:trip_ref> # Trip detail page
GET /dive/<path:dive_ref> # Dive detail page
# Examples:
# /trip/2024/06/15-Bonaire
# /dive/2024/06/15-Sat-10=30=00GET /api/dives # Get dive list (JSON)
GET /api/trip/<path:trip_ref> # Get trip details (JSON)
GET /api/dive/<path:dive_ref> # Get dive details (JSON)
GET /api/profile/<path:dive_ref>/<int:dc> # Get profile image
GET /api/stats # Get statistics (JSON)
POST /api/refresh # Force git sync
# Examples:
# /api/trip/2024/06/15-Bonaire
# /api/dive/2024/06/15-Sat-10=30=00
# /api/profile/2024/06/15-Sat-10=30=00/0Purpose: Encapsulate all CLI tool interactions in a single Python module.
import subprocess
import json
import tempfile
from pathlib import Path
from typing import Dict, Any, Optional
class SubsurfaceWebCLI:
def __init__(self, config_path: str, cli_binary: str = "subsurface-cli"):
self.config_path = config_path
self.cli_binary = cli_binary
def _run_command(self, command: str, args: Dict[str, Any]) -> Dict[str, Any]:
"""
Execute CLI command and return parsed JSON result.
Raises:
SubsurfaceCLIError: If command fails or returns error status
"""
cmd = [self.cli_binary, f"--config={self.config_path}", command]
# Add arguments
for key, value in args.items():
cmd.append(f"--{key}={value}")
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=30,
check=False
)
if result.returncode != 0:
raise SubsurfaceCLIError(
f"CLI command failed: {result.stderr}"
)
data = json.loads(result.stdout)
if data.get("status") == "error":
raise SubsurfaceCLIError(
data.get("message", "Unknown error"),
error_code=data.get("error_code")
)
return data
except subprocess.TimeoutExpired:
raise SubsurfaceCLIError("CLI command timed out")
except json.JSONDecodeError as e:
raise SubsurfaceCLIError(f"Invalid JSON from CLI: {e}")
def list_dives(self, start: int = 0, count: int = 50) -> Dict[str, Any]:
"""Get paginated dive list."""
return self._run_command("list-dives", {
"start": start,
"count": count
})
def get_trip(self, trip_ref: str, dive_start: int = 0,
dive_count: int = 100) -> Dict[str, Any]:
"""Get trip details with dive list."""
return self._run_command("get-trip", {
"trip-ref": trip_ref,
"dive-start": dive_start,
"dive-count": dive_count
})
def get_dive(self, dive_ref: str) -> Dict[str, Any]:
"""Get detailed dive information."""
return self._run_command("get-dive", {
"dive-ref": dive_ref
})
def get_profile(self, dive_ref: str, dc_index: int = 0,
width: int = 1024, height: int = 768) -> Dict[str, Any]:
"""Generate dive profile PNG."""
return self._run_command("get-profile", {
"dive-ref": dive_ref,
"dc-index": dc_index,
"width": width,
"height": height
})
def get_stats(self) -> Dict[str, Any]:
"""Get overall statistics."""
return self._run_command("get-stats", {})
class SubsurfaceCLIError(Exception):
"""Exception raised for CLI tool errors."""
def __init__(self, message: str, error_code: Optional[str] = None):
self.message = message
self.error_code = error_code
super().__init__(self.message)Two deployment modes:
- No authentication needed
- Config file points to user's local git repo, relying on file system permissions for control
- Flask runs on localhost only
- External authentication already handled by existing cloud web frontend
- User session includes:
- User ID
- Token for accessing their git repo
- Repo path
- Flask receives authenticated requests (via reverse proxy or similar)
- Session management:
from flask import session @app.before_request def ensure_authenticated(): if not session.get('user_id'): return redirect('/login') # Or return 401 for API # Create per-user CLI instance config = create_user_config( user_id=session['user_id'], token=session['token'], repo_path=session['repo_path'] ) g.cli = SubsurfaceWebCLI(config_path=config)
from flask import Flask, render_template, request, jsonify, g, send_file
from pathlib import Path
from subsurface_cli import SubsurfaceWebCLI, SubsurfaceCLIError
@app.route('/dives')
def dive_list():
"""Render dive list page."""
start = request.args.get('start', 0, type=int)
count = request.args.get('count', 50, type=int)
try:
data = g.cli.list_dives(start=start, count=count)
return render_template(
'dive_list.html',
items=data['items'],
total_dives=data['total_dives'],
total_trips=data['total_trips'],
statistics=data['statistics'],
start=start,
count=count
)
except SubsurfaceCLIError as e:
return render_template('error.html', error=str(e)), 500
@app.route('/dive/<path:dive_ref>')
def dive_detail(dive_ref: str):
"""Render dive detail page."""
try:
data = g.cli.get_dive(dive_ref=dive_ref)
return render_template(
'dive_detail.html',
dive=data['dive']
)
except SubsurfaceCLIError as e:
return render_template('error.html', error=str(e)), 500
@app.route('/api/profile/<path:dive_ref>/<int:dc_index>')
def api_profile(dive_ref: str, dc_index: int):
"""Return dive profile image."""
width = request.args.get('width', 1024, type=int)
height = request.args.get('height', 768, type=int)
try:
data = g.cli.get_profile(
dive_ref=dive_ref,
dc_index=dc_index,
width=width,
height=height
)
profile = data['profile']
file_path = profile['file_path']
# Serve file and schedule cleanup
response = send_file(
file_path,
mimetype='image/png',
as_attachment=False,
download_name=f'profile_{dc_index}.png'
)
# Clean up temp file after response is sent
@response.call_on_close
def cleanup():
try:
Path(file_path).unlink()
except Exception:
pass
return response
except SubsurfaceCLIError as e:
return jsonify({'error': str(e)}), 500Technology:
- Responsive HTML/CSS (Bootstrap 5 or similar)
- Minimal JavaScript (progressive enhancement)
- Mobile-first design
Key views:
-
Dive List View
- Collapsible trips
- Infinite scroll or pagination
- Quick stats at top
- Filter/search (future)
-
Dive Detail View
- Profile image(s) with DC switcher
- All metadata in organized sections
- Responsive layout (single column on mobile)
-
Trip Detail View
- Trip info at top
- List of dives in trip
Profile Image Handling:
- Request different sizes based on viewport:
- Mobile: 800x600
- Tablet: 1024x768
- Desktop: 1280x960 or 1920x1080
- Use responsive images:
<img srcset="..."> - Lazy loading for images below fold
- All repo paths validated against allowed base directory
- Token scoped to specific repo path
- CLI tool validates all file paths before access
- CLI tool only returns data for authenticated user's repo
- Token validation ensures user can't access other users' data
- No direct file system access from web layer
- All user inputs sanitized (dive_id, dc_index, pagination params)
- Size limits on image dimensions
- Rate limiting on API endpoints (future)
- Short-lived tokens (1 hour TTL)
- Stored in secure location with restrictive permissions
- Refreshed automatically by Flask app
- Never exposed in URLs or logs
Setup:
- User installs
subsurface-cliCLI tool - User installs Flask app (or uses pre-packaged version)
- User runs:
flask run --host=127.0.0.1 --port=5000 - User opens browser to
http://localhost:5000
Config:
- Single config file pointing to user's local git repo
- No authentication needed
- Flask runs in development mode
Setup:
- Cloud server has
subsurface-cliCLI tool installed - Flask app deployed with Gunicorn + Nginx
- Existing authentication layer (current cloud web frontend) handles auth
- For each authenticated user:
- Create/sync their git repo clone
- Generate time-limited token
- Create per-user config file
- Pass user to Flask app via session
Config:
- Per-user config files in
/var/subsurface/users/<user_id>/config.json - Token files co-located with repos
- Flask runs behind Apache2 reverse proxy
- HTTPS enforced
Architecture:
Internet
↓
Apache2 (SSL termination, reverse proxy) -- this exists and also handles git traffic via https:// and cloud storage user management interface
↓
Existing Cloud Frontend (authentication)
↓
Flask App (multiple workers)
↓
subsurface-cli CLI (one or more processes)
↓
Git Repos (/var/www/git/<user_id>/)
- Set up CLI argument parsing and config file reading
- Implement token validation and repo path security
- Implement
list-divescommand with JSON output - Implement
get-divecommand - Implement
get-profilecommand (wrap existing PNG generation) - Implement
get-tripcommand - Implement
get-statscommand - Add error handling and logging
- Write unit tests for each command
- Set up Flask project structure
- Implement
subsurface_cli.pywrapper module - Implement basic routes (dive list, dive detail)
- Create HTML templates with responsive design
- Implement API routes
- Add session management for multi-tenant mode
- Add error handling and logging
- Test with CLI tool
- End-to-end testing with real dive data
- Performance testing with large dive sets (5000+ dives)
- Security testing (path traversal, token validation)
- Multi-tenant testing
- Browser compatibility testing
- Mobile responsiveness testing
- Create installation documentation
- Package for single-user deployment
- Deploy to cloud server for multi-tenant
- Monitor and tune performance
- Write operations (edit dives, add notes, etc.)
- Advanced filtering and search
- Export functionality (PDF, CSV)
- Social features (share dives, compare with buddies)
- Real-time sync (WebSocket updates when data changes)
- Progressive Web App (offline support)
- Performance optimizations (process pooling, caching)
- API rate limiting and quotas
- User-configurable themes
- Dive computer direct downloads
- Photos/media display and serving
- Screen-optimized profile rendering (different colors/styling than print)
- Structured file logging with rotation
- ✅ CLI tool binary name:
subsurface-cli - ✅ Config file location:
- Local: Platform-standard location (
~/.config/subsurface-cli/config.jsonon Linux) - Multi-tenant: Per-request temporary file created by Flask app
- Local: Platform-standard location (
- ✅ Temp file cleanup strategy: Immediate after serving (using Flask's
@response.call_on_close) - ✅ JSON library choice: Qt's QJsonDocument (already a dependency, no new libs needed)
- ✅ Token generation: Not needed - authentication handled by existing flask-auth-proxy
- ✅ Git sync frequency: Cached with TTL (default 5 minutes) + manual refresh endpoint
- ✅ Dive/Trip identification: Git path-based references (e.g.,
2024/06/15-Sat-10=30=00) - ✅ Authentication approach: X-Authenticated-User header from Apache/flask-auth-proxy
- ✅ Flask app packaging: Systemd service (matches flask-auth-proxy pattern)
- ✅ Flask app location: In Subsurface repo as
webui/directory - ✅ Profile rendering: Use current print rendering for v1; review screen-optimized styling for v2
- ✅ Photos/media: Defer to v2 (no photo references in v1)
- ✅ Error logging: stderr only (Flask captures); structured file logging can be added in v2
- ✅ Web UI can display dive list for users with 5000+ dives without performance issues
- ✅ Profile images load quickly (<2 seconds on typical connection)
- ✅ Data transfer per page view is <10 MB (vs. hundreds of MB with current HTML export)
- ✅ CLI tool reuses existing C++ code without major refactoring
- ✅ Multi-tenant deployment works with isolated user data
- ✅ Security: no path traversal, data exfiltration, or unauthorized access possible
- ✅ Mobile-friendly responsive design works on phones, tablets, desktops
- ✅ Single-user deployment is simple (one command to run)
User views dive list:
- User browses to
https://cloud.subsurface-divelog.org/webui/dives - Apache validates auth via flask-auth-proxy, adds
X-Authenticated-User: user@example.comheader - Apache proxies to Flask WebUI app
- Flask reads header, creates per-request config file with repo path
/var/www/git/user@example.com/ - Flask calls:
g.cli.list_dives(start=0, count=50) - CLI wrapper executes:
subsurface-cli --config=/tmp/subsurface-cli-xxx.json list-dives --start=0 --count=50 - CLI tool:
- Reads config file
- Checks sync cache file for last sync time
- If cache expired, syncs git repo from cloud storage
- Loads dive data from local git repo
- Extracts items 0-49 from top-level list
- Serializes to JSON with
dive_refandtrip_refidentifiers - Outputs to stdout
- Flask wrapper parses JSON
- Flask renders HTML template with data
- User sees dive list in browser
User clicks on a dive:
- User clicks dive "Bonaire - 1000 Steps" from June 15, 2024
- Browser navigates to
/dive/2024/06/15-Sat-10=30=00 - Flask extracts
dive_reffrom URL path - Flask calls:
g.cli.get_dive(dive_ref="2024/06/15-Sat-10=30=00") - CLI tool parses dive_ref, looks up dive by timestamp, returns dive metadata JSON
- Flask renders dive detail template
- Template includes:
<img src="/api/profile/2024/06/15-Sat-10=30=00/0?width=1024&height=768"> - Browser requests profile image
- Flask calls:
g.cli.get_profile(dive_ref="2024/06/15-Sat-10=30=00", dc_index=0, width=1024, height=768) - CLI tool generates PNG, returns file path in JSON
- Flask serves PNG file to browser
- Flask cleans up temp file after response sent
User switches to dive computer #2:
- JavaScript in page changes image src to
/api/profile/2024/06/15-Sat-10=30=00/1?width=1024&height=768 - Browser requests new profile (steps 9-12 above)
- New profile displays
Key files to create:
C++ CLI Tool (in cli/ directory):
cli/main.cpp- Entry point, command routingcli/commands.cpp- Command implementations (list-dives, get-dive, get-trip, get-profile, get-stats)cli/json_serializer.cpp- Convert C++ dive/trip structures to JSONcli/config.cpp- Config file handlingcli/dive_ref.cpp- Parse dive_ref/trip_ref strings to internal structurescli/CMakeLists.txt- Build configuration
Python Flask App (in webui/ directory):
webui/app.py- Main Flask applicationwebui/subsurface_cli.py- CLI wrapper modulewebui/routes.py- Route handlerswebui/templates/base.html- Base templatewebui/templates/dive_list.html- Dive list viewwebui/templates/dive_detail.html- Dive detail viewwebui/templates/trip_detail.html- Trip detail viewwebui/static/css/style.css- Responsive CSSwebui/static/js/app.js- Minimal JavaScript (DC switcher, lazy loading)webui/requirements.txt- Python dependencieswebui/config.py- Flask configuration
Dependencies:
C++:
- Qt 6 (QCore, QJson, etc.) - already a dependency; do not bother with supporting Qt 5
- Existing Subsurface core libraries (core/, backend-shared/)
Python:
- Flask >= 3.0
- gunicorn (for production)
- python >= 3.10
Build/Run Commands:
C++ CLI Tool:
cd subsurface
mkdir build && cd build
cmake .. -DSUBSURFACE_TARGET_EXECUTABLE=CliToolExecutable
make subsurface-cli
./subsurface-cli --helpFlask App (development):
cd subsurface/webui
pip install -r requirements.txt
flask run --port 5001Flask App (production):
cd subsurface/webui
gunicorn -w 4 -b 127.0.0.1:5001 app:appTesting Strategy:
-
Unit Testing:
- Use a framework like
unittestorpytestfor the Flask application. - Ensure all core functionalities, such as token management, user authentication, and API endpoints, are covered.
- For the CLI tool, use a C++ testing framework like
Catch2orGoogle Testto validate individual commands and error handling.
- Use a framework like
-
Integration Testing:
- Test the interaction between the Flask app and the CLI tool.
- Simulate real-world scenarios, such as token generation and validation workflows.
- Use tools like
Postmanorpytestwithrequeststo test API endpoints.
-
End-to-End Testing:
- Automate end-to-end tests using tools like
SeleniumorPlaywrightfor the frontend. - Simulate user workflows, such as logging in, uploading dive logs, and viewing profiles.
- Ensure compatibility across major browsers (Chrome, Firefox, Safari, Edge).
- Automate end-to-end tests using tools like
-
Load Testing:
- Use tools like
Apache JMeterorLocustto simulate high traffic and measure performance. - Focus on critical endpoints, such as token generation, data synchronization, and user authentication.
- Identify bottlenecks and optimize the application for scalability.
- Use tools like
-
Security Testing:
- Perform penetration testing to identify vulnerabilities in the Flask app and CLI tool.
- Use tools like
OWASP ZAPorBurp Suiteto test for common security issues, such as SQL injection and XSS. - Ensure secure handling of sensitive data, such as tokens and user credentials.
-
Regression Testing:
- Maintain a suite of regression tests to ensure new changes do not break existing functionality.
- Automate regression tests using CI/CD pipelines.
-
Test Coverage:
- Aim for at least 80% code coverage for both the Flask app and CLI tool.
- Use tools like
coverage.pyfor Python andgcovfor C++ to measure coverage.
-
Implementation Notes:
- Set up a dedicated testing environment with mock data for integration and end-to-end tests.
- Use Docker containers to ensure consistent testing environments across different platforms.
- Document all test cases and scenarios for future reference.
This comprehensive testing strategy ensures the reliability, security, and scalability of the Subsurface Web UI and CLI tool.
Overview
A robust error logging strategy is essential for diagnosing issues and ensuring the reliability of both the CLI tool and the Flask application. This section outlines the logging mechanisms, storage locations, rotation policies, and monitoring tools for the Subsurface Web UI.
-
Log Levels:
- The CLI tool will support multiple log levels:
INFO,WARNING,ERROR, andDEBUG. - By default, only
WARNINGandERRORmessages will be logged in production.
- The CLI tool will support multiple log levels:
-
Log Storage:
- Local User Scenario:
- Logs will be stored in a platform-appropriate location:
- Linux:
~/.local/share/subsurface-cli/logs/cli.log - macOS:
~/Library/Logs/SubsurfaceCLI/cli.log - Windows:
%LOCALAPPDATA%\SubsurfaceCLI\logs\cli.log
- Linux:
- This ensures logs are stored in a standard location for each platform.
- Logs will be stored in a platform-appropriate location:
- Multi-Tenant Scenario:
- Logs will be written to a centralized log file on the server, e.g.,
/var/log/subsurface/cli.log. - Each log entry will include a tag identifying the
userid(email address) of the user for whom the CLI was invoked.
- Logs will be written to a centralized log file on the server, e.g.,
- Local User Scenario:
-
Log Rotation:
- Use the standard
logrotatetool for managing log rotation. - Retain logs for 30 days, compressing older logs to save space.
- Use the standard
-
Log Format:
- Each log entry will include a timestamp, log level, and message.
- For multi-tenant scenarios, the
useridwill also be included. - Example:
[2026-01-23 14:30:00] [ERROR] [User: user@example.com] Failed to validate token: Token expired
-
Error Reporting:
- Critical errors (e.g., token validation failures, file access issues) will be reported to
stderrin addition to being logged.
- Critical errors (e.g., token validation failures, file access issues) will be reported to
-
Implementation Notes:
- Use a logging library like
boost::logto handle log formatting and writing. - Ensure the log directory is created if it does not exist.
- Use a logging library like
Updated Implementation Plan:
-
Local User Scenario:
- Update the CLI tool to detect the platform and write logs to the appropriate directory.
- Provide a configuration option to override the default log path if needed.
-
Multi-Tenant Scenario:
- Add a
useridfield to the log context for tagging log entries. - Ensure the CLI tool writes logs to
/var/log/subsurface/cli.logon the server. - Configure
logrotateto manage log rotation and retention.
- Add a
This updated strategy ensures that logs are stored in appropriate locations for both local and multi-tenant scenarios, with clear tagging and rotation policies.