Nupplo

# Nupplo SE ๐Ÿงฑ > **Formerly Brickfolio.** Since 3.0.0 the app is called **Nupplo SE** โ€“ SE > stands for *Server Edition*, the edition you run yourself. Same app, new > name; existing installations keep running without any change. *๐Ÿ‡ฌ๐Ÿ‡ง English ยท [๐Ÿ‡ฉ๐Ÿ‡ช Deutsch](README.md)* [![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-support-ffdd00?logo=buymeacoffee&logoColor=black)](https://buymeacoffee.com/melle79) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) A self-hosted PWA to scan, manage and value a LEGOยฎ collection โ€“ built for the whole family on one shared database, with an optional **Collector-Pro mode** for anyone who buys and sells at flea markets. **Photo โ†’ recognition โ†’ collection.** Recognition runs on the free [Brickognize API](https://brickognize.com); prices, set inventories and metadata come from [BrickLink](https://www.bricklink.com) and [Rebrickable](https://rebrickable.com) (your own API keys required, see below). > ๐Ÿ“– The full **user manual** lives at [`docs/MANUAL.md`](docs/MANUAL.md) > ยท [๐Ÿ‡ฉ๐Ÿ‡ช deutsche Fassung](docs/HANDBUCH.md). ## Features **Capture & manage** - ๐Ÿ“ท Scan minifigures and sets straight from the phone camera (candidate list with match score) or search by name/number โ€“ plain numbers are looked up as both a set *and* a figure automatically - ๐Ÿ“ฆ Quantities, condition (new/used), notes, image gallery, full-text search (with ๐Ÿ” icon and โœ• to clear), sorting and type filter; optional **grid view** (two per row) for browsing - ๐Ÿ”Ž Catalogue search from three characters, **10 hits per page** with โ€œload more resultsโ€ โ€“ every match is reachable - ๐Ÿ‘ฅ **Add a set's figures with it**: when a set enters the collection, the app asks which of its minifigures are included (all, none or a selection, with their own condition) โ€“ without double-counting them in the total value - ๐Ÿ‘ฅ Set linking: sets know their figures (โ€œ๐Ÿ‘ฅ 3/4 โœ”โ€ completeness), figures show which of your sets they belong to; missing set figures move to the wishlist with a single tap - ๐Ÿ–ผ **Reload the image** for an entry if it is missing or wrong - ๐Ÿ”Ž **Detail view everywhere**: tapping a search or scan hit opens a popup with year, theme, prices and โ€“ for minifigures โ€“ the **parts they contain, with colour names** - ๐ŸŽจ **Custom figures**: home-made builds with a number the app assigns for you (`custom-001`, `-002` โ€ฆ) and your own picture โ€“ optionally the photo straight from the scan. They live under the โ€œCustomโ€ theme - ๐Ÿ—‚๏ธ **Theme cards**: sorted by theme, the collection shows collapsible cards (Star Wars, City, Custom โ€ฆ) with count and value per theme. The last sort order is remembered per profile **Prices & value** - ๐Ÿ’ถ BrickLink average prices (new/used) fetched automatically in the background, with a built-in **price-history log** and per-item chart - ๐ŸŒ **Selectable price region** (More โ†’ Price region): worldwide, Germany, Austria, Switzerland or Europe โ€“ with no sales in the chosen region the app widens in two steps (Europe first, then worldwide). Existing prices can be recalculated step by step, priceless items re-fetched with one click. In the item details a small **flag** (๐Ÿ‡ฉ๐Ÿ‡ช/๐Ÿ‡ช๐Ÿ‡บ/๐ŸŒ) on every price row shows where the price comes from - ๐Ÿท **Asking prices** (optional): on top of the average sale price, *from how much* a figure is available right now โ€“ in the item details and as a tag in wish and shopping lists - ๐Ÿ“Š **Stats tab**: key figures, overall value trend, split by type/condition, value by release year (tap a bar for the yearโ€™s details), Top 10 **Wishlist** - โญ Save from any scan/search result, average-price widgets, โ€œboughtโ€ hand-off into the collection incl. condition choice - ๐Ÿงฉ Figures that belong to one of your sets and are still missing are flagged with โ€œmissing from your setโ€ โ€“ a tap jumps straight to the set **Trading network** (optional, invite only) > **It is off by default, and nothing is shared.** Your collection stays on > your server. The network only exists for you once you join with an > invitation code โ€“ and even then only what you explicitly choose goes out: > the items you offer, your profile and, if you switch it on, your wishlist. > Leaving takes everything out again. - ๐Ÿค Several Nupplo instances connect through a small **trading hub**: everyone offers the items they want to pass on โ€“ **for trade, for sale or both** โ€“ and sees what the others offer - ๐Ÿ”ข **You pick what goes in**, and how many: out of three identical figures you can offer just one - ๐Ÿงญ **Discover**: who has what you're looking for, who wants what you have spare, and who collects the same themes โ€“ worked out on your own instance - ๐Ÿ‘ค **Profiles**: about me, area, favourite themes, trade record โ€“ all optional; the collection size only rounded and only if you want - ๐Ÿ“ฆ **Trades with a beginning and an end**: accepted โ†’ sent โ†’ arrived โ†’ booked โ†’ completed. Adding and removing book straight into the collection, purchase log included - ๐Ÿ’ฌ **Messages are end-to-end encrypted** (X25519 + AES-256-GCM). The hub cannot read them and deletes them once delivered; the readable history stays on the instances involved - โœ‰๏ธ **Invitations with a quota**: everyone may invite three friends and can request more. Nobody gets in without an invite - โธ **Pause after inactivity**: if you haven't been around for a while, the hub hides your offers until you come back - โš‘ **Reports with a follow-up**: the report stays visible to the reporter, the hub admin can ask questions and take a measure (notice, warning, block) that the person concerned sees in their app โ€“ without learning who reported them - ๐Ÿ–ผ๏ธ Custom figures travel with a **downscaled thumbnail** so the other side doesnโ€™t just see a placeholder **Collector-Pro mode** (a role the admin grants per user) - ๐Ÿ’ฐ Purchase price per entry โ€“ pre-filled with the BrickLink average of the capture day (โš™๏ธ) or entered manually (โœ๏ธ), with profit/loss display - ๐Ÿ›’ **Shopping lists** for the flea market: fill by scanning, live market value, purchase price per item, a **bundle offer** with proportional distribution by market value and a 60 % price suggestion - ๐Ÿ“‹ **Sell list**: every duplicate with its giveaway quantity and sale value โ€“ figures needed for your own sets stay reserved, everything else keeps one copy for you - ๐Ÿงฉ **Missing set figures**: across all your sets, which minifigures are still missing (count, sets, estimated cost) โ€“ add them to the wishlist one by one or all at once, export as CSV or print - ๐Ÿ—’ When receiving from a shopping list, the **list name is written into the notes** of the collection entry (origin stays traceable) - ๐Ÿ“ˆ Extra stats: total paid, profit, best value gains **Family & operations** - ๐Ÿ” Multi-user with token login (PBKDF2-hashed passwords), admin and pro roles (admins can appoint further admins), self-service password/name change - ๐Ÿ’พ Full **backup** as JSON (download & restore), CSV export and print-ready lists - ๐Ÿท Configurable **display name** in logo and title (empty by default); ideal when several family members each run their own instance - ๐ŸŒŒ Three **themes** to choose from (More โ†’ Design): light โ€œClassicโ€, dark โ€œGalaxyโ€ with a starfield, and โ€œNovaโ€ โ€“ a modern glass design with a blue accent; the choice is per device - ๐Ÿ”” **Notices on the start screen**: if BrickLink renames or removes a number from your collection, it shows up in the Scan tab โ€“ and stays there until someone dismisses it. The app looks up the new number in the BrickLink Catalog Change Log and applies it everywhere with one click - ๐ŸŒ **External-access helper** (More โ†’ External access, admin): turns your Cloudflare tunnel token into a ready-to-paste `docker-compose` block โ€“ reach the app on the go without port forwarding. The app never starts the tunnel itself (no Docker access); the token stays in the browser - ๐Ÿž **Error report** (More โ†’ Error report, admin): errors from every device report themselves to your own server and identical ones are grouped. With a GitHub token stored, one click turns an entry into an issue; API keys and the token are stripped from the text first - ๐Ÿ–ฅ **Responsive to screen width**: bottom tab bar on phones, a left sidebar with a wider grid (four to five cards per row) on desktop โ€“ same app, just laid out to use the space - โœจ **Modern look**: items open in a window that leads with the **image across the full width**; below it the name, tags and โ€“ for dealers โ€“ three tiles (paid, value, profit). The rest sits on three tabs (copy, prices, more) instead of stacking up. In the collection the product image shows as a subtle card background - ๐Ÿ“ฒ Installable as a PWA โ€“ on a phone the scan page offers to put it **on the home screen** (a button on Android, instructions on iPhone) and hides the hint once it is there. Offline shell, no cloud โ€“ everything stays on your own server ## Screenshots | Scan | Collection | |:---:|:---:| | Scan | Collection | | **Stats** | **Shopping list (flea-market mode)** | | Stats | Shopping list | *(Screenshots with demo data)* ## Quick start (Docker) No source code, no build โ€“ two commands: ```bash mkdir nupplo && cd nupplo curl -sLo docker-compose.yml https://raw.githubusercontent.com/Melle79/nupplo/main/docker-compose.example.yml docker compose up -d ``` Open `http://:8300` โ€“ on first visit a **setup wizard** walks you through everything: admin account, display name, API keys (with a test button) and, if you have one, your invite to the trading network. Every step can be skipped; anything can be added later under *More*. The database is stored persistently at `./data/brickfolio.db`. The image (`ghcr.io/melle79/nupplo:latest`, mirrored as `melle79/nupplo:latest` on Docker Hub) is built for **amd64** โ€“ that is **x86-64**, any ordinary Intel *or* AMD machine (Synology, Intel NAS, PC) and **arm64** (Raspberry Pi, ARM NAS). ### Synology NAS No shell needed: **Container Manager โ†’ Project โ†’ Create**, paste the YAML, done. Step by step in [`docs/SYNOLOGY.md`](docs/SYNOLOGY.md) (in German). Over SSH it works just as well โ€“ create `/volume1/docker/nupplo` and run the commands above with `sudo`. ### Other NAS systems and computers It is an ordinary OCI image on two public registries โ€“ it runs wherever containers run. Same YAML as above, just pasted somewhere else: | System | Where the YAML goes | |---|---| | **QNAP** | Container Station โ†’ *Applications* โ†’ Create | | **UGREEN** | Docker โ†’ *Project* โ†’ Create | | **TerraMaster** | Docker Manager โ†’ *Compose* | | **Asustor** | Portainer (from App Central) โ†’ *Stacks* | | **Unraid** | Docker tab, or *Compose Manager* from Community Apps | | **TrueNAS SCALE** | Apps โ†’ *Custom App* (YAML) | | **OpenMediaVault** | omv-extras โ†’ Compose โ†’ *Files* | | **Linux / Raspberry Pi / Mac / Windows** | create a folder, `docker compose up -d` | Very old 32-bit ARM devices (`armv7`) are not supported. **Unraid** has it easier: under *Docker โ†’ Settings โ†’ Template Repositories* add this address โ€“ ``` https://github.com/Melle79/unraid-templates ``` โ€“ and Nupplo appears under *Docker โ†’ Add Container* in the template picker, with the port, the `/data` path and the optional keys pre-filled. The template is maintained in [Melle79/unraid-templates](https://github.com/Melle79/unraid-templates); a copy lives here under [`unraid/brickfolio.xml`](unraid/brickfolio.xml). ### Hosted instead of at home If you would rather not run your own hardware, rent the instance. **It belongs to whoever creates it** โ€“ data, cost and access. There is still no Nupplo service in between. | Provider | How | Note | |---|---|---| | **Render** | [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/Melle79/nupplo) | Reads [`render.yaml`](render.yaml). Needs the smallest **paid** plan: Nupplo requires a persistent disk for `/data`, and the free plan has none | | **Railway** | New project โ†’ *Deploy from GitHub* โ†’ this repo, then set *Settings โ†’ Config-as-Code* to `deploy/railway/railway.json` | Afterwards attach a **volume** at `/data`, otherwise the collection is gone after every restart | | **Coolify / Dokploy** | Paste `docker-compose.example.yml` | On your own VPS, full control | > **Why no free plan?** Without a persistent disk the database lives inside > the container โ€“ every restart would wipe the collection. Better an honest > note than a deployment that quietly loses data. Without a compose UI: ```bash docker run -d --name nupplo --restart unless-stopped \ -p 8300:8300 -v /path/to/data:/data \ ghcr.io/melle79/nupplo:latest ``` ### Building it yourself (instead of the ready-made image) Only needed if you want to run your own changes: ```bash mkdir nupplo && cd nupplo curl -sL https://github.com/Melle79/nupplo/archive/refs/heads/main.tar.gz | tar xz --strip-components=1 cp docker-compose.example.yml docker-compose.yml sed -i 's|image: ghcr.io/melle79/nupplo:latest|build: .|' docker-compose.yml docker compose up -d --build ``` ## Configuration | Variable | Required | Description | |---|---|---| | `ADMIN_USER` / `ADMIN_PASSWORD` | no | Optional: create the admin automatically (otherwise via browser setup) | | `DB_PATH` | no | Path to the SQLite file (container default: `/data/brickfolio.db`) | | `BL_CONSUMER_KEY` / `BL_CONSUMER_SECRET` / `BL_TOKEN` / `BL_TOKEN_SECRET` | no | BrickLink Store API for prices & set inventories ([request a key](https://www.bricklink.com/v2/api/register_consumer.page)) | | `BACKUP_KEEP` | no | Keep this many automatic daily backups (default 14, 0 = off) | | `REBRICKABLE_KEY` | no | Rebrickable API for name search ([create a key](https://rebrickable.com/api/)) | | `GITHUB_REPO` | no | Target repository for issues from the error report (default `Melle79/nupplo`) | | `BRICKFOLIO_NAME` | no | Display name in logo/title (empty by default); also settable in-app | All API keys can alternatively be stored **in the app** (More โ†’ API keys, admin only) โ€“ environment variables serve as a fallback. ## Permissions overview | Action | Default | Collector-Pro | Admin | |---|:-:|:-:|:-:| | Scan, collection, wishlist, stats | โœ” | โœ” | โœ” | | See shopping lists & mark an item โ€œreceivedโ€ | โœ”ยน | โœ” | โœ”ยน | | Create/fill/archive lists, bundle offer, sell list | โ€“ | โœ” | โ€“ | | See purchase prices & profit | โ€“ | โœ” | โ€“ | | Users, roles, API keys, backup | โ€“ | โ€“ | โœ” | ยน The tab only appears when at least one active list exists. Roles combine (the admin can grant themselves the pro role). ## Updates & backup **The app tells you.** Under *More โ†’ Version & updates* it compares the running version against the latest release, whichever way you installed. **With the ready-made image** (the quick start above), two commands in the project folder: ```bash docker compose pull && docker compose up -d ``` On a Synology the same works without a shell: *Container Manager โ†’ Project โ†’ nupplo โ†’ Action โ†’ Build and restart*. The `data/` folder is untouched, database migrations run automatically on start and are idempotent. **A snapshot beforehand never hurts** โ€“ download the JSON file in the app under *More โ†’ Backup*. > `update.sh` and `update-watch.sh` are **not** in the image and not in your > folder if you only fetched the `docker-compose.yml`. If you want them > (automatic snapshot, updating from inside the app), fetch them once: > > ```bash > curl -sLO https://raw.githubusercontent.com/Melle79/nupplo/main/update.sh > curl -sLO https://raw.githubusercontent.com/Melle79/nupplo/main/update-watch.sh > ``` **Built from source**: `sudo bash update.sh` in the project folder. It takes a database snapshot first (the last three are kept) and detects on its own whether to pull the image or rebuild. ### Updating from inside the app (optional) **Entirely optional** โ€“ without setup nothing changes and updates keep working via `update.sh` over SSH. The button only appears once the helper below is in place; until then the app just points to it. **How it works.** The app does **not** run the update itself โ€“ it canโ€™t, since it lives inside the container. It only writes `data/update-requested.json`. A small script on the server picks that up and runs `update.sh`. That way the app needs **no Docker access** โ€“ handing the socket to a container is effectively root on the host. #### Setup Run `update-watch.sh` regularly. **Once a minute is plenty** โ€“ the update itself takes one to three minutes anyway. **Synology (DSM):** Control Panel โ†’ Task Scheduler โ†’ Create โ†’ Scheduled Task โ†’ User-defined script | Tab | Setting | | --- | --- | | General | User: **`root`** (otherwise the script may not run `docker compose`) | | Schedule | Daily ยท start `00:00` ยท โ€œContinue running within the same dayโ€ โœ” ยท repeat **every minute** ยท last run time: **`23:59`** | | Task Settings | Command: `sh /path/to/nupplo/update-watch.sh` | > โš ๏ธ โ€œLast run timeโ€ defaults to `00:59` โ€“ the task would then only run during > the first hour of the day. Set it to `23:59`. **Linux with cron:** `* * * * * sh /path/to/nupplo/update-watch.sh` #### Multiple instances Running several Nupplos (each in its own folder with its own `docker-compose.yml`)? Prefer **one task per instance**. That is the most robust setup: each runs independently, and the task scheduler shows you per instance whether it succeeded. If you would rather use a single task, append `|| true` to every line: ```sh sh /volume1/docker/nupplo/update-watch.sh || true sh /volume1/docker/nupplo-zweit/update-watch.sh || true ``` > โš ๏ธ Without `|| true` the second line may never run: if the first one exits > with an error (missing permissions, say), the scheduler aborts the whole > script and the second instance never gets a heartbeat. Either way the instances stay independent โ€“ each has its own marker in its own `data` folder, so updating one leaves the other alone. #### Flow The admin picks now / 1 min / 5 min โ†’ every signed-in browser shows a countdown (โ€œplease finish your editsโ€), then a lock screen. Once the server is back, the browsers reload themselves. While the countdown runs, the admin can cancel. - The helper touches `data/update-watch-alive` on every run. That is how the app knows it is set up โ€“ if the heartbeat is older than five minutes, the update is not offered at all. - Log of every run: `data/update-watch.log`. - Backup: in-app under More โ†’ Backup (JSON with all data incl. users and price history) **or** simply copy `data/brickfolio.db`. ## Tech FastAPI + SQLite (no ORM) ยท Vanilla-JS PWA (no build step) ยท Docker deployment ยท APIs: Brickognize, BrickLink Store API (OAuth1), Rebrickable. ### What lives in its own repo This repo holds the **program**. Three things beside it move at their own pace and therefore have their own repo โ€“ all three private, because they describe credentials or carry screenshots of a real collection: | what | repo | for | |---|---|---| | project site brickfolio.cc | `brickfolio-website` | static Cloudflare Worker, published by hand | | catalogue service | `brickfolio-katalogdienst` | builds the catalogue index and writes it here, encrypted, to `katalog/index.nupplo` | | hub console | `brickfolio-hub-admin` | interface for running the trade network | | trade hub | `brickfolio-hub` | Cloudflare Worker + D1 that instances talk to for trading | The app itself only talks to the hub over HTTP; it does not need its source. ## Legal LEGOยฎ is a trademark of the LEGO Group, which does not sponsor, authorise or endorse this project. BrickLink, Rebrickable and Brickognize are trademarks of their respective owners; their APIs are subject to their respective terms of use. This is a private hobby project with no commercial intent. Data and images come from Rebrickable (catalogue search), BrickLink (prices, set contents, images) and Brickognize (image recognition โ€“ taking a photo sends it there). The same notes are shown in the app under **More โ†’ Sources & legal**. The **Nunito** typeface (SIL Open Font License 1.1, licence text in `frontend/fonts/OFL.txt`) is served locally โ€“ so no visitor data goes to font CDNs, and the interface still looks right when the server has no internet connection. ### What works without internet Your collection and the interface both live on your own server. If the server is reachable on the local network but has no internet, these keep working: - browsing and editing the collection, wishlist and shopping lists - manual entry, including custom figures with your own picture - statistics, CSV export and print lists - downloading and restoring a backup Everything that asks the outside world needs internet: **scanning** (Brickognize), **search by name** (Rebrickable), **prices and set contents** (BrickLink), the **catalogue images** (they are hosted on their servers โ€“ images you uploaded yourself are not), the **update check** and the **trading network**. With no server reachable at all, the installed PWA still loads its interface from the cache but shows no data โ€“ data always comes live from your own server, none of it is kept in the browser. ## Support Nupplo is a private hobby project and free to use. If you like it and want to support development, I'd be happy about a coffee โ˜• Buy Me a Coffee ## License [MIT](LICENSE) for the program code. The figure catalogue in `katalog/` is excluded: it may only be used within Nupplo, see [katalog/LIZENZ.md](katalog/LIZENZ.md).