- Match 'Fetch Slide' button color to 'Analyze' button (primary variant)
- Expand CSV batch upload docs with column table, required/optional status,
valid values, defaults, and note that the CSV can have any filename
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
fix: pass session_id to telemetry so logged-in users are not reported as Anonymous
On HF Spaces the HF proxy strips custom cookies, so extract_user_info()
could never find the mosaic_auth cookie. Thread the localStorage
session_id through analyze_with_tcga_support → analyze_slides →
analyze_slide → extract_user_info, and prioritise get_user_by_session_id()
(the reliable method) over cookie and OAuthProfile fallbacks.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: show existing slides row when switching to Upload Files mode
switch_input_mode hardcoded visible=not IS_HF_SPACES for the existing
files row, which hid it on HF Spaces. Now always visible in Upload
Files mode since logged-in users need to see it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: move session_id_input definition before first reference
session_id_input was defined after input_slides.upload() which referenced
it, causing UnboundLocalError at startup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: pass session_id to storage/slides functions on HF Spaces
load_storage_usage, load_existing_slides, handle_slide_upload, and
select_existing_slide all called _get_username(request, profile) without
session_id, so on HF Spaces they always got None for the username.
Add session_id parameter to all four functions and pass session_id_input
as a Gradio input. Fold storage usage and existing slides loading into
the main on_page_load handler so they benefit from the gr.skip()
reconnection protection.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: use gr.skip() to prevent reconnection events from clearing login state
Replace the DOM manipulation + session_id_input.change() approach with
demo.load(js=...) plus a hidden "JS ran" flag. The JS returns both the
localStorage session ID and "1" as a flag. On WebSocket reconnections,
Gradio re-fires demo.load() without re-executing JS, so the flag stays
at its default "". The handler detects this and returns gr.skip() to
preserve the current UI state instead of overwriting it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: use session_id_input.change() instead of demo.load() for login state
Replace demo.load(js=...) with gr.Blocks(js=...) + session_id_input.change()
to prevent Gradio WebSocket reconnections from overwriting the logged-in state.
demo.load() fires on both initial page load AND reconnections, but the js=
parameter only executes on the first load — subsequent fires pass empty
values that overwrite the valid login state. The new approach injects JS
via gr.Blocks(js=...) that reads localStorage and sets the hidden textbox
via DOM manipulation, triggering .change() exactly once.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: cache and replay page-load result on Gradio reconnection events
gr.update() with no arguments may reset components to defaults in
Gradio 6, rather than preserving current state. Instead of returning
no-ops on reconnection (when js= doesn't re-execute), cache the last
successful on_page_load result and replay it. This ensures the
logged-in state survives Gradio WebSocket reconnections.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: prevent Gradio reconnection from overwriting logged-in state
Root cause: Gradio fires demo.load() both on initial page load AND on
WebSocket reconnections. The js= parameter only executes on the initial
load; reconnection events pass the component's current value (empty
string). The second empty-session load overwrote the first successful
login with the logged-out state.
Fix: (1) Set the default login_status_md to show the sign-in prompt,
so the UI is correct without any demo.load() running. (2) Return
gr.update() no-ops when session_id is empty, so reconnection events
don't overwrite a valid logged-in state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
debug: add visible diagnostics to on_page_load for session troubleshooting
When session lookup fails, shows debug info (session_id prefix and
active session count) in the sign-in prompt text. Also wraps load_files
and load_results in try/except to prevent silent failures. This helps
diagnose whether the JS localStorage read is working on HF Spaces.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: consolidate page-load into single demo.load() to avoid chaining issues
The .then() chain relied on Gradio updating session_id_input on the
frontend between each step, but the intermediate component value may
not propagate before the next handler reads it. Replaced with a single
on_page_load() function that does everything in one Python call:
check login state + load files + load results. The session_id is
passed as a function argument throughout, with no intermediate
frontend reads. Also persists session_id_input as an output so
subsequent button clicks can read it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: chain demo.load events sequentially for reliable session propagation
Multiple independent demo.load() events with js= parameters fired
simultaneously, causing race conditions where session_id_input wasn't
populated before load_files/load_results handlers ran. Replaced with
a single chained sequence: read localStorage → check_login_state →
load_files → load_results. Also fixed async OAuth tests to use
asyncio.run() instead of pytest-asyncio, and updated callback/logout
tests to match current HTMLResponse-based implementation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: pass localStorage session ID to all user storage handlers
The HF proxy forges OAuthProfile with the Space owner's identity, so
_get_username() was always returning the owner's username for load_files
and load_results even after sign-out. Now:
- _get_username() uses session_id (from localStorage) as sole identity
source on HF Spaces; OAuthProfile fallback removed
- session_id_input textbox created early, populated via demo.load js=
- All user tab handlers (load, refresh, download, delete) receive
session_id_input as explicit input
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: use pure JS logout — clear localStorage and reload page
Previous attempts with Python handler + gr.update() weren't working.
Simplest approach: fn=None, js clears localStorage and calls
location.reload(). On reload, demo.load() sees empty localStorage
and renders logged-out state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: move localStorage clear to .then() chain after logout handler
The js= parameter on the click handler may have been preventing the
Python function from executing. Move localStorage cleanup to a
.then() chain that runs after the Python handler completes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: simplify logout handler — no inputs, just clear localStorage side-effect
The JS block with return statement may have been silently failing.
Simplified: handle_logout() takes no inputs, js= just clears
localStorage as a side effect, Python handler updates the UI.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: use Gradio button for sign-out instead of navigation link
The sign-out link navigated away from the Gradio SPA to a server route,
which returned HTML to clear localStorage and redirect. This page
reload didn't reliably re-trigger demo.load(). Replace with a Gradio
button whose click handler clears localStorage via js= and updates the
UI directly through Gradio's event system.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: use localStorage instead of cookies for session persistence
HF Spaces proxy strips all custom cookies — only the forged `session`
cookie survives. Switch to storing the session ID in the browser's
localStorage via an intermediate HTML page from the callback. The
demo.load() handler reads it back via js= parameter.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: handle Gradio Obj type in cookie lookup for session detection
Gradio's gr.Request wraps cookies/headers through dict_to_obj() which
returns a gradio.route_utils.Obj, not a plain dict. Changed isinstance(dict)
checks to hasattr(get) to support both types. Added debug logging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: remove macOS-only marker from torch override-dependencies
The override-dependencies had `sys_platform == 'darwin'` markers on
torch and torchvision, which told uv to skip installing them on Linux.
This broke Docker builds on HF Spaces where torch comes from
mussel[torch-gpu]. Remove the platform marker so the override applies
everywhere.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: use lifespan handler + server-side session for OAuth on HF Spaces
Mount custom OAuth routes via app_kwargs lifespan protocol so they
survive Gradio recreating the app on launch(). Read visitor identity
from our server-side mosaic_auth cookie instead of gr.OAuthProfile,
which always returns the Space owner on Docker SDK Spaces due to the
forged Starlette session cookie.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: use Gradio built-in OAuth routes instead of custom ones
launch() recreates demo.app from scratch via App.create_app(), discarding
any routes prepended beforehand. Gradio's attach_oauth() registers
/login/huggingface, /login/callback, and /logout inside create_app(),
so they survive the app recreation and have SessionMiddleware active.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: use ASGI middleware for OAuth routes to prevent Gradio 404
On HF Spaces with sdk:docker, Gradio's built-in OAuth injects the Space
owner's identity into every session. This adds custom /api/auth/* routes
that perform the full Authorization Code flow directly with HF's OAuth
provider, writing the correct visitor identity into the Starlette session
so Gradio's OAuthProfile injection works transparently.
- Add src/mosaic/ui/oauth.py with login/callback/logout routes and
server-side session store (24h TTL, mosaic_auth cookie fallback)
- Mount custom OAuth routes on Gradio app when IS_HF_SPACES
- Update login/logout links to /api/auth/login and /api/auth/logout
- Add server-session fallback to extract_user_info() and _get_username()
- Stop hashing usernames in telemetry (use raw HF usernames)
- Add pyproject.toml uv index-strategy for macOS CPU torch compatibility
- Add tests/test_oauth.py with 18 tests covering the full OAuth flow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: clear LoginButton load event on HF Spaces to prevent self-reveal
LoginButton.activate() registers _check_login_status as a load event
that returns a NEW LoginButton with visible=True (default), overriding
our visible=False. The session oauth_info is populated by HF's reverse
proxy with the Space owner's identity, so _check_login_status shows
"Logout (raylim)" to all visitors. Fix: clear load_event_to_attach so
the button stays permanently hidden.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: hide LoginButton on HF Spaces (sdk:docker) and hash usernames in telemetry
With sdk:docker, HF's reverse proxy injects the Space owner's identity
into request.username, so LoginButton shows "Logged in as raylim" for
all visitors. Fix: keep LoginButton hidden on HF Spaces and use custom
Markdown with Gradio's OAuth routes (/login/huggingface, /logout).
Also hash usernames in telemetry events for privacy — callers still pass
raw usernames, hashing happens inside TelemetryTracker.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: stop using request.username on HF Spaces (returns Space owner, not visitor)
On HF Spaces, request.username returns the Space owner's username for
all visitors. This caused every user to appear as "raylim" regardless
of who was actually logged in. Now only OAuthProfile from
gr.LoginButton() is used to identify users on HF Spaces.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: hide LoginButton on initial load in HF Spaces to prevent flash
On HuggingFace Spaces, the LoginButton relies on client-side cookie
detection which can briefly show "Sign in" even when the user is already
authenticated. Hide the button on initial load and reveal it via
demo.load() after the server-side OAuthProfile check confirms the auth
state, so the button renders correctly (login vs logout) without flash.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
docs: add login & user storage section to in-app documentation
Adds a conditionally rendered "Login & User Storage" section to the
Gradio app's Documentation & Help accordion. On HF Spaces it explains
the OAuth login flow and features unlocked after login. In local mode
it describes the debug username and auto-available storage features.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: use OAuthProfile for login state across all UI functions
LoginButton.click() fires before the OAuth redirect so the profile is
never available at that point, and OAuthProfile has no is_authenticated
attribute. Replaced the broken login_button.click() handler with
demo.load() to check login state after OAuth redirect.
Added profile: gr.OAuthProfile parameter to every function that
resolves a username (_get_username, load_storage_usage,
load_existing_slides, handle_slide_upload, select_existing_slide,
load_files, load_results, and all tab action handlers). This mirrors
the working telemetry pattern: try OAuthProfile.username first, fall
back to request.username.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: use login() instead of success() for LoginButton event handler
LoginButton in Gradio 6.0+ does not have a success() method.
Changed to use the correct login() event handler to show user
storage components after successful authentication.
Fixes AttributeError: 'LoginButton' object has no attribute 'success'
fix: hide existing slides selector when TCGA fetch mode is selected
Hide the 'select existing slides' UI element when user switches to 'Fetch from TCGA' mode to avoid UI conflicts. The existing_file_row is now:
- Visible in Upload Files mode (respecting HF Spaces vs local)
- Hidden in Fetch from TCGA mode
This provides a cleaner UX by showing only relevant input options for each mode.
fix: resolve column mismatch error in get_settings function
Removed ihc_subtype parameter from get_settings() function to match
SETTINGS_COLUMNS which has 6 columns (IHC Subtype is commented out).
This fixes ValueError: 6 columns passed, passed data had 7 columns.
Changes:
- Remove ihc_subtype from get_settings() signature
- Remove ihc_subtype from settings list in get_settings()
- Remove ihc_subtype argument from get_settings() call in update_files()
All 410 tests pass.
fix: use OAuthProfile instead of request.username for HF Spaces login
gr.LoginButton() provides user info via gr.OAuthProfile injection,
not request.username (which only works with Gradio's built-in password
auth). Added Optional[gr.OAuthProfile] parameter to event handlers so
Gradio auto-injects the OAuth profile for logged-in users.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: handle AuthenticationMiddleware error when accessing request.username
Fixed issue where accessing request.username raises AuthenticationMiddleware
error on HF Spaces despite user being logged in. Now uses fallback strategies:
1. Try request.username directly (catch middleware errors)
2. Fallback to request.kwargs for username
3. Fallback to request.request.state.user (underlying Starlette request)
This should properly extract logged-in user info from Gradio 6.x OAuth requests.
Fixes telemetry user detection on HuggingFace Spaces.
fix: add diagnostic logging for HF Spaces user authentication
Enhanced extract_user_info() to log request object attributes when username
is None. This will help diagnose why user authentication isn't being detected
on HuggingFace Spaces despite user being logged in.
Logs:
- All public request attributes
- Alternative auth-related attributes (user, user_info, auth, oauth_info)
Related to telemetry user detection issue.
feat: add gradio[oauth] dependency for authentication
OAuth requires additional dependencies (itsdangerous, authlib) that are
not included with base gradio. Changed gradio>=6.0.0 to gradio[oauth]>=6.0.0
to automatically install these dependencies.
This fixes the ImportError:
Cannot initialize OAuth to due a missing library. Please run
`pip install gradio[oauth]`
Note: OAuth is OPTIONAL for users - they can still use the app anonymously.
The LoginButton provides the option to sign in for personalized telemetry
tracking, but does not enforce authentication.
fix: use LoginButton instead of expects_oauth parameter
The expects_oauth is not a parameter for gr.Blocks(). Instead, it's a
property that returns True when a LoginButton component is present.
Fixed by adding gr.LoginButton() to the interface, which:
1. Automatically triggers OAuth when hf_oauth: true is in metadata
2. Shows a 'Sign in with HuggingFace' button on Spaces
3. Is invisible/inactive on local deployments
This fixes the TypeError:
BlockContext.__init__() got an unexpected keyword argument 'expects_oauth'
References:
- https://www.gradio.app/docs/gradio/loginbutton
- Gradio source: blocks.py expects_oauth property checks for LoginButton
To get user information on HuggingFace Spaces with Gradio 6.x, OAuth
must be explicitly enabled. Without this, request.username is always None
even for logged-in users.
Changes:
- README.md: Add hf_oauth: true to Space metadata
- src/mosaic/ui/app.py: Set expects_oauth=True in gr.Blocks()
This enables proper user tracking in telemetry. When users log in to the
HF Space, their username will now be captured in telemetry reports instead
of showing as 'Anonymous'.
Note: OAuth only activates on HuggingFace Spaces. Local deployments are
unaffected.
Reference: https://www.gradio.app/guides/sharing-your-app#oauth-sign-in
fix: use Gradio 6.x built-in username instead of JWT parsing
HuggingFace changed how they pass authentication in Gradio 6.x - they
no longer use __sign= JWT tokens in the referer URL. Instead, Gradio
now provides username directly via request.username attribute.
This is a much simpler and more reliable approach that works with the
current Gradio/HuggingFace Spaces authentication system.
Changes:
- src/mosaic/telemetry/utils.py: Simplified extract_user_info() to use
request.username instead of JWT token parsing
- Removed unused imports (base64, binascii, json)
- Updated documentation to reflect Gradio 6.x approach
- Added debug logging for troubleshooting
Tests:
- tests/telemetry/test_utils.py: Rewrote all JWT-based tests to use
simpler mock request objects with username attribute
- Removed 3 old JWT tests, added 7 new simpler tests
- All 140 telemetry tests passing
This fixes the issue where logged-in users were showing as 'None'
in telemetry reports.
fix: handle None values in telemetry long format reports
Setuptools 70+ removed pkg_resources module which is required by
pytorch-lightning 1.7.7 (transitive dependency). Pinning to <70.0.0
ensures pkg_resources remains available.
Root cause: pytorch-lightning 1.7.7 is being pulled in as a
dependency and requires pkg_resources, but setuptools 82.0.0
no longer provides it.
fix: add echo statement to force Docker layer rebuild
HuggingFace Spaces is still using cached layers from before setuptools
was added to pyproject.toml. This echo statement forces a rebuild of
the dependency installation layer.
fix: force Docker rebuild to install setuptools dependency
Added cache-busting comment to Dockerfile to force HuggingFace Spaces
to rebuild with the updated pyproject.toml that includes setuptools.
This fixes the 'ModuleNotFoundError: No module named pkg_resources'
error that occurs when pytorch_lightning tries to import pkg_resources.
fix: add setuptools to pyproject.toml for Docker builds
The Dockerfile uses 'uv pip install -e .' which installs from
pyproject.toml, not requirements.txt. setuptools is required by
pytorch_lightning for pkg_resources import.
requirements.txt already has setuptools for ZeroGPU deployment.
docs: clarify app.py is for ZeroGPU deployment, not Docker
app.py performs runtime pip installs which is appropriate for ZeroGPU
(Gradio SDK) deployment but not used for Docker SDK deployment.
The Dockerfile handles all dependency installation for Docker deployments.
fix: add lightning to requirements.txt for HuggingFace Spaces
pytorch_lightning (part of lightning package) is imported by paladin's
dependencies and requires pkg_resources from setuptools. Adding both
lightning and setuptools to requirements.txt ensures they are installed
before paladin is installed at runtime.