Add NextSnap PWA with photo gallery viewer and continuous capture

Offline-first photo capture app for Nextcloud with:
- Camera capture with continuous mode (auto-reopens after each photo)
- File browser with fullscreen image gallery, swipe navigation, and rename
- Upload queue with background sync engine
- Admin panel for Nextcloud user management
- Service worker for offline-first caching (v13)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-07 04:53:13 -06:00
commit cad4118f72
55 changed files with 9038 additions and 0 deletions

421
nextsnap.md Normal file
View File

@@ -0,0 +1,421 @@
# CLAUDE.md — NextSnap: Offline-First Photo Capture for Nextcloud
## Project Overview
NextSnap is a mobile-optimized, offline-first web application for capturing photos on smartphones and syncing them to a user's Nextcloud instance. It is designed for field use in environments with poor or no connectivity. Photos are cached locally in the browser and only removed from cache after confirmed upload to the Nextcloud server. The app supports multiple users, each authenticated against Nextcloud credentials, and provides an admin panel for managing Nextcloud user accounts.
---
## Core Requirements
### 1. Offline-First Architecture
- The app MUST function fully offline for photo capture and local caching.
- Use a **Service Worker** to cache the application shell and all static assets for offline use.
- Use **IndexedDB** (via a library like Dexie.js) to store captured photos as blobs with metadata until upload is confirmed.
- A photo is NEVER removed from IndexedDB until the app has verified (via HTTP response or a follow-up check) that the file exists on the Nextcloud server.
- Implement automatic background sync: when connectivity is detected, the app should begin uploading queued photos without user intervention.
- Provide a manual "Sync Now" button for user-triggered uploads.
- Display a clear connectivity indicator (online/offline/syncing) in the UI at all times.
- Display a count of photos pending upload.
### 2. Photo Capture
- Use the device's native camera via `<input type="file" accept="image/*" capture="environment">` for maximum compatibility across iOS Safari and Android Chrome.
- Do NOT attempt to use `getUserMedia` or a custom camera viewfinder — use the native camera intent for reliability.
- Accept full-resolution images from the device camera.
- Convert all captured images to **JPEG format** on the client side before storage/upload. Use a `<canvas>` element to read the image and export as `image/jpeg` at quality 0.92.
- Preserve EXIF orientation where possible; if the canvas strips EXIF, detect and correct rotation before export.
- File naming convention: `{username}_{timestamp}.jpg` where timestamp is `YYYYMMDD_HHmmss` in local time (e.g., `waldo_20260206_143022.jpg`).
### 3. Nextcloud Integration
- All file operations use the **Nextcloud WebDAV API** (`/remote.php/dav/files/{username}/`).
- Authentication: Use Nextcloud username/password credentials. The backend proxies all WebDAV requests to avoid CORS issues and to keep credentials secure.
- **Directory Browsing**: Users can browse their full Nextcloud directory tree to select an upload destination folder. Use `PROPFIND` requests via the backend proxy to list directories.
- **Upload**: Use `PUT` requests via the backend proxy to upload JPEG files to the selected directory.
- **Upload Verification**: After each upload, perform a `PROPFIND` or `HEAD` request on the uploaded file to confirm it exists on the server before removing it from IndexedDB.
- Allow users to create new directories within their Nextcloud file tree from the app.
### 4. User Authentication & Management
- Users log in with their Nextcloud credentials (username + password).
- The backend validates credentials against the Nextcloud instance using a WebDAV or OCS API call.
- User sessions are managed via server-side sessions (Flask session with a secret key) or JWT tokens stored in the browser.
- **Admin Panel**: Accessible only to users with Nextcloud admin privileges.
- List existing Nextcloud users (via OCS Provisioning API: `GET /ocs/v1.php/cloud/users`).
- Create new Nextcloud users (via OCS Provisioning API: `POST /ocs/v1.php/cloud/users`).
- Disable/enable Nextcloud users.
- Admin status is determined by checking Nextcloud group membership (the `admin` group).
### 5. Multi-User Support
- Each user has their own session, upload queue, and selected destination directory.
- IndexedDB stores are keyed/partitioned by username to prevent data leakage between users on shared devices.
- Logging out clears the session but does NOT delete pending uploads from IndexedDB (to prevent data loss). On next login by the same user, pending uploads resume.
### 6. Batch Photo Review & Rename
- Users can enter a full-screen **photo review mode** to swipe through photos in a folder and rename them individually before or after upload.
- **Entry points**: From the Queue screen (for local pending photos) or from the File Browser (for photos already uploaded to a Nextcloud folder).
- **Review UI**: Full-screen image viewer optimized for mobile. Displays one photo at a time with the current filename shown in an editable text field overlaid at the bottom.
- **Navigation**: Horizontal swipe gestures (touch swipe on mobile) to move between photos. Also provide left/right arrow buttons for accessibility. Display a position indicator (e.g., "3 / 17").
- **Renaming**:
- Tap the filename field to edit it. The `.jpg` extension is shown but not editable to prevent accidental removal.
- For **local photos** (pending in IndexedDB): Update the `filename` field in IndexedDB immediately on change. If the photo has not yet been uploaded, the new name is used for upload.
- For **remote photos** (already on Nextcloud): Issue a WebDAV `MOVE` request via the backend proxy to rename the file on the server. Show a spinner during the rename and confirm success/failure with a toast.
- Validate filenames: disallow empty names, `/`, `\`, and other characters invalid in Nextcloud paths. Auto-trim whitespace.
- **Batch workflow**: The user can rapidly swipe through dozens of photos, renaming each as needed, without leaving the review screen. Changes are saved per-photo as the user swipes away (auto-save on swipe/navigate).
- **Offline behavior**: Renaming local/pending photos works fully offline. Renaming remote photos requires connectivity — if offline, show a message that remote renames require a connection, but still allow browsing/viewing.
---
## Tech Stack
| Layer | Technology |
|-------|------------|
| Backend | Python 3.11+ / Flask |
| WSGI Server | Gunicorn |
| Frontend | Vanilla JavaScript (ES6+), no framework |
| CSS | Minimal custom CSS, mobile-first responsive design |
| Offline Storage | IndexedDB via Dexie.js |
| Offline Shell | Service Worker (Workbox or hand-rolled) |
| Image Processing | Client-side Canvas API for JPEG conversion |
| Nextcloud API | WebDAV (file ops), OCS Provisioning API (user management) |
| Deployment | Docker / Docker Compose |
---
## Project Structure
```
nextsnap/
├── CLAUDE.md
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── config.py # App configuration (Nextcloud URL, secret key, etc.)
├── app/
│ ├── __init__.py # Flask app factory
│ ├── routes/
│ │ ├── __init__.py
│ │ ├── auth.py # Login/logout endpoints
│ │ ├── webdav_proxy.py # Proxy endpoints for Nextcloud WebDAV
│ │ ├── admin.py # Admin panel API endpoints
│ │ └── health.py # Health check endpoint
│ ├── services/
│ │ ├── __init__.py
│ │ ├── nextcloud.py # Nextcloud WebDAV and OCS API client
│ │ └── auth.py # Credential validation, session management
│ ├── static/
│ │ ├── manifest.json # PWA manifest
│ │ ├── sw.js # Service worker
│ │ ├── js/
│ │ │ ├── app.js # Main application logic
│ │ │ ├── camera.js # Photo capture and JPEG conversion
│ │ │ ├── storage.js # IndexedDB / Dexie.js wrapper
│ │ │ ├── sync.js # Upload queue and sync engine
│ │ │ ├── reviewer.js # Batch photo review & rename swiper
│ │ │ ├── filebrowser.js # Nextcloud directory browser
│ │ │ └── admin.js # Admin panel logic
│ │ ├── css/
│ │ │ └── style.css # Mobile-first styles
│ │ ├── icons/ # PWA icons (192x192, 512x512)
│ │ └── lib/
│ │ └── dexie.min.js # Dexie.js (vendored for offline use)
│ └── templates/
│ ├── base.html # Base template with nav and connectivity indicator
│ ├── login.html # Login page
│ ├── capture.html # Main camera/capture page
│ ├── queue.html # Upload queue / pending photos view
│ ├── reviewer.html # Full-screen batch photo review & rename
│ ├── browser.html # Nextcloud file browser for selecting destination
│ └── admin.html # Admin panel for user management
└── tests/
├── test_auth.py
├── test_webdav_proxy.py
└── test_admin.py
```
---
## Detailed Implementation Guide
### Backend (Flask)
#### `config.py`
```python
import os
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY', 'change-me-in-production')
NEXTCLOUD_URL = os.environ.get('NEXTCLOUD_URL', 'https://cloud.example.com')
SESSION_TYPE = 'filesystem'
MAX_CONTENT_LENGTH = 50 * 1024 * 1024 # 50MB max upload
```
#### `app/routes/auth.py`
- `POST /api/auth/login` — Accepts `{username, password}`. Validates against Nextcloud by attempting a `PROPFIND` on the user's WebDAV root. On success, stores `username` and `password` (encrypted or in server session) and returns user info including admin status.
- `POST /api/auth/logout` — Clears the session.
- `GET /api/auth/status` — Returns current auth state and admin flag.
#### `app/routes/webdav_proxy.py`
- `GET /api/files/list?path=/` — Proxies a `PROPFIND` (Depth: 1) to Nextcloud WebDAV. Returns JSON list of files/directories with name, type, size, modified date.
- `PUT /api/files/upload` — Accepts multipart file upload + `path` parameter. Proxies the file to Nextcloud via `PUT`. Returns success with the file's WebDAV href.
- `HEAD /api/files/verify?path=/path/to/file.jpg` — Proxies a `HEAD` or `PROPFIND` (Depth: 0) to check file existence. Returns 200 if exists, 404 if not.
- `MKCOL /api/files/mkdir` — Creates a new directory via WebDAV `MKCOL`.
- `POST /api/files/rename` — Accepts `{sourcePath, destPath}`. Proxies a WebDAV `MOVE` request to rename/move a file on Nextcloud. Used by the batch rename feature. Returns 201 on success.
- `GET /api/files/thumbnail?path=/path/to/file.jpg&size=256` — Returns a downsized JPEG thumbnail of a remote file for use in the photo review swiper. Fetches the full image from Nextcloud and resizes server-side using Pillow. Cache thumbnails briefly in memory or on disk to avoid repeated fetches during a swipe session.
#### `app/routes/admin.py`
- All admin endpoints check that the logged-in user is in the Nextcloud `admin` group.
- `GET /api/admin/users` — Lists Nextcloud users via OCS API.
- `POST /api/admin/users` — Creates a new Nextcloud user. Accepts `{username, password, email?, displayName?, groups?}`.
- `PUT /api/admin/users/<username>/enable` — Enables a user.
- `PUT /api/admin/users/<username>/disable` — Disables a user.
#### `app/services/nextcloud.py`
- Wraps all Nextcloud API calls using the `requests` library.
- Methods: `propfind(path, depth)`, `put_file(path, data)`, `head(path)`, `mkcol(path)`, `move(source, dest)`, `get_file(path)`, `ocs_get_users()`, `ocs_create_user(data)`, `ocs_enable_user(username)`, `ocs_disable_user(username)`, `check_admin(username)`.
- All methods accept user credentials as parameters (pulled from the session by the route handlers).
- Handle and translate HTTP errors from Nextcloud into meaningful JSON error responses.
### Frontend
#### Service Worker (`sw.js`)
- On install, precache all static assets: HTML templates, CSS, JS files, vendored libraries, icons.
- Use a cache-first strategy for static assets.
- Use a network-first strategy for API calls, falling back to a "you're offline" JSON response.
- The service worker should NOT cache API responses that contain file data — only the app shell.
#### IndexedDB Schema (`storage.js`)
```javascript
const db = new Dexie('nextsnap');
db.version(1).stores({
photos: '++id, username, timestamp, filename, targetPath, status',
// status: 'pending' | 'uploading' | 'uploaded' | 'verified'
settings: 'username'
// stores: { username, targetPath (last selected directory) }
});
```
- `photos` table stores: `id`, `username`, `timestamp`, `filename`, `targetPath`, `status`, `blob` (the JPEG image data), `retryCount`, `lastError`.
- `settings` table stores per-user preferences like last selected upload directory.
#### Camera Module (`camera.js`)
- Trigger native camera via a hidden `<input type="file" accept="image/*" capture="environment">`.
- On file selection:
1. Read the file as an `Image` object.
2. Detect EXIF orientation and apply rotation correction on a `<canvas>`.
3. Export canvas as JPEG blob (`canvas.toBlob(callback, 'image/jpeg', 0.92)`).
4. Generate filename: `{username}_{YYYYMMDD_HHmmss}.jpg`.
5. Store the blob + metadata in IndexedDB with status `pending`.
6. Show a brief thumbnail preview/confirmation toast.
7. If online, trigger the sync engine.
#### Sync Engine (`sync.js`)
- On initialization and on `online` event, query IndexedDB for all photos with status `pending` or `uploading` (retry stalled uploads).
- Upload process for each photo:
1. Set status to `uploading`.
2. `POST` the JPEG blob to `/api/files/upload` with the target path.
3. On success, set status to `uploaded`.
4. Verify: `HEAD /api/files/verify?path={targetPath}/{filename}`.
5. On verification success, set status to `verified`, then **delete the blob from IndexedDB**.
6. On any failure, increment `retryCount`, set `lastError`, revert status to `pending`, and schedule retry with exponential backoff (5s, 15s, 45s, 2min, 5min, cap at 5min).
- Process uploads sequentially (one at a time) to avoid overwhelming limited bandwidth.
- Expose observable state for the UI: `{ pendingCount, currentUpload, isOnline, isSyncing }`.
#### File Browser (`filebrowser.js`)
- Fetches directory listing from `/api/files/list?path={currentPath}`.
- Renders a simple list/tree of directories with tap-to-navigate.
- When browsing a folder containing images, show a **"Review Photos"** button that opens the photo reviewer for all JPEG files in that directory.
- Shows a "Select This Folder" button to set the upload destination.
- Shows a "New Folder" button with a name prompt to create directories.
- Caches the last-used directory path in IndexedDB settings per user.
#### Photo Reviewer (`reviewer.js`)
- A full-screen swipeable photo viewer for batch reviewing and renaming photos.
- **Two modes**:
- **Local mode**: Reviews pending photos from IndexedDB (entered from Queue screen). Loads blobs directly from IndexedDB — fully offline capable.
- **Remote mode**: Reviews photos in a Nextcloud folder (entered from File Browser). Loads thumbnails via `/api/files/thumbnail` and full images on demand via `/api/files/list` + direct fetch.
- **Swipe navigation**: Implement touch-based horizontal swipe detection. Track `touchstart`, `touchmove`, `touchend` events. A swipe threshold of 50px triggers navigation. Also support left/right arrow tap areas on screen edges and keyboard arrow keys.
- **Preloading**: Preload the next and previous image while viewing the current one to ensure smooth swiping. In remote mode, preload thumbnails for ±3 images.
- **Filename editor**:
- Displayed as an editable text input overlaid on the bottom of the photo.
- Shows filename without `.jpg` extension during editing; extension is appended automatically on save.
- Auto-saves on: swipe to next/previous photo, tap outside the input, or pressing Enter.
- For local photos: updates IndexedDB `filename` field synchronously.
- For remote photos: calls `POST /api/files/rename` with old and new paths. Shows a brief spinner overlay during the request. On failure, reverts the displayed name and shows an error toast.
- **Position indicator**: Shows "3 / 17" style counter at the top of the screen.
- **Exit**: "Done" button or swipe-down gesture returns to the originating screen (Queue or File Browser).
- **Filename validation**: Reject empty names, names containing `/`, `\`, `?`, `*`, `"`, `<`, `>`, `|`, or `:`. Show inline validation error. Prevent save until corrected.
#### Admin Panel (`admin.js`)
- Only rendered/accessible if the user has admin status.
- Lists users in a simple table with username, display name, email, enabled status.
- "Add User" form: username (required), password (required), email (optional), display name (optional).
- Enable/disable toggle per user.
### UI/UX Requirements
#### Layout
- Single-column, mobile-first layout. Maximum width 480px centered on larger screens.
- Bottom navigation bar with 3-4 tabs: **Capture**, **Queue**, **Files**, **Admin** (admin only).
- Top bar: app name, connectivity indicator (green dot = online, red dot = offline, spinning = syncing), pending upload count badge.
#### Capture Screen
- Large, prominent "Take Photo" button (full-width, tall, easy to tap with gloves or in a hurry).
- Below the button: currently selected upload folder path with a "Change" link to the file browser.
- Below that: thumbnail strip of the last 5 captured photos (from IndexedDB) with status indicators.
#### Queue Screen
- List of all pending/uploading photos with thumbnails, filenames, status, and retry count.
- "Sync Now" button at the top.
- **"Review & Rename" button** — opens the photo reviewer in local mode for all pending photos, allowing the user to swipe through and rename before upload.
- Swipe-to-delete or delete button per photo (with confirmation dialog).
- "Clear Verified" button to clean up any verified entries still showing.
#### File Browser Screen
- Breadcrumb path at top.
- Directory listing with folder icons, tap to navigate.
- When a folder contains images, show a **"Review Photos"** button to enter the swipe reviewer for that folder's images.
- "Select This Folder" fixed button at bottom.
- "New Folder" button.
#### Photo Reviewer Screen
- Full-screen image display with dark/black background for focus.
- Editable filename field pinned to the bottom of the screen with a semi-transparent background bar.
- Position counter ("3 / 17") at the top center.
- Left/right tap zones on screen edges as swipe alternatives.
- "Done" button (top-left corner) to exit back to the originating screen.
- Brief spinner overlay when a remote rename is in progress.
- Toast notifications for rename success/failure.
#### Admin Screen
- User list table.
- "Add User" form.
- Enable/disable toggles.
#### Styling
- High contrast, large touch targets (minimum 48x48px).
- System font stack for fast rendering.
- Dark mode support via `prefers-color-scheme` media query.
- Status colors: green (verified/online), yellow (uploading/syncing), red (error/offline), gray (pending).
---
## PWA Configuration
### `manifest.json`
```json
{
"name": "NextSnap",
"short_name": "NextSnap",
"description": "Offline-first photo capture for Nextcloud",
"start_url": "/",
"display": "standalone",
"orientation": "portrait",
"background_color": "#1a1a2e",
"theme_color": "#16213e",
"icons": [
{ "src": "/static/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/static/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}
```
### iOS Compatibility
- Add `<meta name="apple-mobile-web-app-capable" content="yes">` and related meta tags.
- Note: iOS Safari has limited Service Worker and IndexedDB persistence. The app should warn iOS users to open the app regularly to prevent cache eviction, and should never rely solely on client-side storage for long-term data — the upload-and-verify cycle is critical.
---
## Docker Deployment
### `Dockerfile`
```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "app:create_app()"]
```
### `docker-compose.yml`
```yaml
version: '3.8'
services:
nextsnap:
build: .
ports:
- "8000:8000"
environment:
- SECRET_KEY=your-secret-key-here
- NEXTCLOUD_URL=https://your-nextcloud-instance.com
volumes:
- flask_sessions:/app/flask_session
restart: unless-stopped
volumes:
flask_sessions:
```
### `requirements.txt`
```
Flask==3.0.*
gunicorn==21.*
requests==2.31.*
Flask-Session==0.5.*
Pillow==10.*
```
---
## Security Considerations
- **Never store Nextcloud passwords in IndexedDB or localStorage.** Credentials are held only in the server-side session.
- Use HTTPS in production (enforce via reverse proxy like Nginx/Caddy).
- Set `SameSite=Strict` and `HttpOnly` on session cookies.
- Rate-limit login attempts to prevent brute force.
- The backend proxy ensures Nextcloud credentials are never exposed to the browser's JavaScript.
- Validate and sanitize all file paths to prevent directory traversal attacks.
- Admin endpoints must verify admin group membership on every request (do not cache admin status client-side as the sole check).
---
## Error Handling & Resilience
- **Network errors during upload**: Catch all fetch/XHR errors. Revert photo status to `pending`. Log the error in the photo's `lastError` field. Schedule retry with exponential backoff.
- **Partial uploads**: If the upload appears to succeed but verification fails, keep the photo in the queue and retry the full upload.
- **IndexedDB quota**: Monitor storage usage. If approaching quota, warn the user and prioritize uploading. Never delete unverified photos to free space.
- **Session expiry**: If a 401 is received from the backend, redirect to login. Do NOT clear the upload queue.
- **Nextcloud downtime**: Queue continues to accumulate. When Nextcloud returns, uploads resume automatically.
---
## Testing Priorities
1. **Offline capture and cache**: Disconnect network, take photos, verify they're stored in IndexedDB.
2. **Upload and verification cycle**: Reconnect, verify photos upload and are removed from queue only after server-side verification.
3. **Multi-user isolation**: Log in as two different users on the same device, verify queues are separate.
4. **Admin operations**: Create user, verify the new user can log in and use the app.
5. **iOS Safari and Android Chrome**: Test on real devices — emulators don't accurately represent camera input and Service Worker behavior.
6. **Slow/intermittent connectivity**: Use browser DevTools network throttling to simulate edge cases.
7. **Batch rename (local)**: Enter reviewer from Queue, swipe through photos, rename several, verify IndexedDB filenames update and uploads use new names.
8. **Batch rename (remote)**: Enter reviewer from File Browser on an uploaded folder, rename a file, verify the file is renamed on Nextcloud via WebDAV.
---
## Build Order
Implement in this order to enable incremental testing:
1. **Flask skeleton** — App factory, config, health endpoint, static file serving.
2. **Auth** — Login/logout routes, Nextcloud credential validation, session management.
3. **WebDAV proxy** — File listing, upload, verify, mkdir, rename, and thumbnail endpoints.
4. **Frontend login page** — Basic login form and session handling.
5. **File browser** — Directory navigation and folder selection.
6. **Camera capture** — Native camera input, JPEG conversion, IndexedDB storage.
7. **Sync engine** — Upload queue, retry logic, verification loop.
8. **Service Worker** — App shell caching for offline access.
9. **Queue UI** — Pending photos list, manual sync, delete.
10. **Photo reviewer** — Full-screen swipe viewer with inline rename for both local and remote photos.
11. **Admin panel** — User listing, creation, enable/disable.
12. **PWA manifest and icons** — Installability.
13. **Docker packaging** — Dockerfile, docker-compose.
14. **Polish** — Dark mode, connectivity indicator animations, error toasts, iOS meta tags.