- In-panel bulk actions footer (Delete/Enable/Disable stay inside the box)
- Per-row icon actions with tooltips; Save button lights up only on changes
- Per-row QR modal; Details modal close fixed and reliable
- Auto-refresh after creating a link; new row is highlighted and scrolled into view
- Expiry save fix: stores end-of-day to avoid off-by-one date issues
Roadmap highlight
- We’d like to add a per-user UI and remove reliance on a fixed API key by allowing users to authenticate and provide their own Immich API tokens. This is not in scope for the initial versions but aligns with future direction.
- The frontend automatically switches to chunked mode only for files larger than the configured chunk size.
### 📱 Device‑Flexible HMI (New)
- Fully responsive UI with improved spacing and wrapping for small and large screens.
- Mobile‑safe file picker and a sticky bottom “Choose files” bar on phones.
- Safe‑area padding for devices with notches; refined dark/light theme behavior.
├─ data/ # Local dev data dir (bind to /data in Docker)
├─ main.py # Thin dev entrypoint (python main.py)
├─ requirements.txt # Python dependencies
├─ Dockerfile
├─ docker-compose.yml
├─ .env.example # Example dev environment (optional)
├─ README.md
└─ screenshot.png # UI screenshot for README
$ sudo docker compose up --build -d
```
---
### Chunked Uploads
## Requirements
- Chunked uploads areenabled by default. Uses setting `CHUNKED_UPLOADS_ENABLED=true`.
- Configure chunk size with `CHUNK_SIZE_MB` (default: `50`). The client only uses chunked mode for files larger than this.
- Intended to bypass upstream proxy limits (e.g., 100MB) while preserving duplicate checks, EXIF timestamps, album add, and per‑item progress via WebSocket.
## Developtment
### Architecture
- **Frontend:** static HTML/JS (Tailwind). Drag & drop or "Choose files", queue UI with progress and status chips.
- **Backend:** FastAPI + Uvicorn.
- Saves uploaded files to the local filesystem.
- Computes SHA‑1 and checks a local SQLite cache (`state.db`) to prevent duplicates.
- WebSocket `/ws` pushes per‑item progress to the current browser session only.
- **Persistence:** A local SQLite database (`state.db`) prevents re‑uploads across sessions. Uploaded files are stored in `/data/uploads`.
### Requirements
- **Python** 3.11
- An **Immich** server + **API key**
---
# Local dev quickstart
## Development
### Local dev quickstart
Run with live reload:
@@ -202,29 +84,23 @@ python main.py
The backend contains docstrings so you can generate docs later if desired.
---
## Dev Configuration (.env)
### Dev Configuration (.env)
```ini
# Server (dev only)
HOST=0.0.0.0
PORT=8080
# Immich connection (include /api)
IMMICH_BASE_URL=http://REPLACE_ME:2283/api
IMMICH_API_KEY=ADD-YOUR-API-KEY # needs: asset.upload; for albums also: album.create, album.read, albumAsset.create
MAX_CONCURRENT=3
# Public uploader page (optional) — disabled by default
PUBLIC_UPLOAD_PAGE_ENABLED=TRUE
# Album (optional): auto-add uploads from public uploader to this album (creates if needed)
IMMICH_ALBUM_NAME=dead-drop
# Local dedupe cache (SQLite)
STATE_DB=./data/state.db
# Telegram Bot for notifications (optional)
#TELEGRAM_BOT_API_KEY=
#TELEGRAM_BOT_OWNER_ID=
# Base URL for generating absolute invite links (recommended for production)
You can keep a checked‑in `/.env.example` with the keys above for onboarding.
---
### How it works
## How it works
1.**Queue**– Files selected in the browser are queued; each gets a client-side ID.
2.**De-dupe (local)**– Server computes **SHA‑1** and checks `state.db`. If seen, marks as **duplicate**.
3.**Save**– The file is saved to the local filesystem under `./data/uploads`.
4.**Album**– If an album is specified via an invite link, or a folder name is provided on the public page, the file is saved into a corresponding subdirectory. Client-side folder structure is also preserved.
5.**Progress**– Backend streams progress via WebSocket to the same session.
6.**Privacy**– The UI shows only the current session's items. It does not provide a way to browse saved files.
1.**Queue**– Files selected in the browser are queued; each gets a client‑side ID.
2.**De‑dupe (local)**– Server computes **SHA‑1** and checks `state.db`. If seen, marks as **duplicate**.
3.**De‑dupe (server)**– Attempts Immich `/assets/bulk-upload-check`; if Immich reports duplicate, marks accordingly.
4.**Upload**– Multipart POST to `${IMMICH_BASE_URL}/assets` with:
-`assetData`, `deviceAssetId`, `deviceId`,
-`fileCreatedAt`, `fileModifiedAt` (from EXIF when available; else `lastModified`),
-`isFavorite=false`, `filename`, and header `x-immich-checksum`.
5.**Album**– If `IMMICH_ALBUM_NAME` is configured, adds the uploaded asset to the album (creates album if it doesn't exist).
6.**Progress**– Backend streams progress via WebSocket to the same session.
7.**Privacy**– UI shows only the current session's items. It never lists server media.
### Security notes
---
## Security notes
- The menu and invite creation are behind login. Logout clears the session.
- Invite links are public by URL; share only with intended recipients.
- The default uploader page at `/` is disabled unless `PUBLIC_UPLOAD_PAGE_ENABLED=true`.
- The Immich API key remains **server‑side**; the browser never sees it.
- No browsing of uploaded media; only ephemeral session state is shown.
- The menu and invite creation are behind login. Logout clears the session.
- Invite links are public by URL; share only with intended recipients.
- The default uploader page at `/` is disabled unless `PUBLIC_UPLOAD_PAGE_ENABLED=true`.
- No browsing of uploaded media; only ephemeral session state is shown.
- Run behind HTTPS with a reverse proxy and restrict CORS to your domain(s).
## Usage flow
- Admin: Login → Menu → Create invite link (optionally one‑time / expiry / album) → Share link or QR.
- Guest: Open invite link → Drop files → Upload progress and results shown.
- Optional: Enable public uploader and set `IMMICH_ALBUM_NAME` for a default landing page.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.