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>
24 KiB
24 KiB
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
getUserMediaor 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 asimage/jpegat quality 0.92. - Preserve EXIF orientation where possible; if the canvas strips EXIF, detect and correct rotation before export.
- File naming convention:
{username}_{timestamp}.jpgwhere timestamp isYYYYMMDD_HHmmssin 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
PROPFINDrequests via the backend proxy to list directories. - Upload: Use
PUTrequests via the backend proxy to upload JPEG files to the selected directory. - Upload Verification: After each upload, perform a
PROPFINDorHEADrequest 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
admingroup).
- List existing Nextcloud users (via OCS Provisioning API:
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
.jpgextension is shown but not editable to prevent accidental removal. - For local photos (pending in IndexedDB): Update the
filenamefield 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
MOVErequest 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.
- Tap the filename field to edit it. The
- 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
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 aPROPFINDon the user's WebDAV root. On success, storesusernameandpassword(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 aPROPFIND(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 +pathparameter. Proxies the file to Nextcloud viaPUT. Returns success with the file's WebDAV href.HEAD /api/files/verify?path=/path/to/file.jpg— Proxies aHEADorPROPFIND(Depth: 0) to check file existence. Returns 200 if exists, 404 if not.MKCOL /api/files/mkdir— Creates a new directory via WebDAVMKCOL.POST /api/files/rename— Accepts{sourcePath, destPath}. Proxies a WebDAVMOVErequest 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
admingroup. 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
requestslibrary. - 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)
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) }
});
photostable stores:id,username,timestamp,filename,targetPath,status,blob(the JPEG image data),retryCount,lastError.settingstable 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:
- Read the file as an
Imageobject. - Detect EXIF orientation and apply rotation correction on a
<canvas>. - Export canvas as JPEG blob (
canvas.toBlob(callback, 'image/jpeg', 0.92)). - Generate filename:
{username}_{YYYYMMDD_HHmmss}.jpg. - Store the blob + metadata in IndexedDB with status
pending. - Show a brief thumbnail preview/confirmation toast.
- If online, trigger the sync engine.
- Read the file as an
Sync Engine (sync.js)
- On initialization and on
onlineevent, query IndexedDB for all photos with statuspendingoruploading(retry stalled uploads). - Upload process for each photo:
- Set status to
uploading. POSTthe JPEG blob to/api/files/uploadwith the target path.- On success, set status to
uploaded. - Verify:
HEAD /api/files/verify?path={targetPath}/{filename}. - On verification success, set status to
verified, then delete the blob from IndexedDB. - On any failure, increment
retryCount, setlastError, revert status topending, and schedule retry with exponential backoff (5s, 15s, 45s, 2min, 5min, cap at 5min).
- Set status to
- 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/thumbnailand full images on demand via/api/files/list+ direct fetch.
- Swipe navigation: Implement touch-based horizontal swipe detection. Track
touchstart,touchmove,touchendevents. 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
.jpgextension 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
filenamefield synchronously. - For remote photos: calls
POST /api/files/renamewith 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-schememedia query. - Status colors: green (verified/online), yellow (uploading/syncing), red (error/offline), gray (pending).
PWA Configuration
manifest.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
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
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=StrictandHttpOnlyon 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'slastErrorfield. 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
- Offline capture and cache: Disconnect network, take photos, verify they're stored in IndexedDB.
- Upload and verification cycle: Reconnect, verify photos upload and are removed from queue only after server-side verification.
- Multi-user isolation: Log in as two different users on the same device, verify queues are separate.
- Admin operations: Create user, verify the new user can log in and use the app.
- iOS Safari and Android Chrome: Test on real devices — emulators don't accurately represent camera input and Service Worker behavior.
- Slow/intermittent connectivity: Use browser DevTools network throttling to simulate edge cases.
- Batch rename (local): Enter reviewer from Queue, swipe through photos, rename several, verify IndexedDB filenames update and uploads use new names.
- 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:
- Flask skeleton — App factory, config, health endpoint, static file serving.
- Auth — Login/logout routes, Nextcloud credential validation, session management.
- WebDAV proxy — File listing, upload, verify, mkdir, rename, and thumbnail endpoints.
- Frontend login page — Basic login form and session handling.
- File browser — Directory navigation and folder selection.
- Camera capture — Native camera input, JPEG conversion, IndexedDB storage.
- Sync engine — Upload queue, retry logic, verification loop.
- Service Worker — App shell caching for offline access.
- Queue UI — Pending photos list, manual sync, delete.
- Photo reviewer — Full-screen swipe viewer with inline rename for both local and remote photos.
- Admin panel — User listing, creation, enable/disable.
- PWA manifest and icons — Installability.
- Docker packaging — Dockerfile, docker-compose.
- Polish — Dark mode, connectivity indicator animations, error toasts, iOS meta tags.