Commit Graph

459 Commits

Author SHA1 Message Date
Diocrafts df336da679 feat(frontend): i18n expansion, admin/profile i18n, grid/list view fix, empty state
- Add 5 new locales (hi, ar, ru, ja, ko) — now 14 total
- Admin panel: 117 i18n keys, confirm modal, animated tabs, no inline handlers
- Profile page: 58 i18n keys with data-i18n attributes
- Fix i18n safeT() shadowing bug and translationsLoaded timing
- Fix grid/list view: list header no longer shows in grid mode on login
- Fix classList.toggle hidden sync for view switching across all nav functions
- Revert .hidden important that broke login page rendering
- Add files empty state (no_files + empty_hint) with translations
- Fix language selector dropdown scroll and styling
- Fix admin panel scroll with sticky tabs
2026-03-09 00:08:34 +01:00
Diocrafts f409a9edd7 perf(frontend): add build.rs asset pipeline with oxc + lightningcss
- Bundle 31 JS files → single app.{hash}.js (oxc minifier)
- Bundle 36 CSS files → single app.{hash}.css (lightningcss)
- Resolve CSS @import chains at build time
- Inline theme-init.js to eliminate render-blocking script
- Minify all individual JS/CSS/JSON assets in static-dist/
- Auto-update Service Worker cache manifest with bundle hashes
- FNV hash-based cache-busting filenames

Results: 88 → 12 requests, 640 kB → 96.5 kB transferred (-85%)

Build modes:
- Debug: copies HTML to OUT_DIR, serves original static/
- Release: generates static-dist/ with processed assets

Also:
- Update Dockerfile to include build.rs in cacher stage
- Serve static-dist/ in release Docker builds
- Remove host static/ bind mount from docker-compose
- Switch include_str!() to OUT_DIR for all HTML pages
2026-03-08 13:10:38 +01:00
Dionisio Pozo 75979d280a Add lightcss to frontend tasks 2026-03-08 00:04:36 +01:00
Diocrafts 49ca01ea1c fix(schema): add DROP TRIGGER IF EXISTS before CREATE TRIGGER trg_sync_media_sort_date
The CREATE TRIGGER statement is not idempotent — it fails on restart
when the trigger already exists. Add the same DROP/CREATE guard used
by trg_files_decrement_blob_ref.
2026-03-07 20:55:23 +01:00
Diocrafts 2bc77a0bb1 perf(photos): append-only render eliminates DOM rebuild on scroll
Replace the innerHTML full-rebuild in _render() with two paths:
- _renderFull(): used for first load, group-mode change, and deletions
- _appendBatch(n): append-only for infinite-scroll pages — O(batch)
  instead of O(total). Existing <img> nodes are never destroyed,
  eliminating the visual flash and unnecessary DOM churn.

Also:
- Extract _renderTile() helper (DRY tile HTML generation)
- Extract _observeSentinel() helper
- Scope _setupVideoThumbnails(startIndex) to only process new tiles
- Add data-group attribute on headers for efficient CSS.escape lookup
- Fix stale WebP references in comments (now JPEG)
- Add virtual scrolling idea to TODO-LIST.md for future evaluation
2026-03-07 20:46:04 +01:00
Diocrafts b8638f5131 perf(thumbs): switch thumbnail encoding from WebP to JPEG q=80
Replace all 3 ImageFormat::WebP encode sites with JpegEncoder q=80.
Update fast-path to detect JPEG SOI instead of RIFF/WEBP magic.
Change file extension .webp -> .jpg, Content-Type headers, and
browser toBlob. Remove unused ImageFormat import and stale comments.
The webp feature stays for DECODING uploaded WebP images.
2026-03-07 20:37:19 +01:00
Diocrafts 05108d3e12 perf: denormalize media_sort_date + pre-scale video thumbs
Schema (media_sort_date denormalization):
- Add media_sort_date column to storage.files with DEFAULT created_at
- Add trigger sync_media_sort_date: when file_metadata is upserted,
  copies COALESCE(captured_at, created_at) into files.media_sort_date
- Add partial index idx_files_media_timeline on (user_id, media_sort_date DESC)
  WHERE NOT is_trashed AND media type -- enables Index Scan + Limit (no Sort)
- copy_folder_tree now copies media_sort_date for copied files
- Remove dead idx_file_metadata_captured (no longer needed)

Query optimization (list_media_files):
- Rewrite to use fi.media_sort_date instead of COALESCE(fm.captured_at,...)
- Eliminates LEFT JOIN file_metadata -- one fewer table touch
- Plan: Limit to Index Scan O(LIMIT) instead of Sort O(N)

Video thumbnail pre-scaling (client + server):
- JS: pre-scale canvas to max 400px before toBlob -- 22x less RAM, 15x less BW
- Rust: fast-path in store_external_thumbnail -- if payload is already
  WebP with dims within max_dim, store as-is (zero decode, zero encode)
2026-03-07 20:12:31 +01:00
Diocrafts fc5f101e61 perf(thumbnails): cache-first + ETag 304 eliminates DB queries on GET
Solution A — Cache-first path:
- get_thumbnail now tries moka (RAM) → disk BEFORE any DB query
- Ownership was verified at creation time; UUIDv4 prevents enumeration
- 146-photo timeline: 0 SQL queries instead of 146 per page load

Solution C — ETag / If-None-Match short-circuit:
- Deterministic ETag 'thumb-{id}-{size}' on every response
- If browser sends matching If-None-Match → 304 with zero I/O
- Cache-Control: immutable prevents even conditional requests

Additional improvements:
- Remove moka TTL (was 600s); thumbnails are immutable, weight-only eviction
- generate_all_sizes_background populates moka after disk write;
  first GET after upload served from RAM with zero disk I/O
- DB path only taken on cache miss for images needing generation
2026-03-07 19:48:35 +01:00
Diocrafts f5dd2b9a95 fix: add CSRF header to video thumbnail PUT requests
The double-submit CSRF middleware blocks all mutating requests
without the X-CSRF-Token header. photos.js was uploading video
thumbnails via PUT without it, causing a flood of 403 errors.

- Add getCsrfHeaders() spread to the fetch headers
- Add credentials: 'same-origin' to ensure cookie is sent
2026-03-07 19:24:10 +01:00
Diocrafts 661c9cb688 fix: delete thumbnails when files are permanently deleted
- Inject ThumbnailService into TrashService and FileManagementService
- Call delete_thumbnails() after permanent file deletion in:
  - TrashService::delete_permanently (single item)
  - TrashService::empty_trash (bulk: collects file IDs first)
  - FileManagementService::delete_file
  - FileManagementService::delete_with_cleanup (fallback path)
- All thumbnail cleanup is best-effort (warn on failure, never blocks)
- Prevents orphaned thumbnail files from accumulating on disk
2026-03-07 19:15:36 +01:00
Diocrafts 2aeb97383c fix: instant video thumbnails on tab switch + throttle decodes
Backend:
- 204 response: add Cache-Control: no-store so browser never caches
  'no thumbnail yet' — next GET after PUT upload gets the WebP

Frontend:
- _videoThumbCache (Map): persists fileId → URL across re-renders,
  so switching tabs reuses cached URLs instantly (no re-decode)
- Render: videos with cached URL skip the 204/error/decode cycle
- After PUT succeeds: swap blob URL → server ?v=1 URL so blob is GC'd
- Concurrency throttle: max 3 simultaneous video decodes to avoid
  overwhelming network + CPU when gallery has many videos
- Decode queue: pending videos processed as slots free up
2026-03-07 19:06:37 +01:00
Diocrafts db93b48149 feat: client-side video thumbnail generation (no ffmpeg)
Backend:
- get_thumbnail: for non-images, serve cached thumb or 204 (not 400)
- store_external_thumbnail: validate + re-encode to WebP + persist
- get_cached_thumbnail: memory → disk lookup without generation
- PUT /api/files/{id}/thumbnail/{size} endpoint for client uploads
- ThumbnailPort trait: add get_cached_thumbnail + store_external_thumbnail

Frontend (photos.js):
- On <img> error for video tiles, use <video> + <canvas> to extract
  a frame at 25% duration using browser's native codec
- Show frame immediately via blob URL
- Fire-and-forget PUT to server for permanent caching
- Subsequent visits serve cached WebP instantly (no re-extraction)

Zero server-side dependencies — CPU distributed across clients.
2026-03-07 18:55:44 +01:00
Diocrafts 54ce2d085a style: apply card border/shadow style from Archivos to Fotos tiles
- photo-tile: border-radius 4px → 12px, add 2px solid border + box-shadow
- hover: translateY(-2px) elevation effect matching file-card
- selected: border-color + background tint instead of outline
- grid gaps: 4px → 12/14/10px for daily/monthly/yearly to breathe
2026-03-07 18:34:16 +01:00
Diocrafts 6e10c6a1ac fix: files grid cards now stretch to fill full width like photos view
- grid-template-columns: minmax(200px, 240px) → minmax(200px, 1fr)
- Remove justify-content: start (no longer needed with 1fr)
2026-03-07 18:27:02 +01:00
Diocrafts 03c051f9d8 fix: add blob: to CSP frame-src and media-src for PDF/video viewing
- frame-src: add 'blob:' (wildcard * only matches network schemes)
- media-src: new directive with 'self' blob: (was falling back to default-src)
- Fixes: PDFs blocked in iframe, videos blocked by media-src fallback
2026-03-07 18:17:35 +01:00
Diocrafts 6b02c10942 perf: complete storage-layer UUID migration — eliminate UUID↔String round-trips
- Domain: Folder.owner_id + File.owner_id → Option<Uuid> (was Option<String>)
- Traits: folder_repository signatures accept Uuid directly (was &str)
- Infra: remove all fi.user_id::text / fo.user_id::text SQL casts in repos
- Infra: remove $N::uuid bind casts (columns are native UUID now)
- Services: eliminate to_string() bridge variables in folder_service,
  search_service, trash_service
- DTOs: boundary conversion to String only at API edge (file_dto, folder_dto)
- Ports: default impls compare Option<Uuid> directly
- Stubs: updated signatures to match trait

13 files changed across domain/application/infrastructure layers.
Compiles clean (cargo check passes).
2026-03-07 18:05:52 +01:00
Diocrafts cc98c5b0da fix: add ::uuid/::text SQL casts in storage repos for UUID columns
- folder_db_repository: cast user_id to ::text in SELECT, ::uuid in INSERT/WHERE
- file_blob_write_repository: cast ::uuid in INSERT for user_id column
- file_blob_read_repository: bind Uuid natively instead of .to_string()

Fixes runtime errors:
  - column user_id is of type uuid but expression is of type text
  - operator does not exist: uuid = text
2026-03-07 15:17:15 +01:00
Diocrafts 06ed0455ce perf: migrate all user/session/auth IDs from VARCHAR(36) to native UUID
- Schema: all ~15 VARCHAR(36) columns → UUID with DEFAULT gen_random_uuid()
- Domain entities: User, Session, DeviceCode, AppPassword, Share → id: Uuid
- DTOs: CurrentUser.id → Uuid (API boundary DTOs keep String for JSON)
- Auth middleware: parse JWT claims.sub (String) → Uuid at boundary
- All repository traits, port traits, service impls updated end-to-end
- Handlers: pass Uuid by value (Copy, 16 bytes) instead of String refs
- Settings chain: updated_by column → Uuid (was text, caused setup crash)
- Removed ~650 lines of String↔Uuid conversion boilerplate
- Eliminates per-request heap allocations for ID cloning
- 16-byte binary comparison vs 36-byte string comparison in all queries
- Native UUID indexing in PostgreSQL (btree on 16 bytes vs 36-char text)

85 files changed, 1090 insertions(+), 1739 deletions(-)
2026-03-07 14:59:32 +01:00
Diocrafts 9f08460027 perf: OnceLock for env var, Arc<CurrentUser> in auth, pre-compute query lowercase
- rate_limit: cache OXICLOUD_TRUST_PROXY_HEADERS in OnceLock<bool> to avoid
  syscall on every request (~500ns → ~1ns)
- auth middleware: insert Arc<CurrentUser> instead of bare CurrentUser;
  all 5 extractors now clone Arc (~1ns) instead of 4 Strings (~60-100ns)
- search_service: pre-compute query.to_lowercase() once before loops,
  eliminating N redundant heap allocations per search
2026-03-07 11:23:56 +01:00
Dionisio Pozo e4bcab0488 Merge pull request #181 from vhsdream/fix/admin 2026-03-07 09:05:28 +01:00
arch Cloud User 228d698223 Fix(admin/csp) admin.html: restore removal of inline event handlers; use theme-init.js for theming 2026-03-06 20:54:18 -05:00
Dionisio 69d25d46d6 perf: eliminate redundant PG round-trip for blob_hash after image upload
upload_file_inner now returns (FileDto, blob_hash) tuple, propagating
the BLAKE3 hash computed during the hash-on-write spool directly to
upload_file_with_thumbnails. This removes the unnecessary
get_blob_hash() SQL query that was fetching a value already in memory.

Saves ~0.5-2ms + 1 PG pool connection per image upload.
2026-03-07 00:10:56 +01:00
Dionisio 1a1e635bd3 quick fix 2026-03-06 23:46:03 +01:00
Dionisio 625453a3c9 chore: track .cargo/config.toml (target-cpu=native build config)
- Update .gitignore to allow .cargo/config.toml while ignoring other .cargo contents
- Include the target-cpu=native config that was created in previous commit but missed due to .gitignore
2026-03-06 23:37:35 +01:00
Dionisio 6c34499bbe perf: enable target-cpu=native for SIMD-optimized BLAKE3/image processing
- Add .cargo/config.toml with target-cpu=native for x86-64 and aarch64
- Update Dockerfile to pass RUSTFLAGS in both cacher and builder stages
- Enables AVX2/AVX-512 (Intel/AMD) or NEON (ARM/Apple Silicon) codepaths
- 2-3x speedup for BLAKE3 hashing and image transcoding
2026-03-06 23:28:37 +01:00
Dionisio 42b242204c perf: enable HTTP compression (gzip + Brotli) with smart predicate
- Apply CompressionLayer globally with content-type filtering
- Compress: JSON, XML, HTML, CSS, JS (60-80% bandwidth savings)
- Skip: images, video, audio, PDF, ZIP, gzip, tar, octet-stream
- Min threshold 256 bytes to avoid CPU waste on tiny responses
- Compatible with future reverse proxy (Content-Encoding passthrough)
2026-03-06 22:59:48 +01:00
Dionisio fc78dcd35d perf: eliminate Vec<char> allocation in split_sql_statements
- Replace sql.chars().collect::<Vec<char>>() with direct byte-level
  iteration over sql.as_bytes()
- Saves ~140 KB heap allocation for 35 KB schema (4× input size)
- Remove unused _start variable
- SQL is ASCII-safe so byte comparison is sufficient for all delimiters
2026-03-06 22:44:04 +01:00
Dionisio 55936699ce perf: zero-alloc File → FileDto via into_parts() ownership transfer
- Add FileParts struct and File::into_parts() to yield owned fields
- Rewrite From<File> for FileDto to move id, name, path, folder_id,
  owner_id by ownership instead of .to_string() copies
- Eliminates ~5 heap allocations per file (5000 saved per 1000-file listing)
2026-03-06 22:37:31 +01:00
Dionisio d4a7cb0414 perf: replace blocking Path::exists() with async try_exists() in dedup hot path
- store_bytes: blob_path.exists() → fs::try_exists(&blob_path).await
- store_from_file: blob_path.exists() → fs::try_exists(&blob_path).await (2 sites)
- Prevents synchronous stat() syscalls from blocking Tokio worker threads
  during concurrent uploads
2026-03-06 22:27:43 +01:00
Dionisio 3d4156673c perf: use blake3 mmap_rayon for file hashing — zero heap allocation
- Replace std::fs::read() + update_rayon() with update_mmap_rayon()
  for file hashing, eliminating full-file heap allocation (500MB file
  no longer needs 500MB of RAM to hash)
- Enable blake3 'mmap' feature in Cargo.toml
- Lower hash_bytes rayon threshold from 10MB to 128KB
- Remove dead constants HASH_BLOCK_SIZE and RAYON_HASH_THRESHOLD
2026-03-06 22:14:43 +01:00
Dionisio Pozo 6e9c88f06c Fix formatting in architecture diagram in README 2026-03-06 21:46:58 +01:00
Dionisio Pozo eb8d64383c Merge pull request #178 from jaredwolff/fix/initial-load-race-condition 2026-03-06 20:34:09 +01:00
Jared Wolff 5b5a9173bc fix(auth): resolve race condition causing files not to load on initial visit
The cached-user-data path in checkAuthentication() fired resolveHomeFolder()
and loadFiles() concurrently with refreshUserData() using non-blocking .then()
chains. When the session cookie was expired, the folder/file API calls received
401 errors before the session could be refreshed. Now awaits session validation
before loading files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:01:08 -05:00
Dionisio 9aa35aa0ea quick fix 2026-03-06 13:18:36 +01:00
Dionisio Pozo 22b5c72d11 Merge pull request #177 from jaredwolff/fix/nc-android-discovery 2026-03-06 06:38:17 +01:00
Jared Wolff 4617c1c1df fix(ui): fix list view column spacing, rubber-band selection, and clippy warnings
Add column-gap to list header/items, prevent rubber-band selection from
swallowing clicks on list headers and file items, preserve rubber-band
selection after mouseup, and extract type aliases to resolve clippy
type_complexity warnings.
2026-03-05 21:36:14 -05:00
Jared Wolff d8eecbd9ca fix(nc): enable Nextcloud Android app connectivity and uploads
- Add /remote.php/dav discovery endpoint for Android app server detection
- Add /index.php/204 connectivity check endpoint (returns 204 No Content)
- Redirect login flow to nc:// deep link for mobile credential delivery
- Support GET/HEAD on folders (NC clients use as existence checks)
- Recursive MKCOL to create missing parent directories
- Fix single-file PROPFIND returning empty multistatus response
- Strip instance suffix from preview fileId (e.g. "00000326ocnca")
- Add recommendations stub endpoint
2026-03-05 20:42:54 -05:00
Dionisio Pozo 3fc408d72d Merge pull request #172 from jaredwolff/feat/exif-metadata
feat(photos): EXIF metadata extraction and Photos timeline view
2026-03-05 23:42:10 +01:00
Jared Wolff c53a0602ec fix: resolve clippy warnings (collapsible_if, manual_clamp)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:39:34 -05:00
Dionisio Pozo ce709cc58d Merge pull request #171 from jaredwolff/fix/webdav-path-translation
fix: WebDAV path translation, admin modals, cross-device storage, and MKCOL
2026-03-05 23:36:40 +01:00
Jared Wolff 6a84a5c44e fix(photos): fix SQL type mismatch, pagination panic, missing icons, and add day/month/year grouping
- Fix photos endpoint 500: remove ::uuid cast on user_id WHERE clause (VARCHAR column)
- Fix pagination underflow panic when total_pages is 0
- Add missing 'images' and 'play' icons to SVG icon registry
- Add day/month/year grouping toggle with localStorage persistence
- Improve grid spacing and group header styling per mode
- Add i18n translations for grouping labels (all 9 locales)
2026-03-05 17:32:28 -05:00
Jared Wolff 53e4f5afe6 feat(photos): add Photos timeline view with lightbox and infinite scroll
Backend: new GET /api/photos endpoint with cursor-based pagination that
queries image/video files sorted by EXIF captured_at (falling back to
created_at), joining file_metadata for sort dates.

Frontend: dense photo grid grouped by day with lazy-loaded thumbnails,
IntersectionObserver infinite scroll, multi-select with batch
download/delete, and a full-screen lightbox with prev/next navigation,
EXIF metadata display, and download/favorite/delete toolbar.

Includes navigation wiring, CSS (with dark theme), and i18n translations
for all 9 locales.
2026-03-05 17:32:28 -05:00
Jared Wolff 69fe3a8b07 feat(photos): add EXIF metadata extraction and storage
Extract EXIF orientation, GPS coordinates, camera info, and timestamps
from uploaded images using kamadak-exif. Store metadata in a new
file_metadata PG table. Apply EXIF orientation to thumbnail generation
so images display correctly. Add /api/files/{id}/metadata endpoint.
2026-03-05 17:32:28 -05:00
Jared Wolff cea7665a43 fix(storage): handle cross-device rename and MKCOL on existing folders
- dedup_service: fall back to copy+delete when rename() fails with
  EXDEV (os error 18), which occurs when temp and blob dirs are on
  different filesystems
- NC webdav_handler: return 405 instead of 500 when MKCOL targets an
  existing folder (RFC 4918 §9.3.1)
2026-03-05 17:32:21 -05:00
Jared Wolff 633c1bbe97 fix(admin): fix invisible modals and add missing button icons
- Add hidden class to quota, create-user, and reset-password modal divs
- Override opacity:0 from modals.css with opacity:1 in admin.css
- Add crown and ban SVG icons to icon registry for toggle-role and
  deactivate buttons
2026-03-05 17:20:07 -05:00
Jared Wolff f730f576e7 fix(webdav): translate paths for all operations and fix MKCOL recursive creation
- Add resolve_webdav_path() helper that prepends user's home folder
  prefix to raw WebDAV paths before routing to handlers
- Rewrite handle_mkcol to walk path segments, creating folders with
  proper parent_id chain instead of relying on path-only lookup
- Fix double path stripping in update_file_streaming where
  get_parent_folder_id received an already-stripped parent path
  instead of the full file path it expects

Fixes #165
2026-03-05 17:20:07 -05:00
Dionisio Pozo f20e8bf2dc Merge pull request #167 from jaredwolff/feat/app-passwords-ui
feat(profile): add App Passwords UI to profile page
2026-03-05 23:16:48 +01:00
Jared Wolff d5d69d9002 fix(profile): hide auth-error and main-content divs by default
Add hidden class to both #auth-error and #main-content so they only
show when JS explicitly displays them.
2026-03-05 17:07:48 -05:00
Jared Wolff 405cb54240 fix(profile): show active/revoked status for app passwords
Add Status column to app passwords table with Active/Revoked badge.
Only show the revoke button for active passwords.
2026-03-05 17:02:43 -05:00
Jared Wolff 9adcdc436f fix(auth): use middleware-based auth for app-password API endpoints
The Nextcloud integration added duplicate /api/auth/app-passwords
handlers that only accepted Bearer tokens, breaking cookie-authenticated
browser sessions (profile page). Remove the duplicates and mount the
original app_password_handler routes which use CurrentUser from the auth
middleware, supporting all auth methods (cookie, Bearer, Basic).
2026-03-05 16:56:45 -05:00