Files
receipt-manager/nextsnap.md
kamaji f7e704943e Add NextSnap PWA specification and implementation guide
Complete specification for NextSnap, an offline-first Progressive Web App for Nextcloud photo management. Includes 14 modules covering camera capture, upload queue, photo review, admin panel, and Docker deployment.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-06 23:32:05 -06:00

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.


All work is to be performed in the nextsnap/ directory on host docker1

nginx reverse proxy configuration is managed is /etc/nginx/sites-available/reverse-proxy.conf on host nginx-reverse

git repositories are on host git1

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

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)

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

{
    "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=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.