feat(pres_mgmt): File Downloads report with clean filename presets

New report at Pres Mgmt > Reports > File Downloads. Groups all event files
by session and presenter, with 10 filename format presets (original, session
code/date/name combos, presenter variants). Per-file Download button and Copy
Link button using hosted_file endpoint for ?key= auth support.

Also fixes direct download links in element_manage_event_file_li and the
hosted_files download button — event_file endpoint does not yet propagate
?key= auth internally, so all direct links now use hosted_file endpoint
which supports it today.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-06-10 14:19:25 -04:00
parent 1b81b8873c
commit 6b122a065e
6 changed files with 597 additions and 25 deletions

View File

@@ -96,6 +96,26 @@ The primary way to retrieve data.
* **Endpoint:** `POST /v3/crud/{obj_type}/search`
* **Security:** Automatically filters results to only show records belonging to your `x-account-id`. If no account context is provided, it will return **0 records** for private objects.
#### Sorting with `order_by_li`
Pass a JSON object as the `order_by_li` query parameter to sort results:
```ts
// ?order_by_li={"filename":"ASC","created_on":"DESC"}
const params = new URLSearchParams({
order_by_li: JSON.stringify({ filename: 'ASC', created_on: 'DESC' })
});
```
> [!IMPORTANT]
> **`order_by_li` only accepts columns from the raw base table** — not view-only join columns.
>
> Some object types (e.g. `event_file`) have enriched views that JOIN other tables to expose convenience fields like `event_presenter_family_name`. These are available in search results when using `?view=alt`, but they **cannot** be used in `order_by_li`. Attempting to sort by them silently drops those sort keys (the query proceeds without them).
>
> If you need to sort by a joined field, sort client-side on the returned list.
>
> **Columns safe to sort on for `event_file`:** any field in the `event_file` table itself — `filename`, `title`, `extension`, `created_on`, `updated_on`, `sort`, `enable`, etc.
### C. POST Create / PATCH Update
Modify data in the system.
* **Endpoints:**
@@ -281,6 +301,47 @@ Every Event File (`event_file`) **must** have a linked Hosted File (`hosted_file
* `hosted_file_size` (string - in bytes)
2. **Nested Hosted File Object:** A full `hosted_file` object will be nested under the `hosted_file` key. This object (`Hosted_File_Base` model) will contain all its standard fields, including `id` (random string ID), `hash_sha256`, `content_type`, `size`, etc.
### Direct Download Links (Shareable / External)
Event files can be downloaded without standard auth headers using one of two bypass mechanisms. This is useful for generating shareable links for staff or external recipients.
- **Method:** `GET`
- **Path:** `/v3/action/event_file/{event_file_id}/download`
The endpoint also accepts `hosted_file_id` or `archive_content_id` directly — it resolves the chain automatically.
#### Auth bypass options
| Query param | Value | When to use |
|---|---|---|
| `?key=<account_id_random>` | Any valid account random ID | Staff sharing within a known account context |
| `?site_key=<site_access_key>` | The site's `access_key` value | Public or semi-public distribution tied to a specific site |
Either param replaces the need for `x-aether-api-key` / `x-account-id` headers, so the URL is self-contained and works in a plain browser tab or `<a href>` link.
#### Optional params
| Query param | Description |
|---|---|
| `filename` | Override the download filename (min 4 chars). Useful for giving files clean display names. |
#### Building a shareable link
```ts
// Build a self-contained download URL for staff/external use
function makeDownloadUrl(eventFileId: string, accountId: string, displayName?: string): string {
const base = `https://dev-api.oneskyit.com/v3/action/event_file/${eventFileId}/download`;
const params = new URLSearchParams({ key: accountId });
if (displayName) params.set('filename', displayName);
return `${base}?${params}`;
}
```
The endpoint supports byte-range requests (`Range` header), so it works correctly for in-browser media streaming as well as direct file downloads.
> [!NOTE]
> The `?key=` bypass verifies only that the account ID exists — it does not confirm the file belongs to that account. It is appropriate for internal staff tools. For publicly distributed links, prefer `?site_key=` which ties access to a specific site's configured key.
---
## 6. Hosted File Actions: Convert & Clip (Frontend Notes)
@@ -301,18 +362,16 @@ These helper endpoints let the frontend request small server-side transformation
- Required query params: `link_to_type`, `link_to_id`, `start_time`, `end_time` (format `HH:MM:SS`)
- Optional query params: `filename_no_ext` (defaults to `automated_hosted_file_clip_video`), `reencode` (bool), `scale_down` (bool)
- Auth: standard V3 headers
- Behavior: extracts a clip using `ffmpeg` and saves it as a new `hosted_file`. Defaults to stream-copying to be fast; set `reencode=true` to force H.264 or `scale_down=true` to resize. Returns 400 on failure.
- Behavior: extracts a clip using `ffmpeg` and saves it as a new `hosted_file`.
- Defaults to stream-copying to be fast; set `reencode=true` to force H.264 or `scale_down=true` to resize.
- For longer-running clips you can schedule the job in the background by adding `?background=true`. When scheduled the API returns `202 Accepted` and the clip runs asynchronously on the server; check the returned `hosted_file` record later via the standard V3 `hosted_file` endpoints.
- Returns 400 on synchronous failure; returns 202 when scheduled successfully.
- Behavior: extracts a clip using `ffmpeg` and saves it as a new `hosted_file`.
- Defaults to stream-copying (fast); set `reencode=true` to force H.264 or `scale_down=true` to resize.
- Add `?background=true` to schedule the clip asynchronously — returns `202 Accepted` immediately; poll the `hosted_file` record for completion.
- Returns 400 on synchronous failure; 202 when scheduled successfully.
Frontend guidance:
- Call these routes with the same `link_to_type` / `link_to_id` you plan to associate the resulting hosted_file with — the server resolves random IDs for you.
- After a successful response, use the V3 `hosted_file` action endpoints (download/delete) to manage or retrieve the new file.
- These endpoints run synchronously and can take time for large inputs; for heavy or batch workloads use a queued job pattern instead.
- These endpoints may take time for large inputs. Prefer using `?background=true` to schedule work and receive a `202 Accepted` response for async processing. For heavy or batch workloads use a queued job pattern instead.
- Prefer `?background=true` for large inputs to avoid request timeouts. For heavy or batch workloads use a queued job pattern instead.
---
@@ -673,19 +732,19 @@ Verifies a Novi AMS member UUID by proxying the Novi API call through the Aether
|---|---|---|
| `404` | UUID not found in Novi, or Novi returned 200 with no identity data (empty-member anti-pattern — member may have just joined) | Treat as denied / not a member |
| `429` | Novi rate limit hit | Surface as `'rate_limited'`; advise retry |
| `503` | Novi unreachable or Novi 5xx error | Auto-retry once after 3s; if retry also fails, surface as `'api_error'` |
| `503` | Novi unreachable or Novi 5xx error | Surface as `'api_error'`; advise retry |
### Migration from direct Novi call — ✅ Complete (2026-05-19)
### Migration from direct Novi call
`+layout.svelte:verify_novi_uuid()` now calls this endpoint instead of Novi directly. Response code mapping (for reference):
The frontend's `+layout.svelte:verify_novi_uuid()` currently calls Novi directly from the browser. Replace that `fetch()` with this endpoint. Response code mapping:
| Direct Novi result | This endpoint returns | Frontend behavior |
| Direct Novi result | This endpoint returns | Frontend state |
|---|---|---|
| `200` with identity data | `200` | `verified` |
| `200` with no identity data | `404` | `denied` |
| `404` | `404` | `denied` |
| `429` | `429` | Auto-retry after 10s; `'rate_limited'` if retry fails |
| Network error / Novi 5xx | `503` | Auto-retry after 3s; `'api_error'` if retry fails |
| `429` | `429` | `'rate_limited'` |
| Network error / Novi 5xx | `503` | `'api_error'` |
### Caching