Skip to content

Public viewable binders - #308

Merged
Git-Romer merged 33 commits into
Git-Romer:mainfrom
hiddenbanana:feature/public-binders
Jul 27, 2026
Merged

Public viewable binders#308
Git-Romer merged 33 commits into
Git-Romer:mainfrom
hiddenbanana:feature/public-binders

Conversation

@hiddenbanana

@hiddenbanana hiddenbanana commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds an admin-controlled global switch for public profiles and shared binders. It is disabled by default, and only admins can change it.
  • Lets each user opt their own public profile in or out independently, with market values hidden unless the user explicitly enables them.
  • Derives public URLs from the trainer name (for example /u/audit-trainer) with reserved-name, invalid-character, collision, migration, and concurrent-claim protection.
  • Adds the anonymous public directory at /u, public profile pages, and public collection-binder pages.
  • Keeps anonymous serializers privacy-safe: private/wishlist binders, purchase prices, costs, P&L, usernames, auth data, and other owner-only fields are excluded.
  • Enforces collection-only sharing at the write boundary. Wishlist binders cannot be published, and changing binder type always requires a fresh sharing decision.
  • Uses grouped public card tiles with per-variant quantities, real stacked card artwork, a badge legend, and deterministic shine priority: First Edition → Special → Reverse Holo → Holo → Other → Normal.
  • Keeps private collection binders ungrouped while adding variant badges, an amount badge and legend, variant-specific shine, and sorting by recent, number, name, price, quantity, or variant.
  • Consolidates the application onto one non-stacking, variant-specific shine system everywhere variant data is available, including reduced-motion support.
  • Adds consistent public navigation: shared back-button styling and one persistent, clickable Pokéball home control.
  • Handles public-link clipboard failures instead of reporting false success.
  • Updates the application version to 1.26.0.

Public response behavior

  • Successful anonymous responses use public, max-age=0, must-revalidate.
  • Disabled, opted-out, missing, and other privacy-sensitive failure responses use no-store.
  • The public client never attaches an Authorization header, including when an expired token exists in local storage.

Validation

  • Backend: 276 tests passed, plus 20 subtests
  • Frontend: 64 tests passed
  • Production frontend build
  • Backend and frontend Docker builds
  • Version consistency and git diff --check
  • PostgreSQL 18 fresh initialization and upgrade-migration simulation
  • Publication-transition, handle collision/race, cache, privacy, and expired-token coverage
  • Real Firefox-style and Chromium desktop workflows
  • Real Chromium 320px mobile workflow with no horizontal overflow
  • Reduced-motion, navigation persistence/hit-target, clipboard success/failure, and visual screenshot checks
  • Load sanity: 200 public profiles and 5,000 binder entries
  • Two fresh independent reviewer passes; final verdict: ready

Deliberately out of scope

  • A public dashboard was discussed but intentionally excluded to keep this PR focused.
  • Languages without new dedicated strings continue to use the existing English fallback.

hiddenbanana and others added 26 commits July 25, 2026 08:51
Public profile (custom handle) exposing owner-shared binders, viewable
anonymously and discoverable in-app. Dedicated unauthenticated /api/public
namespace with whitelist serializers so purchase price / cost / P&L cannot
leak; market values shown only on owner opt-in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
11 TDD tasks: data model + migrations, handle validation, whitelist
serialization, unauthenticated public API, owner controls, per-binder
toggle, leaderboard discovery, frontend pages/routes + token-less client,
owner UI, cache/rate-limit. Backend unittest + frontend vitest throughout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
- Add is_public field to BinderUpdate schema (Optional[bool] = None)
- Add is_public field to BinderResponse schema (bool = False)
- Include is_public in _binder_response() constructor
- Persist is_public updates in update_binder()
- Add BinderPublicToggleTests test class

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lient

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… frontend)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds PublicProfile and PublicBinderView pages consuming the Task 8
publicClient, a shared formatEur(value) helper (returns null for
hidden/non-numeric values so callers can conditionally render), and
wires /u/:handle and /u/:handle/binder/:binderId as siblings in the
top-level Routes, before the /* catch-all, so they render outside
ProtectedRoutes for anonymous visitors.

Co-Authored-By: Claude <noreply@anthropic.com>
…rboard links

Add GET /api/profile/ to hydrate the public-profile columns (they live on
User, not the settings key/value store), plus client.js helpers, a
Settings.jsx "Public Profile" section (handle input with debounced
availability check, public/show-values toggles, copy link), a per-binder
"Share publicly" toggle in Binders.jsx, and a public-profile link on
Leaderboard rows that have a handle.

Co-Authored-By: Claude <noreply@anthropic.com>
Add Cache-Control: public, max-age=300 to both public GET endpoints
(get_public_profile, get_public_binder) via a test-safe defaulted
response: Response = None param, so Task 4's direct-call unit tests
(no response arg) keep passing.

Rate limiting: no per-route @limiter.limit decorator added. main.py's
existing Limiter(default_limits=["60/minute"]) + SlowAPIMiddleware
already applies globally to these routes, and importing that limiter
into api/public.py would create an import cycle (main.py imports
api.public). Documented via code comment instead.

Co-Authored-By: Claude <noreply@anthropic.com>
…iants

Public binder view returned cards in arbitrary DB order and omitted the
variant, so cards were unordered and variant prints were indistinguishable
(and mispriced when values were shown).

- _binder_cards: order by added_at desc (matches owner's binder view) and
  eager-load card/set/collection_item to avoid N+1
- _serialize_card: expose variant (from the linked collection item) and use
  it for effective_market_price; summary total_value now variant-correct
- PublicCard schema gains variant; PublicBinderView renders VariantPills
- tests: variant presence, variant-absent default, added_at ordering

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The public binder listed cards newest-added-first (matching the owner's app
view), but collectors expect card-number order. Sort by set, then natural card
number (1, 2, 10 — not 1, 10, 2), then variant so same-number prints group.

- extract natural_card_number_key into services/card_numbers.py (shared);
  api/sets.py now imports it instead of a private duplicate
- public_profile._binder_cards sorts in Python via that key
- test asserts 1, 2, 10 ordering regardless of add order

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
public,max-age=300 pinned a stale browser copy for up to 5 minutes, so owner
edits (unshare, value toggle) and card-order/variant fixes appeared not to
take effect until the cache expired. Drop to public,max-age=30,must-revalidate
so changes surface within seconds while still blunting load bursts. Dedupe the
two header assignments into _set_public_cache and pin the value with tests
(set on success, absent on 404).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PublicBinderView imported ../components/VariantPills, which was never
added on this branch - build has been failing standalone. VariantPills
was unified into CardStateIndicators and merged upstream as part of
v1.24.0 (PR Git-Romer#295), so pull that component plus its cardVariants.js
dependency straight from upstream/main instead of reintroducing the
old deploy-only component. Also brings in groupCardsByPrint (grouped,
stacked-tile rendering) already proven on the deploy branch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@engelben

Copy link
Copy Markdown
Contributor

This may be a challenge for instances hosted behind some other auth mechanism. Right now I am using forward auth via Authentik. I was thinking of adding proper OIDC support but haven't gotten to it yet. This may impact that. Will have a look later this week.

@Git-Romer

Copy link
Copy Markdown
Owner

Hi @hiddenbanana 👋

Thank you very much for this PR! Public profiles and shareable binders are a great addition, and your implementation gave me a strong foundation to build on.

After testing it, I made several follow-up adjustments to align it with the rest of the application:

• Added an admin-controlled global switch, disabled by default.
• Derived public URLs automatically from trainer names.
• Added the public trainer directory at /u.
• Strengthened privacy, validation, collision handling, and wishlist restrictions.
• Polished the public and private binder interfaces, navigation, badges, legends, sorting, mobile layout, and variant-specific card effects.
• Fixed several smaller edge cases discovered during real-browser testing.

The final version passed 276 backend tests, 64 frontend tests, PostgreSQL migration tests, Docker builds, desktop/mobile browser workflows, and two independent review passes.

The PR is now ready from my side. Thanks again for contributing this feature. I really appreciate the time and work you put into it! 🙌

@Git-Romer
Git-Romer merged commit ccae7de into Git-Romer:main Jul 27, 2026
2 checks passed
@hiddenbanana

Copy link
Copy Markdown
Contributor Author

Amazing 🔥

@hiddenbanana
hiddenbanana deleted the feature/public-binders branch July 27, 2026 17:09
@hiddenbanana

Copy link
Copy Markdown
Contributor Author

just pulled 1.26.0 to my deploy... youve done some tweaks i was honestly considering :D the stacking looks awesome and the legend is awesome :D

@Git-Romer

Copy link
Copy Markdown
Owner

@engelben, you were right to flag forward-auth setups here. PokéCollector treats the public routes as anonymous, but Authentik still sees and protects those requests before they reach the app.

I added a follow-up in #315 with the exact routes needed for the public pages, API data, card images, trainer sprites, frontend bundles, and fallback assets. It includes separate Authentik examples for single-application and domain-level forward auth, plus a checklist to confirm that collection, settings, and admin routes remain protected.

This gives existing Authentik installations a safe setup for public profiles. Native OIDC would still be separate work.

@Git-Romer

Copy link
Copy Markdown
Owner

just pulled 1.26.0 to my deploy... youve done some tweaks i was honestly considering :D the stacking looks awesome and the legend is awesome :D

Really happy to hear that 😄 The stacking took a few rounds to get right, but it makes mixed variants much easier to understand without filling the card with more labels. The legend ties the badges together nicely too. Funny that you were already considering some of the same changes. Your original binder implementation made them much easier to add.

@engelben

Copy link
Copy Markdown
Contributor

can only add the same - stacking is a super idea. Also thanks for the forward auth additions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants