Oysiyl commited on
Commit
293dd62
·
1 Parent(s): cc65da2

Fix My QR dashboard auth fallback and sizing

Browse files
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 _load_my_qr_dashboard(request: Union[gr.Request, None] = None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- gr.update(value=None, visible=False),
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
- gr.update(value=None, visible=False),
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
- gr.update(value=None, visible=False),
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
- gr.update(value=None, visible=False),
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
- gr.update(value=None, visible=False),
430
  [],
431
  [],
432
  "{}",
@@ -436,7 +452,7 @@ def _select_my_qr_generation(
436
 
437
  return (
438
  detail["detail_markdown"],
439
- gr.update(value=detail["image"], visible=bool(detail["image"])),
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.Image(
6633
  label="Selected QR",
6634
- visible=False,
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
- username = str(userinfo.get("preferred_username") or getattr(request, "username", "") or "").strip()
 
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 list_my_generations(request: Any | None) -> dict[str, Any]:
557
- user = ensure_app_user_for_request(request)
 
 
 
 
 
 
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(request: Any | None, records: list[dict[str, Any]], index: int) -> dict[str, Any]:
 
 
 
 
 
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 = ensure_app_user_for_request(request)
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