Hotfix: expand landing-page search to all requests
This commit is contained in:
@@ -1 +1 @@
|
|||||||
0203262044
|
0303261323
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
BUILD_NUMBER = "0203262044"
|
BUILD_NUMBER = "0303261323"
|
||||||
CHANGELOG = '2026-03-02|Process 1 build 0203261953\n2026-03-02|Process 1 build 0203261610\n2026-03-02|Process 1 build 0203261608\n2026-03-02|Add dedicated profile invites page and fix mobile admin layout\n2026-03-01|Persist Seerr media failure suppression and reduce sync error noise\n2026-03-01|Add repository line ending policy\n2026-03-01|Finalize diagnostics, logging controls, and email test support\n2026-03-01|Add invite email templates and delivery workflow\n2026-02-28|Finalize dev-1.3 upgrades and Seerr updates\n2026-02-27|admin docs and layout refresh, build 2702261314\n2026-02-27|Build 2702261153: fix jellyfin sync user visibility\n2026-02-26|Build 2602262241: live request page updates\n2026-02-26|Build 2602262204\n2026-02-26|Build 2602262159: restore jellyfin-first user source\n2026-02-26|Build 2602262049: split magent settings and harden local login\n2026-02-26|Build 2602262030: add magent settings and hardening\n2026-02-26|Build 2602261731: fix user resync after nuclear wipe\n2026-02-26|Build 2602261717: master invite policy and self-service invite controls\n2026-02-26|Build 2602261636: self-service invites and count fixes\n2026-02-26|Build 2602261605: invite trace and cross-system user lifecycle\n2026-02-26|Build 2602261536: refine invite layouts and tighten UI\n2026-02-26|Build 2602261523: live updates, invite cleanup and nuclear resync\n2026-02-26|Build 2602261442: tidy users and invite layouts\n2026-02-26|Build 2602261409: unify invite management controls\n2026-02-26|Build 2602260214: invites profiles and expiry admin controls\n2026-02-26|Build 2602260022: enterprise UI refresh and users bulk auto-search\n2026-02-25|Build 2502262321: fix auto-search quality and per-user toggle\n2026-02-02|Build 0202261541: allow FQDN service URLs\n2026-01-30|Build 3001262148: single container\n2026-01-29|Build 2901262244: format changelog\n2026-01-29|Build 2901262240: cache users\n2026-01-29|Tidy full changelog\n2026-01-29|Update full changelog\n2026-01-29|Bake build number and changelog\n2026-01-29|Hardcode build number in backend\n2026-01-29|release: 2901262102\n2026-01-29|release: 2901262044\n2026-01-29|release: 2901262036\n2026-01-27|Hydrate missing artwork from Jellyseerr (build 271261539)\n2026-01-27|Fallback to TMDB when artwork cache fails (build 271261524)\n2026-01-27|Add service test buttons (build 271261335)\n2026-01-27|Bump build number (process 2) 271261322\n2026-01-27|Add cache load spinner (build 271261238)\n2026-01-27|Fix snapshot title fallback (build 271261228)\n2026-01-27|Fix request titles in snapshots (build 271261219)\n2026-01-27|Bump build number to 271261202\n2026-01-27|Clarify request sync settings (build 271261159)\n2026-01-27|Fix backend cache stats import (build 271261149)\n2026-01-27|Improve cache stats performance (build 271261145)\n2026-01-27|Add cache control artwork stats\n2026-01-26|Fix sync progress bar animation\n2026-01-26|Fix cache title hydration\n2026-01-25|Build 2501262041\n2026-01-25|Harden request cache titles and cache-only reads\n2026-01-25|Serve bundled branding assets by default\n2026-01-25|Seed branding logo from bundled assets\n2026-01-25|Tidy request sync controls\n2026-01-25|Add Jellyfin login cache and admin-only stats\n2026-01-25|Add user stats and activity tracking\n2026-01-25|Move account actions into avatar menu\n2026-01-25|Improve mobile header layout\n2026-01-25|Automate build number tagging and sync\n2026-01-25|Add site banner, build number, and changelog\n2026-01-24|Improve request handling and qBittorrent categories\n2026-01-24|Map Prowlarr releases to Arr indexers for manual grab\n2026-01-24|Clarify how-it-works steps and fixes\n2026-01-24|Document fix buttons in how-it-works\n2026-01-24|Route grabs through Sonarr/Radarr only\n2026-01-23|Use backend branding assets for logo and favicon\n2026-01-23|Copy public assets into frontend image\n2026-01-23|Fix backend Dockerfile paths for root context\n2026-01-23|Add Docker Hub compose override\n2026-01-23|Remove password fields from users page\n2026-01-23|Use bundled branding assets\n2026-01-23|Add default branding assets when missing\n2026-01-23|Show available status on landing when in Jellyfin\n2026-01-23|Fix cache titles and move feedback link\n2026-01-23|Add feedback form and webhook\n2026-01-23|Hide header actions when signed out\n2026-01-23|Fallback manual grab to qBittorrent\n2026-01-23|Split search actions and improve download options\n2026-01-23|Fix cache titles via Jellyseerr media lookup\n2026-01-22|Update README with Docker-first guide\n2026-01-22|Update README\n2026-01-22|Ignore build artifacts\n2026-01-22|Initial commit'
|
CHANGELOG = '2026-03-02|Hotfix: add logged-out password reset flow\n2026-03-02|Process 1 build 0203261953\n2026-03-02|Process 1 build 0203261610\n2026-03-02|Process 1 build 0203261608\n2026-03-02|Add dedicated profile invites page and fix mobile admin layout\n2026-03-01|Persist Seerr media failure suppression and reduce sync error noise\n2026-03-01|Add repository line ending policy\n2026-03-01|Finalize diagnostics, logging controls, and email test support\n2026-03-01|Add invite email templates and delivery workflow\n2026-02-28|Finalize dev-1.3 upgrades and Seerr updates\n2026-02-27|admin docs and layout refresh, build 2702261314\n2026-02-27|Build 2702261153: fix jellyfin sync user visibility\n2026-02-26|Build 2602262241: live request page updates\n2026-02-26|Build 2602262204\n2026-02-26|Build 2602262159: restore jellyfin-first user source\n2026-02-26|Build 2602262049: split magent settings and harden local login\n2026-02-26|Build 2602262030: add magent settings and hardening\n2026-02-26|Build 2602261731: fix user resync after nuclear wipe\n2026-02-26|Build 2602261717: master invite policy and self-service invite controls\n2026-02-26|Build 2602261636: self-service invites and count fixes\n2026-02-26|Build 2602261605: invite trace and cross-system user lifecycle\n2026-02-26|Build 2602261536: refine invite layouts and tighten UI\n2026-02-26|Build 2602261523: live updates, invite cleanup and nuclear resync\n2026-02-26|Build 2602261442: tidy users and invite layouts\n2026-02-26|Build 2602261409: unify invite management controls\n2026-02-26|Build 2602260214: invites profiles and expiry admin controls\n2026-02-26|Build 2602260022: enterprise UI refresh and users bulk auto-search\n2026-02-25|Build 2502262321: fix auto-search quality and per-user toggle\n2026-02-02|Build 0202261541: allow FQDN service URLs\n2026-01-30|Build 3001262148: single container\n2026-01-29|Build 2901262244: format changelog\n2026-01-29|Build 2901262240: cache users\n2026-01-29|Tidy full changelog\n2026-01-29|Update full changelog\n2026-01-29|Bake build number and changelog\n2026-01-29|Hardcode build number in backend\n2026-01-29|release: 2901262102\n2026-01-29|release: 2901262044\n2026-01-29|release: 2901262036\n2026-01-27|Hydrate missing artwork from Jellyseerr (build 271261539)\n2026-01-27|Fallback to TMDB when artwork cache fails (build 271261524)\n2026-01-27|Add service test buttons (build 271261335)\n2026-01-27|Bump build number (process 2) 271261322\n2026-01-27|Add cache load spinner (build 271261238)\n2026-01-27|Fix snapshot title fallback (build 271261228)\n2026-01-27|Fix request titles in snapshots (build 271261219)\n2026-01-27|Bump build number to 271261202\n2026-01-27|Clarify request sync settings (build 271261159)\n2026-01-27|Fix backend cache stats import (build 271261149)\n2026-01-27|Improve cache stats performance (build 271261145)\n2026-01-27|Add cache control artwork stats\n2026-01-26|Fix sync progress bar animation\n2026-01-26|Fix cache title hydration\n2026-01-25|Build 2501262041\n2026-01-25|Harden request cache titles and cache-only reads\n2026-01-25|Serve bundled branding assets by default\n2026-01-25|Seed branding logo from bundled assets\n2026-01-25|Tidy request sync controls\n2026-01-25|Add Jellyfin login cache and admin-only stats\n2026-01-25|Add user stats and activity tracking\n2026-01-25|Move account actions into avatar menu\n2026-01-25|Improve mobile header layout\n2026-01-25|Automate build number tagging and sync\n2026-01-25|Add site banner, build number, and changelog\n2026-01-24|Improve request handling and qBittorrent categories\n2026-01-24|Map Prowlarr releases to Arr indexers for manual grab\n2026-01-24|Clarify how-it-works steps and fixes\n2026-01-24|Document fix buttons in how-it-works\n2026-01-24|Route grabs through Sonarr/Radarr only\n2026-01-23|Use backend branding assets for logo and favicon\n2026-01-23|Copy public assets into frontend image\n2026-01-23|Fix backend Dockerfile paths for root context\n2026-01-23|Add Docker Hub compose override\n2026-01-23|Remove password fields from users page\n2026-01-23|Use bundled branding assets\n2026-01-23|Add default branding assets when missing\n2026-01-23|Show available status on landing when in Jellyfin\n2026-01-23|Fix cache titles and move feedback link\n2026-01-23|Add feedback form and webhook\n2026-01-23|Hide header actions when signed out\n2026-01-23|Fallback manual grab to qBittorrent\n2026-01-23|Split search actions and improve download options\n2026-01-23|Fix cache titles via Jellyseerr media lookup\n2026-01-22|Update README with Docker-first guide\n2026-01-22|Update README\n2026-01-22|Ignore build artifacts\n2026-01-22|Initial commit'
|
||||||
|
|||||||
@@ -1841,6 +1841,7 @@ def get_cached_requests(
|
|||||||
requested_by_norm: Optional[str] = None,
|
requested_by_norm: Optional[str] = None,
|
||||||
requested_by_id: Optional[int] = None,
|
requested_by_id: Optional[int] = None,
|
||||||
since_iso: Optional[str] = None,
|
since_iso: Optional[str] = None,
|
||||||
|
status_codes: Optional[list[int]] = None,
|
||||||
) -> list[Dict[str, Any]]:
|
) -> list[Dict[str, Any]]:
|
||||||
query = """
|
query = """
|
||||||
SELECT request_id, media_id, media_type, status, title, year, requested_by,
|
SELECT request_id, media_id, media_type, status, title, year, requested_by,
|
||||||
@@ -1858,6 +1859,10 @@ def get_cached_requests(
|
|||||||
if since_iso:
|
if since_iso:
|
||||||
conditions.append("created_at >= ?")
|
conditions.append("created_at >= ?")
|
||||||
params.append(since_iso)
|
params.append(since_iso)
|
||||||
|
if status_codes:
|
||||||
|
placeholders = ", ".join("?" for _ in status_codes)
|
||||||
|
conditions.append(f"status IN ({placeholders})")
|
||||||
|
params.extend(status_codes)
|
||||||
if conditions:
|
if conditions:
|
||||||
query += " WHERE " + " AND ".join(conditions)
|
query += " WHERE " + " AND ".join(conditions)
|
||||||
query += " ORDER BY created_at DESC, request_id DESC LIMIT ? OFFSET ?"
|
query += " ORDER BY created_at DESC, request_id DESC LIMIT ? OFFSET ?"
|
||||||
@@ -1865,11 +1870,12 @@ def get_cached_requests(
|
|||||||
with _connect() as conn:
|
with _connect() as conn:
|
||||||
rows = conn.execute(query, tuple(params)).fetchall()
|
rows = conn.execute(query, tuple(params)).fetchall()
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"requests_cache list: count=%s requested_by_norm=%s requested_by_id=%s since_iso=%s",
|
"requests_cache list: count=%s requested_by_norm=%s requested_by_id=%s since_iso=%s status_codes=%s",
|
||||||
len(rows),
|
len(rows),
|
||||||
requested_by_norm,
|
requested_by_norm,
|
||||||
requested_by_id,
|
requested_by_id,
|
||||||
since_iso,
|
since_iso,
|
||||||
|
status_codes,
|
||||||
)
|
)
|
||||||
results: list[Dict[str, Any]] = []
|
results: list[Dict[str, Any]] = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
@@ -1903,6 +1909,7 @@ def get_cached_requests_count(
|
|||||||
requested_by_norm: Optional[str] = None,
|
requested_by_norm: Optional[str] = None,
|
||||||
requested_by_id: Optional[int] = None,
|
requested_by_id: Optional[int] = None,
|
||||||
since_iso: Optional[str] = None,
|
since_iso: Optional[str] = None,
|
||||||
|
status_codes: Optional[list[int]] = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
query = "SELECT COUNT(*) FROM requests_cache"
|
query = "SELECT COUNT(*) FROM requests_cache"
|
||||||
params: list[Any] = []
|
params: list[Any] = []
|
||||||
@@ -1916,6 +1923,10 @@ def get_cached_requests_count(
|
|||||||
if since_iso:
|
if since_iso:
|
||||||
conditions.append("created_at >= ?")
|
conditions.append("created_at >= ?")
|
||||||
params.append(since_iso)
|
params.append(since_iso)
|
||||||
|
if status_codes:
|
||||||
|
placeholders = ", ".join("?" for _ in status_codes)
|
||||||
|
conditions.append(f"status IN ({placeholders})")
|
||||||
|
params.extend(status_codes)
|
||||||
if conditions:
|
if conditions:
|
||||||
query += " WHERE " + " AND ".join(conditions)
|
query += " WHERE " + " AND ".join(conditions)
|
||||||
with _connect() as conn:
|
with _connect() as conn:
|
||||||
|
|||||||
@@ -1012,6 +1012,7 @@ async def requests_all(
|
|||||||
take: int = 50,
|
take: int = 50,
|
||||||
skip: int = 0,
|
skip: int = 0,
|
||||||
days: Optional[int] = None,
|
days: Optional[int] = None,
|
||||||
|
stage: str = "all",
|
||||||
user: Dict[str, str] = Depends(get_current_user),
|
user: Dict[str, str] = Depends(get_current_user),
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
if user.get("role") != "admin":
|
if user.get("role") != "admin":
|
||||||
@@ -1021,8 +1022,9 @@ async def requests_all(
|
|||||||
since_iso = None
|
since_iso = None
|
||||||
if days is not None and int(days) > 0:
|
if days is not None and int(days) > 0:
|
||||||
since_iso = (datetime.now(timezone.utc) - timedelta(days=int(days))).isoformat()
|
since_iso = (datetime.now(timezone.utc) - timedelta(days=int(days))).isoformat()
|
||||||
rows = get_cached_requests(limit=take, offset=skip, since_iso=since_iso)
|
status_codes = requests_router.request_stage_filter_codes(stage)
|
||||||
total = get_cached_requests_count(since_iso=since_iso)
|
rows = get_cached_requests(limit=take, offset=skip, since_iso=since_iso, status_codes=status_codes)
|
||||||
|
total = get_cached_requests_count(since_iso=since_iso, status_codes=status_codes)
|
||||||
results = []
|
results = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
status = row.get("status")
|
status = row.get("status")
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ def _request_actions_brief(entries: Any) -> list[dict[str, Any]]:
|
|||||||
async def events_stream(
|
async def events_stream(
|
||||||
request: Request,
|
request: Request,
|
||||||
recent_days: int = 90,
|
recent_days: int = 90,
|
||||||
|
recent_stage: str = "all",
|
||||||
user: Dict[str, Any] = Depends(get_current_user_event_stream),
|
user: Dict[str, Any] = Depends(get_current_user_event_stream),
|
||||||
) -> StreamingResponse:
|
) -> StreamingResponse:
|
||||||
recent_days = max(0, min(int(recent_days or 90), 3650))
|
recent_days = max(0, min(int(recent_days or 90), 3650))
|
||||||
@@ -103,6 +104,7 @@ async def events_stream(
|
|||||||
take=recent_take,
|
take=recent_take,
|
||||||
skip=0,
|
skip=0,
|
||||||
days=recent_days,
|
days=recent_days,
|
||||||
|
stage=recent_stage,
|
||||||
user=user,
|
user=user,
|
||||||
)
|
)
|
||||||
results = recent_payload.get("results") if isinstance(recent_payload, dict) else []
|
results = recent_payload.get("results") if isinstance(recent_payload, dict) else []
|
||||||
@@ -110,6 +112,7 @@ async def events_stream(
|
|||||||
"type": "home_recent",
|
"type": "home_recent",
|
||||||
"ts": datetime.now(timezone.utc).isoformat(),
|
"ts": datetime.now(timezone.utc).isoformat(),
|
||||||
"days": recent_days,
|
"days": recent_days,
|
||||||
|
"stage": recent_stage,
|
||||||
"results": results if isinstance(results, list) else [],
|
"results": results if isinstance(results, list) else [],
|
||||||
}
|
}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -117,6 +120,7 @@ async def events_stream(
|
|||||||
"type": "home_recent",
|
"type": "home_recent",
|
||||||
"ts": datetime.now(timezone.utc).isoformat(),
|
"ts": datetime.now(timezone.utc).isoformat(),
|
||||||
"days": recent_days,
|
"days": recent_days,
|
||||||
|
"stage": recent_stage,
|
||||||
"error": str(exc),
|
"error": str(exc),
|
||||||
}
|
}
|
||||||
signature = json.dumps(payload, ensure_ascii=True, separators=(",", ":"), default=str)
|
signature = json.dumps(payload, ensure_ascii=True, separators=(",", ":"), default=str)
|
||||||
|
|||||||
@@ -91,6 +91,17 @@ STATUS_LABELS = {
|
|||||||
6: "Partially ready",
|
6: "Partially ready",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
REQUEST_STAGE_CODES = {
|
||||||
|
"all": None,
|
||||||
|
"pending": [1],
|
||||||
|
"approved": [2],
|
||||||
|
"declined": [3],
|
||||||
|
"ready": [4],
|
||||||
|
"working": [5],
|
||||||
|
"partial": [6],
|
||||||
|
"in_progress": [2, 5, 6],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _cache_get(key: str) -> Optional[Dict[str, Any]]:
|
def _cache_get(key: str) -> Optional[Dict[str, Any]]:
|
||||||
cached = _detail_cache.get(key)
|
cached = _detail_cache.get(key)
|
||||||
@@ -152,6 +163,23 @@ def _status_label(value: Any) -> str:
|
|||||||
return "Unknown"
|
return "Unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_request_stage_filter(value: Optional[str]) -> str:
|
||||||
|
if not isinstance(value, str):
|
||||||
|
return "all"
|
||||||
|
normalized = value.strip().lower().replace("-", "_").replace(" ", "_")
|
||||||
|
if not normalized:
|
||||||
|
return "all"
|
||||||
|
if normalized in {"processing", "inprogress"}:
|
||||||
|
normalized = "in_progress"
|
||||||
|
return normalized if normalized in REQUEST_STAGE_CODES else "all"
|
||||||
|
|
||||||
|
|
||||||
|
def request_stage_filter_codes(value: Optional[str]) -> Optional[list[int]]:
|
||||||
|
normalized = normalize_request_stage_filter(value)
|
||||||
|
codes = REQUEST_STAGE_CODES.get(normalized)
|
||||||
|
return list(codes) if codes else None
|
||||||
|
|
||||||
|
|
||||||
def _normalize_username(value: Any) -> Optional[str]:
|
def _normalize_username(value: Any) -> Optional[str]:
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
return None
|
return None
|
||||||
@@ -1063,6 +1091,7 @@ def _get_recent_from_cache(
|
|||||||
limit: int,
|
limit: int,
|
||||||
offset: int,
|
offset: int,
|
||||||
since_iso: Optional[str],
|
since_iso: Optional[str],
|
||||||
|
status_codes: Optional[list[int]] = None,
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
items = _recent_cache.get("items") or []
|
items = _recent_cache.get("items") or []
|
||||||
results = []
|
results = []
|
||||||
@@ -1078,6 +1107,8 @@ def _get_recent_from_cache(
|
|||||||
item_dt = _parse_iso_datetime(candidate)
|
item_dt = _parse_iso_datetime(candidate)
|
||||||
if not item_dt or item_dt < since_dt:
|
if not item_dt or item_dt < since_dt:
|
||||||
continue
|
continue
|
||||||
|
if status_codes and item.get("status") not in status_codes:
|
||||||
|
continue
|
||||||
results.append(item)
|
results.append(item)
|
||||||
return results[offset : offset + limit]
|
return results[offset : offset + limit]
|
||||||
|
|
||||||
@@ -1521,6 +1552,7 @@ async def recent_requests(
|
|||||||
take: int = 6,
|
take: int = 6,
|
||||||
skip: int = 0,
|
skip: int = 0,
|
||||||
days: int = 90,
|
days: int = 90,
|
||||||
|
stage: str = "all",
|
||||||
user: Dict[str, str] = Depends(get_current_user),
|
user: Dict[str, str] = Depends(get_current_user),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
runtime = get_runtime_settings()
|
runtime = get_runtime_settings()
|
||||||
@@ -1542,9 +1574,17 @@ async def recent_requests(
|
|||||||
since_iso = None
|
since_iso = None
|
||||||
if days > 0:
|
if days > 0:
|
||||||
since_iso = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat()
|
since_iso = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat()
|
||||||
|
status_codes = request_stage_filter_codes(stage)
|
||||||
if _recent_cache_stale():
|
if _recent_cache_stale():
|
||||||
_refresh_recent_cache_from_db()
|
_refresh_recent_cache_from_db()
|
||||||
rows = _get_recent_from_cache(requested_by, requested_by_id, take, skip, since_iso)
|
rows = _get_recent_from_cache(
|
||||||
|
requested_by,
|
||||||
|
requested_by_id,
|
||||||
|
take,
|
||||||
|
skip,
|
||||||
|
since_iso,
|
||||||
|
status_codes=status_codes,
|
||||||
|
)
|
||||||
cache_mode = (runtime.artwork_cache_mode or "remote").lower()
|
cache_mode = (runtime.artwork_cache_mode or "remote").lower()
|
||||||
allow_title_hydrate = False
|
allow_title_hydrate = False
|
||||||
allow_artwork_hydrate = client.configured()
|
allow_artwork_hydrate = client.configured()
|
||||||
@@ -1733,6 +1773,8 @@ async def search_requests(
|
|||||||
request_id = None
|
request_id = None
|
||||||
status = None
|
status = None
|
||||||
status_label = None
|
status_label = None
|
||||||
|
requested_by = None
|
||||||
|
accessible = False
|
||||||
media_info = item.get("mediaInfo") or {}
|
media_info = item.get("mediaInfo") or {}
|
||||||
media_info_id = media_info.get("id")
|
media_info_id = media_info.get("id")
|
||||||
requests = media_info.get("requests")
|
requests = media_info.get("requests")
|
||||||
@@ -1741,27 +1783,23 @@ async def search_requests(
|
|||||||
status = requests[0].get("status")
|
status = requests[0].get("status")
|
||||||
status_label = _status_label(status)
|
status_label = _status_label(status)
|
||||||
elif isinstance(media_info_id, int):
|
elif isinstance(media_info_id, int):
|
||||||
username_norm = _normalize_username(user.get("username", ""))
|
|
||||||
requested_by_id = user.get("jellyseerr_user_id")
|
|
||||||
requested_by = None if user.get("role") == "admin" else username_norm
|
|
||||||
requested_by_id = None if user.get("role") == "admin" else requested_by_id
|
|
||||||
cached = get_cached_request_by_media_id(
|
cached = get_cached_request_by_media_id(
|
||||||
media_info_id,
|
media_info_id,
|
||||||
requested_by_norm=requested_by,
|
|
||||||
requested_by_id=requested_by_id,
|
|
||||||
)
|
)
|
||||||
if cached:
|
if cached:
|
||||||
request_id = cached.get("request_id")
|
request_id = cached.get("request_id")
|
||||||
status = cached.get("status")
|
status = cached.get("status")
|
||||||
status_label = _status_label(status)
|
status_label = _status_label(status)
|
||||||
|
|
||||||
if user.get("role") != "admin":
|
|
||||||
if isinstance(request_id, int):
|
if isinstance(request_id, int):
|
||||||
|
details = get_request_cache_payload(request_id)
|
||||||
|
if not isinstance(details, dict):
|
||||||
details = await _get_request_details(client, request_id)
|
details = await _get_request_details(client, request_id)
|
||||||
if not _request_matches_user(details, user.get("username", "")):
|
requested_by = _request_display_name(details)
|
||||||
continue
|
if user.get("role") == "admin":
|
||||||
else:
|
accessible = True
|
||||||
continue
|
elif isinstance(details, dict):
|
||||||
|
accessible = _request_matches_user(details, user.get("username", ""))
|
||||||
|
|
||||||
results.append(
|
results.append(
|
||||||
{
|
{
|
||||||
@@ -1772,6 +1810,8 @@ async def search_requests(
|
|||||||
"requestId": request_id,
|
"requestId": request_id,
|
||||||
"status": status,
|
"status": status,
|
||||||
"statusLabel": status_label,
|
"statusLabel": status_label,
|
||||||
|
"requestedBy": requested_by,
|
||||||
|
"accessible": accessible,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,17 @@ type RequestRow = {
|
|||||||
createdAt?: string | null
|
createdAt?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const REQUEST_STAGE_OPTIONS = [
|
||||||
|
{ value: 'all', label: 'All stages' },
|
||||||
|
{ value: 'pending', label: 'Waiting for approval' },
|
||||||
|
{ value: 'approved', label: 'Approved' },
|
||||||
|
{ value: 'in_progress', label: 'In progress' },
|
||||||
|
{ value: 'working', label: 'Working on it' },
|
||||||
|
{ value: 'partial', label: 'Partially ready' },
|
||||||
|
{ value: 'ready', label: 'Ready to watch' },
|
||||||
|
{ value: 'declined', label: 'Declined' },
|
||||||
|
]
|
||||||
|
|
||||||
const formatDateTime = (value?: string | null) => {
|
const formatDateTime = (value?: string | null) => {
|
||||||
if (!value) return 'Unknown'
|
if (!value) return 'Unknown'
|
||||||
const date = new Date(value)
|
const date = new Date(value)
|
||||||
@@ -30,6 +41,7 @@ export default function AdminRequestsAllPage() {
|
|||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [pageSize, setPageSize] = useState(50)
|
const [pageSize, setPageSize] = useState(50)
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
|
const [stage, setStage] = useState('all')
|
||||||
|
|
||||||
const pageCount = useMemo(() => {
|
const pageCount = useMemo(() => {
|
||||||
if (!total || pageSize <= 0) return 1
|
if (!total || pageSize <= 0) return 1
|
||||||
@@ -46,8 +58,15 @@ export default function AdminRequestsAllPage() {
|
|||||||
try {
|
try {
|
||||||
const baseUrl = getApiBase()
|
const baseUrl = getApiBase()
|
||||||
const skip = (page - 1) * pageSize
|
const skip = (page - 1) * pageSize
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
take: String(pageSize),
|
||||||
|
skip: String(skip),
|
||||||
|
})
|
||||||
|
if (stage !== 'all') {
|
||||||
|
params.set('stage', stage)
|
||||||
|
}
|
||||||
const response = await authFetch(
|
const response = await authFetch(
|
||||||
`${baseUrl}/admin/requests/all?take=${pageSize}&skip=${skip}`
|
`${baseUrl}/admin/requests/all?${params.toString()}`
|
||||||
)
|
)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
@@ -74,7 +93,7 @@ export default function AdminRequestsAllPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void load()
|
void load()
|
||||||
}, [page, pageSize])
|
}, [page, pageSize, stage])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (page > pageCount) {
|
if (page > pageCount) {
|
||||||
@@ -82,6 +101,10 @@ export default function AdminRequestsAllPage() {
|
|||||||
}
|
}
|
||||||
}, [pageCount, page])
|
}, [pageCount, page])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPage(1)
|
||||||
|
}, [stage])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminShell
|
<AdminShell
|
||||||
title="All requests"
|
title="All requests"
|
||||||
@@ -98,6 +121,16 @@ export default function AdminRequestsAllPage() {
|
|||||||
<span>{total.toLocaleString()} total</span>
|
<span>{total.toLocaleString()} total</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-toolbar-actions">
|
<div className="admin-toolbar-actions">
|
||||||
|
<label className="admin-select">
|
||||||
|
<span>Stage</span>
|
||||||
|
<select value={stage} onChange={(e) => setStage(e.target.value)}>
|
||||||
|
{REQUEST_STAGE_OPTIONS.map((option) => (
|
||||||
|
<option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<label className="admin-select">
|
<label className="admin-select">
|
||||||
<span>Per page</span>
|
<span>Per page</span>
|
||||||
<select value={pageSize} onChange={(e) => setPageSize(Number(e.target.value))}>
|
<select value={pageSize} onChange={(e) => setPageSize(Number(e.target.value))}>
|
||||||
|
|||||||
@@ -1527,6 +1527,13 @@ button span {
|
|||||||
color: var(--ink-muted);
|
color: var(--ink-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.recent-filter-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.recent-filter select {
|
.recent-filter select {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|||||||
@@ -22,6 +22,17 @@ const normalizeRecentResults = (items: any[]) =>
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const REQUEST_STAGE_OPTIONS = [
|
||||||
|
{ value: 'all', label: 'All stages' },
|
||||||
|
{ value: 'pending', label: 'Waiting' },
|
||||||
|
{ value: 'approved', label: 'Approved' },
|
||||||
|
{ value: 'in_progress', label: 'In progress' },
|
||||||
|
{ value: 'working', label: 'Working' },
|
||||||
|
{ value: 'partial', label: 'Partial' },
|
||||||
|
{ value: 'ready', label: 'Ready' },
|
||||||
|
{ value: 'declined', label: 'Declined' },
|
||||||
|
]
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
@@ -38,11 +49,20 @@ export default function HomePage() {
|
|||||||
const [recentError, setRecentError] = useState<string | null>(null)
|
const [recentError, setRecentError] = useState<string | null>(null)
|
||||||
const [recentLoading, setRecentLoading] = useState(false)
|
const [recentLoading, setRecentLoading] = useState(false)
|
||||||
const [searchResults, setSearchResults] = useState<
|
const [searchResults, setSearchResults] = useState<
|
||||||
{ title: string; year?: number; type?: string; requestId?: number; statusLabel?: string }[]
|
{
|
||||||
|
title: string
|
||||||
|
year?: number
|
||||||
|
type?: string
|
||||||
|
requestId?: number
|
||||||
|
statusLabel?: string
|
||||||
|
requestedBy?: string | null
|
||||||
|
accessible?: boolean
|
||||||
|
}[]
|
||||||
>([])
|
>([])
|
||||||
const [searchError, setSearchError] = useState<string | null>(null)
|
const [searchError, setSearchError] = useState<string | null>(null)
|
||||||
const [role, setRole] = useState<string | null>(null)
|
const [role, setRole] = useState<string | null>(null)
|
||||||
const [recentDays, setRecentDays] = useState(90)
|
const [recentDays, setRecentDays] = useState(90)
|
||||||
|
const [recentStage, setRecentStage] = useState('all')
|
||||||
const [authReady, setAuthReady] = useState(false)
|
const [authReady, setAuthReady] = useState(false)
|
||||||
const [servicesStatus, setServicesStatus] = useState<
|
const [servicesStatus, setServicesStatus] = useState<
|
||||||
{ overall: string; services: { name: string; status: string; message?: string }[] } | null
|
{ overall: string; services: { name: string; status: string; message?: string }[] } | null
|
||||||
@@ -143,9 +163,14 @@ export default function HomePage() {
|
|||||||
setRole(userRole)
|
setRole(userRole)
|
||||||
setAuthReady(true)
|
setAuthReady(true)
|
||||||
const take = userRole === 'admin' ? 50 : 6
|
const take = userRole === 'admin' ? 50 : 6
|
||||||
const response = await authFetch(
|
const params = new URLSearchParams({
|
||||||
`${baseUrl}/requests/recent?take=${take}&days=${recentDays}`
|
take: String(take),
|
||||||
)
|
days: String(recentDays),
|
||||||
|
})
|
||||||
|
if (recentStage !== 'all') {
|
||||||
|
params.set('stage', recentStage)
|
||||||
|
}
|
||||||
|
const response = await authFetch(`${baseUrl}/requests/recent?${params.toString()}`)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
clearToken()
|
clearToken()
|
||||||
@@ -167,7 +192,7 @@ export default function HomePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
load()
|
load()
|
||||||
}, [recentDays])
|
}, [recentDays, recentStage])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authReady) {
|
if (!authReady) {
|
||||||
@@ -222,7 +247,14 @@ export default function HomePage() {
|
|||||||
try {
|
try {
|
||||||
const streamToken = await getEventStreamToken()
|
const streamToken = await getEventStreamToken()
|
||||||
if (closed) return
|
if (closed) return
|
||||||
const streamUrl = `${baseUrl}/events/stream?stream_token=${encodeURIComponent(streamToken)}&recent_days=${encodeURIComponent(String(recentDays))}`
|
const params = new URLSearchParams({
|
||||||
|
stream_token: streamToken,
|
||||||
|
recent_days: String(recentDays),
|
||||||
|
})
|
||||||
|
if (recentStage !== 'all') {
|
||||||
|
params.set('recent_stage', recentStage)
|
||||||
|
}
|
||||||
|
const streamUrl = `${baseUrl}/events/stream?${params.toString()}`
|
||||||
source = new EventSource(streamUrl)
|
source = new EventSource(streamUrl)
|
||||||
|
|
||||||
source.onopen = () => {
|
source.onopen = () => {
|
||||||
@@ -282,7 +314,7 @@ export default function HomePage() {
|
|||||||
setLiveStreamConnected(false)
|
setLiveStreamConnected(false)
|
||||||
source?.close()
|
source?.close()
|
||||||
}
|
}
|
||||||
}, [authReady, recentDays])
|
}, [authReady, recentDays, recentStage])
|
||||||
|
|
||||||
const runSearch = async (term: string) => {
|
const runSearch = async (term: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -305,6 +337,8 @@ export default function HomePage() {
|
|||||||
type: item.type,
|
type: item.type,
|
||||||
requestId: item.requestId,
|
requestId: item.requestId,
|
||||||
statusLabel: item.statusLabel,
|
statusLabel: item.statusLabel,
|
||||||
|
requestedBy: item.requestedBy ?? null,
|
||||||
|
accessible: Boolean(item.accessible),
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
setSearchError(null)
|
setSearchError(null)
|
||||||
@@ -403,6 +437,7 @@ export default function HomePage() {
|
|||||||
<div className="recent-header">
|
<div className="recent-header">
|
||||||
<h2>{role === 'admin' ? 'All requests' : 'My recent requests'}</h2>
|
<h2>{role === 'admin' ? 'All requests' : 'My recent requests'}</h2>
|
||||||
{authReady && (
|
{authReady && (
|
||||||
|
<div className="recent-filter-group">
|
||||||
<label className="recent-filter">
|
<label className="recent-filter">
|
||||||
<span>Show</span>
|
<span>Show</span>
|
||||||
<select
|
<select
|
||||||
@@ -416,6 +451,20 @@ export default function HomePage() {
|
|||||||
<option value={180}>180 days</option>
|
<option value={180}>180 days</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
<label className="recent-filter">
|
||||||
|
<span>Stage</span>
|
||||||
|
<select
|
||||||
|
value={recentStage}
|
||||||
|
onChange={(event) => setRecentStage(event.target.value)}
|
||||||
|
>
|
||||||
|
{REQUEST_STAGE_OPTIONS.map((option) => (
|
||||||
|
<option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="recent-grid">
|
<div className="recent-grid">
|
||||||
@@ -467,9 +516,10 @@ export default function HomePage() {
|
|||||||
<aside className="side-panel">
|
<aside className="side-panel">
|
||||||
<section className="main-panel find-panel">
|
<section className="main-panel find-panel">
|
||||||
<div className="find-header">
|
<div className="find-header">
|
||||||
<h1>Find my request</h1>
|
<h1>Search all requests</h1>
|
||||||
<p className="lede">
|
<p className="lede">
|
||||||
Search by title + year, paste a request number, or pick from your recent requests.
|
Search any request by title + year or request number and see whether it already
|
||||||
|
exists in the system.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="find-controls">
|
<div className="find-controls">
|
||||||
@@ -517,12 +567,18 @@ export default function HomePage() {
|
|||||||
<button
|
<button
|
||||||
key={`${item.title || 'Untitled'}-${index}`}
|
key={`${item.title || 'Untitled'}-${index}`}
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!item.requestId}
|
disabled={!item.requestId || !item.accessible}
|
||||||
onClick={() => item.requestId && router.push(`/requests/${item.requestId}`)}
|
onClick={() =>
|
||||||
|
item.requestId && item.accessible && router.push(`/requests/${item.requestId}`)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{item.title || 'Untitled'} {item.year ? `(${item.year})` : ''}{' '}
|
{item.title || 'Untitled'} {item.year ? `(${item.year})` : ''}{' '}
|
||||||
{!item.requestId
|
{!item.requestId
|
||||||
? '- not requested'
|
? '- not requested'
|
||||||
|
: !item.accessible
|
||||||
|
? `- ${item.statusLabel || 'Requested'} · requested by ${
|
||||||
|
item.requestedBy || 'another user'
|
||||||
|
}`
|
||||||
: item.statusLabel
|
: item.statusLabel
|
||||||
? `- ${item.statusLabel}`
|
? `- ${item.statusLabel}`
|
||||||
: ''}
|
: ''}
|
||||||
|
|||||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "magent-frontend",
|
"name": "magent-frontend",
|
||||||
"version": "0203262044",
|
"version": "0303261323",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "magent-frontend",
|
"name": "magent-frontend",
|
||||||
"version": "0203262044",
|
"version": "0303261323",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "magent-frontend",
|
"name": "magent-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0203262044",
|
"version": "0303261323",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
|
|||||||
Reference in New Issue
Block a user