Spaces:
Running on Zero
Running on Zero
Fix My QR dashboard auth fallback and sizing
Browse files- analytics_supabase_schema.sql +24 -1
- app.py +95 -20
- hf_dashboard.py +20 -8
- modal_service.py +2 -1
- tests-unit/hf_dashboard_test.py +11 -0
- tests-unit/requirements.txt +1 -0
analytics_supabase_schema.sql
CHANGED
|
@@ -5,6 +5,7 @@ create table if not exists public.analytics_generation_events (
|
|
| 5 |
generation_id text not null,
|
| 6 |
timestamp timestamptz not null default now(),
|
| 7 |
product text not null,
|
|
|
|
| 8 |
source text not null check (source in ('ui', 'mcp')),
|
| 9 |
pipeline text not null check (pipeline in ('standard', 'artistic')),
|
| 10 |
tool_name text not null,
|
|
@@ -35,6 +36,9 @@ create index if not exists analytics_generation_events_generation_id_idx
|
|
| 35 |
create index if not exists analytics_generation_events_source_pipeline_idx
|
| 36 |
on public.analytics_generation_events (source, pipeline, timestamp desc);
|
| 37 |
|
|
|
|
|
|
|
|
|
|
| 38 |
alter table public.analytics_generation_events enable row level security;
|
| 39 |
|
| 40 |
revoke all on table public.analytics_generation_events from anon, authenticated;
|
|
@@ -44,6 +48,7 @@ create table if not exists public.analytics_download_events (
|
|
| 44 |
generation_id text,
|
| 45 |
timestamp timestamptz not null default now(),
|
| 46 |
product text not null,
|
|
|
|
| 47 |
source text not null check (source in ('ui', 'mcp')),
|
| 48 |
pipeline text not null,
|
| 49 |
tool_name text not null,
|
|
@@ -61,6 +66,9 @@ create index if not exists analytics_download_events_generation_id_idx
|
|
| 61 |
create index if not exists analytics_download_events_source_pipeline_idx
|
| 62 |
on public.analytics_download_events (source, pipeline, timestamp desc);
|
| 63 |
|
|
|
|
|
|
|
|
|
|
| 64 |
alter table public.analytics_download_events enable row level security;
|
| 65 |
|
| 66 |
revoke all on table public.analytics_download_events from anon, authenticated;
|
|
@@ -70,6 +78,7 @@ create table if not exists public.analytics_validation_events (
|
|
| 70 |
generation_id text not null,
|
| 71 |
timestamp timestamptz not null default now(),
|
| 72 |
product text not null,
|
|
|
|
| 73 |
source text not null check (source in ('ui', 'mcp')),
|
| 74 |
pipeline text not null check (pipeline in ('standard', 'artistic')),
|
| 75 |
tool_name text not null,
|
|
@@ -96,11 +105,15 @@ create table if not exists public.analytics_validation_events (
|
|
| 96 |
create index if not exists analytics_validation_events_source_pipeline_idx
|
| 97 |
on public.analytics_validation_events (source, pipeline, timestamp desc);
|
| 98 |
|
|
|
|
|
|
|
|
|
|
| 99 |
alter table public.analytics_validation_events enable row level security;
|
| 100 |
|
| 101 |
revoke all on table public.analytics_validation_events from anon, authenticated;
|
| 102 |
|
| 103 |
alter table public.analytics_generation_events
|
|
|
|
| 104 |
add column if not exists original_qr_payload_length integer,
|
| 105 |
add column if not exists effective_qr_payload_length integer,
|
| 106 |
add column if not exists scheme_stripped_for_qr boolean not null default false,
|
|
@@ -111,7 +124,11 @@ alter table public.analytics_generation_events
|
|
| 111 |
add column if not exists shortener_chars_saved integer not null default 0,
|
| 112 |
add column if not exists url_chars_saved integer not null default 0;
|
| 113 |
|
|
|
|
|
|
|
|
|
|
| 114 |
alter table public.analytics_validation_events
|
|
|
|
| 115 |
add column if not exists original_qr_payload_length integer,
|
| 116 |
add column if not exists effective_qr_payload_length integer,
|
| 117 |
add column if not exists scheme_stripped_for_qr boolean not null default false,
|
|
@@ -126,6 +143,7 @@ select
|
|
| 126 |
g.generation_id,
|
| 127 |
g.timestamp as generation_timestamp,
|
| 128 |
g.source,
|
|
|
|
| 129 |
g.pipeline,
|
| 130 |
g.analytics_opt_in,
|
| 131 |
g.status,
|
|
@@ -143,6 +161,7 @@ select
|
|
| 143 |
d.generation_id,
|
| 144 |
d.timestamp,
|
| 145 |
d.product,
|
|
|
|
| 146 |
d.source,
|
| 147 |
d.pipeline,
|
| 148 |
d.tool_name,
|
|
@@ -168,6 +187,7 @@ with ordered_generations as (
|
|
| 168 |
g.generation_id,
|
| 169 |
g.timestamp,
|
| 170 |
g.product,
|
|
|
|
| 171 |
g.source,
|
| 172 |
g.pipeline,
|
| 173 |
g.tool_name,
|
|
@@ -180,7 +200,7 @@ with ordered_generations as (
|
|
| 180 |
g.settings_full,
|
| 181 |
g.created_at,
|
| 182 |
lead(g.timestamp) over (
|
| 183 |
-
partition by g.source, g.anonymous_id, g.pipeline
|
| 184 |
order by g.timestamp
|
| 185 |
) as next_generation_timestamp
|
| 186 |
from public.analytics_generation_events g
|
|
@@ -189,6 +209,7 @@ with ordered_generations as (
|
|
| 189 |
g.generation_id,
|
| 190 |
g.timestamp,
|
| 191 |
g.product,
|
|
|
|
| 192 |
g.source,
|
| 193 |
g.pipeline,
|
| 194 |
g.analytics_opt_in,
|
|
@@ -203,6 +224,7 @@ with ordered_generations as (
|
|
| 203 |
select 1
|
| 204 |
from public.analytics_download_events d
|
| 205 |
where d.source = g.source
|
|
|
|
| 206 |
and d.anonymous_id = g.anonymous_id
|
| 207 |
and d.timestamp >= g.timestamp
|
| 208 |
and d.timestamp <= g.timestamp + interval '10 minutes'
|
|
@@ -213,6 +235,7 @@ select
|
|
| 213 |
generation_id,
|
| 214 |
timestamp,
|
| 215 |
product,
|
|
|
|
| 216 |
source,
|
| 217 |
pipeline,
|
| 218 |
analytics_opt_in,
|
|
|
|
| 5 |
generation_id text not null,
|
| 6 |
timestamp timestamptz not null default now(),
|
| 7 |
product text not null,
|
| 8 |
+
caller_app text,
|
| 9 |
source text not null check (source in ('ui', 'mcp')),
|
| 10 |
pipeline text not null check (pipeline in ('standard', 'artistic')),
|
| 11 |
tool_name text not null,
|
|
|
|
| 36 |
create index if not exists analytics_generation_events_source_pipeline_idx
|
| 37 |
on public.analytics_generation_events (source, pipeline, timestamp desc);
|
| 38 |
|
| 39 |
+
create index if not exists analytics_generation_events_caller_app_idx
|
| 40 |
+
on public.analytics_generation_events (caller_app, timestamp desc);
|
| 41 |
+
|
| 42 |
alter table public.analytics_generation_events enable row level security;
|
| 43 |
|
| 44 |
revoke all on table public.analytics_generation_events from anon, authenticated;
|
|
|
|
| 48 |
generation_id text,
|
| 49 |
timestamp timestamptz not null default now(),
|
| 50 |
product text not null,
|
| 51 |
+
caller_app text,
|
| 52 |
source text not null check (source in ('ui', 'mcp')),
|
| 53 |
pipeline text not null,
|
| 54 |
tool_name text not null,
|
|
|
|
| 66 |
create index if not exists analytics_download_events_source_pipeline_idx
|
| 67 |
on public.analytics_download_events (source, pipeline, timestamp desc);
|
| 68 |
|
| 69 |
+
create index if not exists analytics_download_events_caller_app_idx
|
| 70 |
+
on public.analytics_download_events (caller_app, timestamp desc);
|
| 71 |
+
|
| 72 |
alter table public.analytics_download_events enable row level security;
|
| 73 |
|
| 74 |
revoke all on table public.analytics_download_events from anon, authenticated;
|
|
|
|
| 78 |
generation_id text not null,
|
| 79 |
timestamp timestamptz not null default now(),
|
| 80 |
product text not null,
|
| 81 |
+
caller_app text,
|
| 82 |
source text not null check (source in ('ui', 'mcp')),
|
| 83 |
pipeline text not null check (pipeline in ('standard', 'artistic')),
|
| 84 |
tool_name text not null,
|
|
|
|
| 105 |
create index if not exists analytics_validation_events_source_pipeline_idx
|
| 106 |
on public.analytics_validation_events (source, pipeline, timestamp desc);
|
| 107 |
|
| 108 |
+
create index if not exists analytics_validation_events_caller_app_idx
|
| 109 |
+
on public.analytics_validation_events (caller_app, timestamp desc);
|
| 110 |
+
|
| 111 |
alter table public.analytics_validation_events enable row level security;
|
| 112 |
|
| 113 |
revoke all on table public.analytics_validation_events from anon, authenticated;
|
| 114 |
|
| 115 |
alter table public.analytics_generation_events
|
| 116 |
+
add column if not exists caller_app text,
|
| 117 |
add column if not exists original_qr_payload_length integer,
|
| 118 |
add column if not exists effective_qr_payload_length integer,
|
| 119 |
add column if not exists scheme_stripped_for_qr boolean not null default false,
|
|
|
|
| 124 |
add column if not exists shortener_chars_saved integer not null default 0,
|
| 125 |
add column if not exists url_chars_saved integer not null default 0;
|
| 126 |
|
| 127 |
+
alter table public.analytics_download_events
|
| 128 |
+
add column if not exists caller_app text;
|
| 129 |
+
|
| 130 |
alter table public.analytics_validation_events
|
| 131 |
+
add column if not exists caller_app text,
|
| 132 |
add column if not exists original_qr_payload_length integer,
|
| 133 |
add column if not exists effective_qr_payload_length integer,
|
| 134 |
add column if not exists scheme_stripped_for_qr boolean not null default false,
|
|
|
|
| 143 |
g.generation_id,
|
| 144 |
g.timestamp as generation_timestamp,
|
| 145 |
g.source,
|
| 146 |
+
g.caller_app,
|
| 147 |
g.pipeline,
|
| 148 |
g.analytics_opt_in,
|
| 149 |
g.status,
|
|
|
|
| 161 |
d.generation_id,
|
| 162 |
d.timestamp,
|
| 163 |
d.product,
|
| 164 |
+
d.caller_app,
|
| 165 |
d.source,
|
| 166 |
d.pipeline,
|
| 167 |
d.tool_name,
|
|
|
|
| 187 |
g.generation_id,
|
| 188 |
g.timestamp,
|
| 189 |
g.product,
|
| 190 |
+
g.caller_app,
|
| 191 |
g.source,
|
| 192 |
g.pipeline,
|
| 193 |
g.tool_name,
|
|
|
|
| 200 |
g.settings_full,
|
| 201 |
g.created_at,
|
| 202 |
lead(g.timestamp) over (
|
| 203 |
+
partition by g.source, g.anonymous_id, g.pipeline, coalesce(g.caller_app, '')
|
| 204 |
order by g.timestamp
|
| 205 |
) as next_generation_timestamp
|
| 206 |
from public.analytics_generation_events g
|
|
|
|
| 209 |
g.generation_id,
|
| 210 |
g.timestamp,
|
| 211 |
g.product,
|
| 212 |
+
g.caller_app,
|
| 213 |
g.source,
|
| 214 |
g.pipeline,
|
| 215 |
g.analytics_opt_in,
|
|
|
|
| 224 |
select 1
|
| 225 |
from public.analytics_download_events d
|
| 226 |
where d.source = g.source
|
| 227 |
+
and coalesce(d.caller_app, '') = coalesce(g.caller_app, '')
|
| 228 |
and d.anonymous_id = g.anonymous_id
|
| 229 |
and d.timestamp >= g.timestamp
|
| 230 |
and d.timestamp <= g.timestamp + interval '10 minutes'
|
|
|
|
| 235 |
generation_id,
|
| 236 |
timestamp,
|
| 237 |
product,
|
| 238 |
+
caller_app,
|
| 239 |
source,
|
| 240 |
pipeline,
|
| 241 |
analytics_opt_in,
|
app.py
CHANGED
|
@@ -5,6 +5,7 @@ import threading
|
|
| 5 |
import time
|
| 6 |
import traceback
|
| 7 |
import hashlib
|
|
|
|
| 8 |
import uuid
|
| 9 |
from datetime import datetime, timezone
|
| 10 |
from urllib import error as urllib_error
|
|
@@ -351,9 +352,23 @@ def _dashboard_empty_payload(message: str):
|
|
| 351 |
)
|
| 352 |
|
| 353 |
|
| 354 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
try:
|
| 356 |
-
payload = list_my_generations(request)
|
| 357 |
except DashboardError as exc:
|
| 358 |
return _dashboard_empty_payload(f"My QR Codes unavailable: {exc}")
|
| 359 |
except Exception as exc:
|
|
@@ -373,7 +388,7 @@ def _load_my_qr_dashboard(request: Union[gr.Request, None] = None):
|
|
| 373 |
gr.update(value=[]),
|
| 374 |
records,
|
| 375 |
"No saved QR codes yet. Generate one while signed in, then come back here.",
|
| 376 |
-
|
| 377 |
[],
|
| 378 |
[],
|
| 379 |
"{}",
|
|
@@ -386,7 +401,7 @@ def _load_my_qr_dashboard(request: Union[gr.Request, None] = None):
|
|
| 386 |
gr.update(value=gallery),
|
| 387 |
records,
|
| 388 |
"Select a saved QR to inspect analytics and reload settings.",
|
| 389 |
-
|
| 390 |
[],
|
| 391 |
[],
|
| 392 |
"{}",
|
|
@@ -397,13 +412,14 @@ def _load_my_qr_dashboard(request: Union[gr.Request, None] = None):
|
|
| 397 |
|
| 398 |
def _select_my_qr_generation(
|
| 399 |
records: list[dict[str, Any]],
|
|
|
|
| 400 |
evt: gr.SelectData,
|
| 401 |
request: Union[gr.Request, None] = None,
|
| 402 |
):
|
| 403 |
if not records:
|
| 404 |
return (
|
| 405 |
"No saved QR codes are loaded yet.",
|
| 406 |
-
|
| 407 |
[],
|
| 408 |
[],
|
| 409 |
"{}",
|
|
@@ -411,11 +427,11 @@ def _select_my_qr_generation(
|
|
| 411 |
gr.update(visible=False),
|
| 412 |
)
|
| 413 |
try:
|
| 414 |
-
detail = get_generation_detail(request, records, int(evt.index))
|
| 415 |
except DashboardError as exc:
|
| 416 |
return (
|
| 417 |
f"Could not load QR details: {exc}",
|
| 418 |
-
|
| 419 |
[],
|
| 420 |
[],
|
| 421 |
"{}",
|
|
@@ -426,7 +442,7 @@ def _select_my_qr_generation(
|
|
| 426 |
print("[dashboard-select]", traceback.format_exc())
|
| 427 |
return (
|
| 428 |
f"Could not load QR details: {_format_exception_message(exc)}",
|
| 429 |
-
|
| 430 |
[],
|
| 431 |
[],
|
| 432 |
"{}",
|
|
@@ -436,7 +452,7 @@ def _select_my_qr_generation(
|
|
| 436 |
|
| 437 |
return (
|
| 438 |
detail["detail_markdown"],
|
| 439 |
-
|
| 440 |
detail["top_countries_rows"],
|
| 441 |
detail["scans_by_day_rows"],
|
| 442 |
detail["settings_json"],
|
|
@@ -518,7 +534,7 @@ def _maybe_shorten_url_for_qr(
|
|
| 518 |
"error": "shortener is not configured on the server",
|
| 519 |
}
|
| 520 |
|
| 521 |
-
request_body = json.dumps({"url": str(original_input or "")}).encode("utf-8")
|
| 522 |
request_headers = {
|
| 523 |
"Content-Type": "application/json",
|
| 524 |
"X-API-Key": URL_SHORTENER_API_KEY,
|
|
@@ -4903,9 +4919,35 @@ STANDARD_EXAMPLES = [
|
|
| 4903 |
],
|
| 4904 |
]
|
| 4905 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4906 |
# Start your Gradio app with automatic cache cleanup (at module level for hot reload)
|
| 4907 |
# delete_cache=(3600, 3600) means: check every hour and delete files older than 1 hour
|
| 4908 |
-
with gr.Blocks(delete_cache=(3600, 3600)) as demo:
|
| 4909 |
# Add a title and description
|
| 4910 |
gr.Markdown("# QR Code Art Generator")
|
| 4911 |
gr.Markdown("""
|
|
@@ -6613,6 +6655,42 @@ with gr.Blocks(delete_cache=(3600, 3600)) as demo:
|
|
| 6613 |
with gr.Row():
|
| 6614 |
dashboard_login_btn = gr.LoginButton("Sign in with Hugging Face")
|
| 6615 |
dashboard_refresh_btn = gr.Button("Refresh My QR Codes", variant="primary")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6616 |
dashboard_status = gr.Markdown(
|
| 6617 |
"Refresh My QR Codes to load items from your account or this browser session."
|
| 6618 |
)
|
|
@@ -6626,16 +6704,13 @@ with gr.Blocks(delete_cache=(3600, 3600)) as demo:
|
|
| 6626 |
allow_preview=False,
|
| 6627 |
object_fit="contain",
|
| 6628 |
show_download_button=False,
|
|
|
|
| 6629 |
)
|
| 6630 |
with gr.Row():
|
| 6631 |
with gr.Column(scale=1):
|
| 6632 |
-
my_qr_detail_image = gr.
|
| 6633 |
label="Selected QR",
|
| 6634 |
-
|
| 6635 |
-
interactive=False,
|
| 6636 |
-
height=220,
|
| 6637 |
-
width=220,
|
| 6638 |
-
show_download_button=False,
|
| 6639 |
)
|
| 6640 |
with gr.Column(scale=1):
|
| 6641 |
my_qr_detail_markdown = gr.Markdown(
|
|
@@ -6681,7 +6756,7 @@ with gr.Blocks(delete_cache=(3600, 3600)) as demo:
|
|
| 6681 |
|
| 6682 |
dashboard_refresh_btn.click(
|
| 6683 |
fn=_load_my_qr_dashboard,
|
| 6684 |
-
inputs=[],
|
| 6685 |
outputs=[
|
| 6686 |
dashboard_status,
|
| 6687 |
my_qr_gallery,
|
|
@@ -6698,7 +6773,7 @@ with gr.Blocks(delete_cache=(3600, 3600)) as demo:
|
|
| 6698 |
|
| 6699 |
demo.load(
|
| 6700 |
fn=_load_my_qr_dashboard,
|
| 6701 |
-
inputs=[],
|
| 6702 |
outputs=[
|
| 6703 |
dashboard_status,
|
| 6704 |
my_qr_gallery,
|
|
@@ -6715,7 +6790,7 @@ with gr.Blocks(delete_cache=(3600, 3600)) as demo:
|
|
| 6715 |
|
| 6716 |
my_qr_gallery.select(
|
| 6717 |
fn=_select_my_qr_generation,
|
| 6718 |
-
inputs=[my_qr_records_state],
|
| 6719 |
outputs=[
|
| 6720 |
my_qr_detail_markdown,
|
| 6721 |
my_qr_detail_image,
|
|
|
|
| 5 |
import time
|
| 6 |
import traceback
|
| 7 |
import hashlib
|
| 8 |
+
import html
|
| 9 |
import uuid
|
| 10 |
from datetime import datetime, timezone
|
| 11 |
from urllib import error as urllib_error
|
|
|
|
| 352 |
)
|
| 353 |
|
| 354 |
|
| 355 |
+
def _render_dashboard_image_html(image_url: str | None) -> str:
|
| 356 |
+
if not image_url:
|
| 357 |
+
return ""
|
| 358 |
+
safe_url = html.escape(str(image_url), quote=True)
|
| 359 |
+
return (
|
| 360 |
+
'<div class="my-qr-detail">'
|
| 361 |
+
f'<img src="{safe_url}" alt="Selected QR" />'
|
| 362 |
+
"</div>"
|
| 363 |
+
)
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
def _load_my_qr_dashboard(
|
| 367 |
+
username_hint: str = "",
|
| 368 |
+
request: Union[gr.Request, None] = None,
|
| 369 |
+
):
|
| 370 |
try:
|
| 371 |
+
payload = list_my_generations(request, username_hint=username_hint)
|
| 372 |
except DashboardError as exc:
|
| 373 |
return _dashboard_empty_payload(f"My QR Codes unavailable: {exc}")
|
| 374 |
except Exception as exc:
|
|
|
|
| 388 |
gr.update(value=[]),
|
| 389 |
records,
|
| 390 |
"No saved QR codes yet. Generate one while signed in, then come back here.",
|
| 391 |
+
"",
|
| 392 |
[],
|
| 393 |
[],
|
| 394 |
"{}",
|
|
|
|
| 401 |
gr.update(value=gallery),
|
| 402 |
records,
|
| 403 |
"Select a saved QR to inspect analytics and reload settings.",
|
| 404 |
+
"",
|
| 405 |
[],
|
| 406 |
[],
|
| 407 |
"{}",
|
|
|
|
| 412 |
|
| 413 |
def _select_my_qr_generation(
|
| 414 |
records: list[dict[str, Any]],
|
| 415 |
+
username_hint: str,
|
| 416 |
evt: gr.SelectData,
|
| 417 |
request: Union[gr.Request, None] = None,
|
| 418 |
):
|
| 419 |
if not records:
|
| 420 |
return (
|
| 421 |
"No saved QR codes are loaded yet.",
|
| 422 |
+
"",
|
| 423 |
[],
|
| 424 |
[],
|
| 425 |
"{}",
|
|
|
|
| 427 |
gr.update(visible=False),
|
| 428 |
)
|
| 429 |
try:
|
| 430 |
+
detail = get_generation_detail(request, records, int(evt.index), username_hint=username_hint)
|
| 431 |
except DashboardError as exc:
|
| 432 |
return (
|
| 433 |
f"Could not load QR details: {exc}",
|
| 434 |
+
"",
|
| 435 |
[],
|
| 436 |
[],
|
| 437 |
"{}",
|
|
|
|
| 442 |
print("[dashboard-select]", traceback.format_exc())
|
| 443 |
return (
|
| 444 |
f"Could not load QR details: {_format_exception_message(exc)}",
|
| 445 |
+
"",
|
| 446 |
[],
|
| 447 |
[],
|
| 448 |
"{}",
|
|
|
|
| 452 |
|
| 453 |
return (
|
| 454 |
detail["detail_markdown"],
|
| 455 |
+
_render_dashboard_image_html(detail["image"]),
|
| 456 |
detail["top_countries_rows"],
|
| 457 |
detail["scans_by_day_rows"],
|
| 458 |
detail["settings_json"],
|
|
|
|
| 534 |
"error": "shortener is not configured on the server",
|
| 535 |
}
|
| 536 |
|
| 537 |
+
request_body = json.dumps({"url": str(original_input or ""), "reuse_existing": False}).encode("utf-8")
|
| 538 |
request_headers = {
|
| 539 |
"Content-Type": "application/json",
|
| 540 |
"X-API-Key": URL_SHORTENER_API_KEY,
|
|
|
|
| 4919 |
],
|
| 4920 |
]
|
| 4921 |
|
| 4922 |
+
DASHBOARD_CSS = """
|
| 4923 |
+
.dashboard-hidden-username {
|
| 4924 |
+
display: none !important;
|
| 4925 |
+
}
|
| 4926 |
+
.my-qr-gallery {
|
| 4927 |
+
min-height: 120px !important;
|
| 4928 |
+
}
|
| 4929 |
+
.my-qr-gallery img {
|
| 4930 |
+
max-height: 96px !important;
|
| 4931 |
+
object-fit: contain !important;
|
| 4932 |
+
}
|
| 4933 |
+
.my-qr-detail {
|
| 4934 |
+
display: flex;
|
| 4935 |
+
align-items: center;
|
| 4936 |
+
justify-content: center;
|
| 4937 |
+
min-height: 220px;
|
| 4938 |
+
}
|
| 4939 |
+
.my-qr-detail img {
|
| 4940 |
+
width: 220px;
|
| 4941 |
+
max-width: 220px;
|
| 4942 |
+
max-height: 220px;
|
| 4943 |
+
object-fit: contain;
|
| 4944 |
+
border-radius: 12px;
|
| 4945 |
+
}
|
| 4946 |
+
"""
|
| 4947 |
+
|
| 4948 |
# Start your Gradio app with automatic cache cleanup (at module level for hot reload)
|
| 4949 |
# delete_cache=(3600, 3600) means: check every hour and delete files older than 1 hour
|
| 4950 |
+
with gr.Blocks(delete_cache=(3600, 3600), css=DASHBOARD_CSS) as demo:
|
| 4951 |
# Add a title and description
|
| 4952 |
gr.Markdown("# QR Code Art Generator")
|
| 4953 |
gr.Markdown("""
|
|
|
|
| 6655 |
with gr.Row():
|
| 6656 |
dashboard_login_btn = gr.LoginButton("Sign in with Hugging Face")
|
| 6657 |
dashboard_refresh_btn = gr.Button("Refresh My QR Codes", variant="primary")
|
| 6658 |
+
dashboard_username_hint = gr.Textbox(
|
| 6659 |
+
value="",
|
| 6660 |
+
visible=True,
|
| 6661 |
+
elem_id="dashboard-username-hint",
|
| 6662 |
+
elem_classes=["dashboard-hidden-username"],
|
| 6663 |
+
label="dashboard_username_hint",
|
| 6664 |
+
)
|
| 6665 |
+
gr.HTML(
|
| 6666 |
+
"""
|
| 6667 |
+
<script>
|
| 6668 |
+
(() => {
|
| 6669 |
+
const syncDashboardUsernameHint = () => {
|
| 6670 |
+
const input = document.querySelector('#dashboard-username-hint textarea, #dashboard-username-hint input');
|
| 6671 |
+
if (!input) return;
|
| 6672 |
+
const buttons = Array.from(document.querySelectorAll('button, [role="button"]'));
|
| 6673 |
+
let username = '';
|
| 6674 |
+
for (const button of buttons) {
|
| 6675 |
+
const text = (button.innerText || button.textContent || '').trim();
|
| 6676 |
+
const match = text.match(/Logout\s*\(([^)]+)\)/i);
|
| 6677 |
+
if (match) {
|
| 6678 |
+
username = match[1].trim();
|
| 6679 |
+
break;
|
| 6680 |
+
}
|
| 6681 |
+
}
|
| 6682 |
+
if ((input.value || '') !== username) {
|
| 6683 |
+
input.value = username;
|
| 6684 |
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
| 6685 |
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
| 6686 |
+
}
|
| 6687 |
+
};
|
| 6688 |
+
syncDashboardUsernameHint();
|
| 6689 |
+
window.setInterval(syncDashboardUsernameHint, 1500);
|
| 6690 |
+
})();
|
| 6691 |
+
</script>
|
| 6692 |
+
"""
|
| 6693 |
+
)
|
| 6694 |
dashboard_status = gr.Markdown(
|
| 6695 |
"Refresh My QR Codes to load items from your account or this browser session."
|
| 6696 |
)
|
|
|
|
| 6704 |
allow_preview=False,
|
| 6705 |
object_fit="contain",
|
| 6706 |
show_download_button=False,
|
| 6707 |
+
elem_classes=["my-qr-gallery"],
|
| 6708 |
)
|
| 6709 |
with gr.Row():
|
| 6710 |
with gr.Column(scale=1):
|
| 6711 |
+
my_qr_detail_image = gr.HTML(
|
| 6712 |
label="Selected QR",
|
| 6713 |
+
value="",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6714 |
)
|
| 6715 |
with gr.Column(scale=1):
|
| 6716 |
my_qr_detail_markdown = gr.Markdown(
|
|
|
|
| 6756 |
|
| 6757 |
dashboard_refresh_btn.click(
|
| 6758 |
fn=_load_my_qr_dashboard,
|
| 6759 |
+
inputs=[dashboard_username_hint],
|
| 6760 |
outputs=[
|
| 6761 |
dashboard_status,
|
| 6762 |
my_qr_gallery,
|
|
|
|
| 6773 |
|
| 6774 |
demo.load(
|
| 6775 |
fn=_load_my_qr_dashboard,
|
| 6776 |
+
inputs=[dashboard_username_hint],
|
| 6777 |
outputs=[
|
| 6778 |
dashboard_status,
|
| 6779 |
my_qr_gallery,
|
|
|
|
| 6790 |
|
| 6791 |
my_qr_gallery.select(
|
| 6792 |
fn=_select_my_qr_generation,
|
| 6793 |
+
inputs=[my_qr_records_state, dashboard_username_hint],
|
| 6794 |
outputs=[
|
| 6795 |
my_qr_detail_markdown,
|
| 6796 |
my_qr_detail_image,
|
hf_dashboard.py
CHANGED
|
@@ -272,11 +272,12 @@ def _build_browser_fingerprint(request: Any | None, source: str = "ui") -> str |
|
|
| 272 |
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:24]
|
| 273 |
|
| 274 |
|
| 275 |
-
def get_current_user_context(request: Any | None) -> dict[str, Any] | None:
|
| 276 |
oauth_info = _session_oauth_info(request)
|
| 277 |
raw_userinfo = oauth_info.get("userinfo")
|
| 278 |
userinfo: Mapping[str, Any] = raw_userinfo if isinstance(raw_userinfo, Mapping) else {}
|
| 279 |
-
|
|
|
|
| 280 |
if not username:
|
| 281 |
return None
|
| 282 |
|
|
@@ -301,8 +302,8 @@ def get_current_user_context(request: Any | None) -> dict[str, Any] | None:
|
|
| 301 |
}
|
| 302 |
|
| 303 |
|
| 304 |
-
def ensure_app_user_for_request(request: Any | None) -> dict[str, Any] | None:
|
| 305 |
-
user = get_current_user_context(request)
|
| 306 |
if user is None:
|
| 307 |
return None
|
| 308 |
|
|
@@ -553,8 +554,14 @@ def persist_generation(
|
|
| 553 |
return "Saved this QR in the current browser session. Open My QR Codes while signed in to claim it."
|
| 554 |
|
| 555 |
|
| 556 |
-
def
|
| 557 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 558 |
if user is None:
|
| 559 |
session_hash = str(getattr(request, "session_hash", "") or "").strip() if request is not None else ""
|
| 560 |
browser_fingerprint = _build_browser_fingerprint(request)
|
|
@@ -702,12 +709,17 @@ def _request_matches_record_session(request: Any | None, record: Mapping[str, An
|
|
| 702 |
return False
|
| 703 |
|
| 704 |
|
| 705 |
-
def get_generation_detail(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 706 |
if index < 0 or index >= len(records):
|
| 707 |
raise DashboardError("Selected QR code is out of range.")
|
| 708 |
|
| 709 |
record = records[index]
|
| 710 |
-
user =
|
| 711 |
if user is None:
|
| 712 |
if not _request_matches_record_session(request, record):
|
| 713 |
raise DashboardError("Please sign in first.")
|
|
|
|
| 272 |
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:24]
|
| 273 |
|
| 274 |
|
| 275 |
+
def get_current_user_context(request: Any | None, username_hint: str | None = None) -> dict[str, Any] | None:
|
| 276 |
oauth_info = _session_oauth_info(request)
|
| 277 |
raw_userinfo = oauth_info.get("userinfo")
|
| 278 |
userinfo: Mapping[str, Any] = raw_userinfo if isinstance(raw_userinfo, Mapping) else {}
|
| 279 |
+
hinted_username = str(username_hint or "").strip()
|
| 280 |
+
username = str(userinfo.get("preferred_username") or getattr(request, "username", "") or hinted_username or "").strip()
|
| 281 |
if not username:
|
| 282 |
return None
|
| 283 |
|
|
|
|
| 302 |
}
|
| 303 |
|
| 304 |
|
| 305 |
+
def ensure_app_user_for_request(request: Any | None, username_hint: str | None = None) -> dict[str, Any] | None:
|
| 306 |
+
user = get_current_user_context(request, username_hint=username_hint)
|
| 307 |
if user is None:
|
| 308 |
return None
|
| 309 |
|
|
|
|
| 554 |
return "Saved this QR in the current browser session. Open My QR Codes while signed in to claim it."
|
| 555 |
|
| 556 |
|
| 557 |
+
def _ensure_user_for_request_compat(request: Any | None, username_hint: str | None = None) -> dict[str, Any] | None:
|
| 558 |
+
if username_hint:
|
| 559 |
+
return ensure_app_user_for_request(request, username_hint=username_hint)
|
| 560 |
+
return ensure_app_user_for_request(request)
|
| 561 |
+
|
| 562 |
+
|
| 563 |
+
def list_my_generations(request: Any | None, username_hint: str | None = None) -> dict[str, Any]:
|
| 564 |
+
user = _ensure_user_for_request_compat(request, username_hint=username_hint)
|
| 565 |
if user is None:
|
| 566 |
session_hash = str(getattr(request, "session_hash", "") or "").strip() if request is not None else ""
|
| 567 |
browser_fingerprint = _build_browser_fingerprint(request)
|
|
|
|
| 709 |
return False
|
| 710 |
|
| 711 |
|
| 712 |
+
def get_generation_detail(
|
| 713 |
+
request: Any | None,
|
| 714 |
+
records: list[dict[str, Any]],
|
| 715 |
+
index: int,
|
| 716 |
+
username_hint: str | None = None,
|
| 717 |
+
) -> dict[str, Any]:
|
| 718 |
if index < 0 or index >= len(records):
|
| 719 |
raise DashboardError("Selected QR code is out of range.")
|
| 720 |
|
| 721 |
record = records[index]
|
| 722 |
+
user = _ensure_user_for_request_compat(request, username_hint=username_hint)
|
| 723 |
if user is None:
|
| 724 |
if not _request_matches_record_session(request, record):
|
| 725 |
raise DashboardError("Please sign in first.")
|
modal_service.py
CHANGED
|
@@ -56,6 +56,7 @@ except ModuleNotFoundError:
|
|
| 56 |
|
| 57 |
|
| 58 |
APP_NAME = os.environ.get("MODAL_APP_NAME", "ai-qr-code-generator-api")
|
|
|
|
| 59 |
GPU = os.environ.get("MODAL_GPU", "A100-40GB")
|
| 60 |
TIMEOUT_SECONDS = int(os.environ.get("MODAL_TIMEOUT_SECONDS", "1800"))
|
| 61 |
SCALEDOWN_WINDOW = int(os.environ.get("MODAL_SCALEDOWN_WINDOW", "300"))
|
|
@@ -96,7 +97,7 @@ image = (
|
|
| 96 |
volumes={"/root/app/models": volume},
|
| 97 |
secrets=[modal.Secret.from_name("huggingface-token"), runtime_secret],
|
| 98 |
)
|
| 99 |
-
@modal.asgi_app()
|
| 100 |
def api():
|
| 101 |
from fastapi import FastAPI, HTTPException, Request
|
| 102 |
from fastapi.concurrency import run_in_threadpool
|
|
|
|
| 56 |
|
| 57 |
|
| 58 |
APP_NAME = os.environ.get("MODAL_APP_NAME", "ai-qr-code-generator-api")
|
| 59 |
+
LEGACY_WEB_LABEL = os.environ.get("MODAL_WEB_LABEL", APP_NAME)
|
| 60 |
GPU = os.environ.get("MODAL_GPU", "A100-40GB")
|
| 61 |
TIMEOUT_SECONDS = int(os.environ.get("MODAL_TIMEOUT_SECONDS", "1800"))
|
| 62 |
SCALEDOWN_WINDOW = int(os.environ.get("MODAL_SCALEDOWN_WINDOW", "300"))
|
|
|
|
| 97 |
volumes={"/root/app/models": volume},
|
| 98 |
secrets=[modal.Secret.from_name("huggingface-token"), runtime_secret],
|
| 99 |
)
|
| 100 |
+
@modal.asgi_app(label=LEGACY_WEB_LABEL)
|
| 101 |
def api():
|
| 102 |
from fastapi import FastAPI, HTTPException, Request
|
| 103 |
from fastapi.concurrency import run_in_threadpool
|
tests-unit/hf_dashboard_test.py
CHANGED
|
@@ -39,6 +39,17 @@ def test_get_current_user_context_reads_hf_oauth_session() -> None:
|
|
| 39 |
assert "access_token" not in user["raw_profile"]["oauth_info"]
|
| 40 |
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
def test_list_my_generations_requires_sign_in(monkeypatch) -> None:
|
| 43 |
monkeypatch.setattr(hf_dashboard, "ensure_app_user_for_request", lambda request: None)
|
| 44 |
|
|
|
|
| 39 |
assert "access_token" not in user["raw_profile"]["oauth_info"]
|
| 40 |
|
| 41 |
|
| 42 |
+
def test_get_current_user_context_uses_username_hint_without_request_username() -> None:
|
| 43 |
+
request = FakeRequest(username="", oauth_info={})
|
| 44 |
+
|
| 45 |
+
user = hf_dashboard.get_current_user_context(request, username_hint="Oysiyl")
|
| 46 |
+
|
| 47 |
+
assert user is not None
|
| 48 |
+
assert user["provider"] == "huggingface"
|
| 49 |
+
assert user["provider_subject"] == "hf-username:oysiyl"
|
| 50 |
+
assert user["username"] == "Oysiyl"
|
| 51 |
+
|
| 52 |
+
|
| 53 |
def test_list_my_generations_requires_sign_in(monkeypatch) -> None:
|
| 54 |
monkeypatch.setattr(hf_dashboard, "ensure_app_user_for_request", lambda request: None)
|
| 55 |
|
tests-unit/requirements.txt
CHANGED
|
@@ -1,3 +1,4 @@
|
|
| 1 |
pytest>=7.8.0
|
| 2 |
pytest-aiohttp
|
| 3 |
pytest-asyncio
|
|
|
|
|
|
| 1 |
pytest>=7.8.0
|
| 2 |
pytest-aiohttp
|
| 3 |
pytest-asyncio
|
| 4 |
+
Pillow
|