@crvouga/atlas 1.19.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -1
- package/dist/core/src/cli.d.ts +1 -1
- package/dist/core/src/cli.js +3 -0
- package/dist/reports/src/compose.d.ts +23 -1
- package/dist/reports/src/compose.js +29 -19
- package/dist/reports/src/report-cli.d.ts +13 -0
- package/dist/reports/src/report-cli.js +6 -4
- package/dist/reports/src/ui-catalog-cli.d.ts +10 -0
- package/dist/reports/src/ui-catalog-cli.js +391 -0
- package/dist/reports/src/ui-catalog-diff.d.ts +104 -0
- package/dist/reports/src/ui-catalog-diff.js +130 -0
- package/dist/reports/src/ui-catalog-export.d.ts +82 -0
- package/dist/reports/src/ui-catalog-export.js +221 -0
- package/dist/schema/src/api.d.ts +55 -0
- package/dist/schema/src/api.js +33 -0
- package/dist/schema/src/cli-results.d.ts +318 -1
- package/dist/schema/src/cli-results.js +89 -1
- package/dist/server/src/app.js +3 -1
- package/dist/server/src/openapi.d.ts +1 -1
- package/dist/server/src/openapi.js +24 -2
- package/dist/server/src/recording-verification.d.ts +34 -0
- package/dist/server/src/recording-verification.js +118 -0
- package/docs/agents.md +5 -0
- package/docs/reference/cli.md +56 -1
- package/docs/reference/http-api.md +52 -0
- package/docs/reference/reports.md +392 -0
- package/docs/reference/schema.md +82 -1
- package/docs/reference/server.md +39 -0
- package/docs/rich-evidence.md +12 -0
- package/docs/server.md +12 -0
- package/docs/ui-catalog.md +106 -12
- package/docs/ui.md +6 -1
- package/llms-full.txt +246 -11
- package/package.json +1 -1
- package/packages/schema/schemas/ui-catalog-diff-result.schema.json +201 -0
- package/packages/schema/schemas/ui-catalog-export-result.schema.json +83 -0
- package/packages/schema/schemas/ui-catalog-publish-result.schema.json +116 -0
- package/packages/schema/schemas/ui-catalog-status.schema.json +197 -0
- package/packages/server/openapi.json +527 -1
- package/packages/visualizer/src/catalog/Picture.tsx +32 -0
- package/packages/visualizer/src/catalog/UiCatalogCompare.tsx +289 -0
- package/packages/visualizer/src/catalog/UiCatalogPage.tsx +18 -28
- package/packages/visualizer/src/catalog/history.ts +61 -0
- package/packages/visualizer/src/catalog/search.ts +14 -3
- package/packages/visualizer/src/evidence/evidence-context.tsx +2 -1
- package/packages/visualizer/src/explorer/RunsView.tsx +32 -6
- package/packages/visualizer/src/explorer/catalog.tsx +24 -1
- package/packages/visualizer/src/explorer/recording-verification.ts +58 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { RecordingVerifyRequestSchema } from '../../schema/src/api.js';
|
|
2
|
+
import { eachLimited } from '../../reports/src/completion-order.js';
|
|
3
|
+
import { layerKey } from '../../reports/src/layer-store.js';
|
|
4
|
+
import { verifyStoreRecordings } from '../../reports/src/recording-verify.js';
|
|
5
|
+
import { runKey } from '../../reports/src/test-catalog.js';
|
|
6
|
+
import { problemResponse } from './problem.js';
|
|
7
|
+
import { readLimited } from './store-api.js';
|
|
8
|
+
import { Refusal } from './validate.js';
|
|
9
|
+
const NO_STORE = { 'cache-control': 'no-store' };
|
|
10
|
+
const HELD_MS = 30_000;
|
|
11
|
+
const DEFAULT_LIMIT = 20;
|
|
12
|
+
const MAX_LIMIT = 200;
|
|
13
|
+
/**
|
|
14
|
+
* Registers `GET` and `POST /api/reports/recordings/verification` on `app`. The result of the last read-back of each
|
|
15
|
+
* layer is held in memory, replaced by a newer one, and served only for layers a store still holds. A layer held by
|
|
16
|
+
* two backends keeps the reading with more gaps, so a copy that lost its blobs is never hidden by one that did not.
|
|
17
|
+
*/
|
|
18
|
+
export function mountRecordingVerification(app, deps) {
|
|
19
|
+
const { stores, auth, index, now } = deps;
|
|
20
|
+
const held = new Map();
|
|
21
|
+
let notices = [];
|
|
22
|
+
let running = false;
|
|
23
|
+
// Whether a layer's manifest is still in some store (garbage collection removes superseded layers), asked at most every
|
|
24
|
+
// HELD_MS. Only a `head` of the manifest: no recording is read.
|
|
25
|
+
const present = new Map();
|
|
26
|
+
const holds = async (ids) => {
|
|
27
|
+
const out = new Set();
|
|
28
|
+
await eachLimited(ids, 8, async (id) => {
|
|
29
|
+
const known = present.get(id);
|
|
30
|
+
let value = known !== undefined && now() - known.at < HELD_MS ? known.value : undefined;
|
|
31
|
+
if (value === undefined) {
|
|
32
|
+
value = false;
|
|
33
|
+
for (const store of stores) {
|
|
34
|
+
if (await store.head(layerKey(id)).catch(() => false)) {
|
|
35
|
+
value = true;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
present.set(id, { at: now(), value });
|
|
40
|
+
}
|
|
41
|
+
if (value)
|
|
42
|
+
out.add(id);
|
|
43
|
+
});
|
|
44
|
+
return out;
|
|
45
|
+
};
|
|
46
|
+
const document = async (request, extra = {}) => {
|
|
47
|
+
const search = new URL(request.url).searchParams;
|
|
48
|
+
const run = search.get('run');
|
|
49
|
+
const prefixes = search.getAll('layer').flatMap(value => value.split(',')).map(value => value.trim().toLowerCase()).filter(Boolean);
|
|
50
|
+
const wanted = [...held.values()].filter(entry => (!run || entry.run === run) && (!prefixes.length || prefixes.some(prefix => entry.layer.startsWith(prefix))));
|
|
51
|
+
const still = await holds(wanted.map(entry => entry.layer));
|
|
52
|
+
const layers = wanted.filter(entry => still.has(entry.layer))
|
|
53
|
+
.sort((a, b) => b.verifiedAtMs - a.verifiedAtMs || (a.layer < b.layer ? -1 : 1));
|
|
54
|
+
return { schemaVersion: 1, layers, notices, ...extra };
|
|
55
|
+
};
|
|
56
|
+
app.get('/api/reports/recordings/verification', async (c) => c.json(await document(c.req.raw), 200, NO_STORE));
|
|
57
|
+
app.post('/api/reports/recordings/verification', async (c) => {
|
|
58
|
+
const verdict = auth.check(c.req.raw, 'publish');
|
|
59
|
+
if (!verdict.ok)
|
|
60
|
+
return deps.unauthorized(verdict);
|
|
61
|
+
if (!index.ready)
|
|
62
|
+
return problemResponse(503, 'The report is still loading. Try again in a moment.', { 'retry-after': '2' });
|
|
63
|
+
let input = {};
|
|
64
|
+
try {
|
|
65
|
+
const bytes = await readLimited(c.req.raw, 16 * 1024);
|
|
66
|
+
if (bytes.length) {
|
|
67
|
+
try {
|
|
68
|
+
input = JSON.parse(new TextDecoder().decode(bytes));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return problemResponse(422, 'The body is not JSON.');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error instanceof Refusal)
|
|
77
|
+
return problemResponse(error.status, error.message);
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
const parsed = RecordingVerifyRequestSchema.safeParse(input);
|
|
81
|
+
if (!parsed.success)
|
|
82
|
+
return problemResponse(422, `Not a verification request: ${parsed.error.issues[0]?.message ?? 'invalid'}.`);
|
|
83
|
+
if (running)
|
|
84
|
+
return problemResponse(409, 'A read-back is already running. Read the result with GET when it finishes.', { 'retry-after': '5' });
|
|
85
|
+
running = true;
|
|
86
|
+
try {
|
|
87
|
+
const limit = Math.min(MAX_LIMIT, parsed.data.limit ?? DEFAULT_LIMIT);
|
|
88
|
+
const runs = new Map();
|
|
89
|
+
const found = [];
|
|
90
|
+
const lost = [];
|
|
91
|
+
const at = now();
|
|
92
|
+
for (const store of stores) {
|
|
93
|
+
const result = await verifyStoreRecordings(store, {
|
|
94
|
+
...(parsed.data.layers?.length ? { ids: parsed.data.layers } : {}), limit,
|
|
95
|
+
include: layer => { runs.set(layer.id, runKey(layer)); return true; }
|
|
96
|
+
});
|
|
97
|
+
lost.push(...result.notices);
|
|
98
|
+
for (const layer of result.layers)
|
|
99
|
+
found.push({ ...layer, run: runs.get(layer.layer) ?? '', verifiedAtMs: at });
|
|
100
|
+
}
|
|
101
|
+
// A layer held by two backends keeps the copy with more gaps; a newer pass replaces an older one.
|
|
102
|
+
const fresh = new Map();
|
|
103
|
+
for (const entry of found)
|
|
104
|
+
if ((fresh.get(entry.layer)?.gaps.length ?? -1) < entry.gaps.length || !fresh.has(entry.layer))
|
|
105
|
+
fresh.set(entry.layer, entry);
|
|
106
|
+
for (const [layer, entry] of fresh)
|
|
107
|
+
held.set(layer, entry);
|
|
108
|
+
notices = [...new Set(lost)];
|
|
109
|
+
return c.json(await document(c.req.raw, { verified: fresh.size }), 200, NO_STORE);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
return problemResponse(502, `The store could not be read: ${error instanceof Error ? error.message.split('\n')[0] : String(error)}`);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
running = false;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
package/docs/agents.md
CHANGED
|
@@ -164,6 +164,11 @@ JSON Schema (`@crvouga/atlas/schemas/<name>.schema.json`, draft 2020-12):
|
|
|
164
164
|
| `atlas report doctor` | `doctor-result` |
|
|
165
165
|
| `atlas report status` | `report-status` |
|
|
166
166
|
| `atlas report verify-recordings` | `verify-recordings-result` |
|
|
167
|
+
| `atlas ui-catalog plan` | `ui-catalog-publish-result` |
|
|
168
|
+
| `atlas ui-catalog publish` | `ui-catalog-publish-result` |
|
|
169
|
+
| `atlas ui-catalog status` | `ui-catalog-status` |
|
|
170
|
+
| `atlas ui-catalog diff` | `ui-catalog-diff-result` |
|
|
171
|
+
| `atlas ui-catalog export` | `ui-catalog-export-result` |
|
|
167
172
|
| `a command that was wrong (standard error)` | `command-error` |
|
|
168
173
|
<!-- /generated:json-command-schemas -->
|
|
169
174
|
|
package/docs/reference/cli.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Generated from the usage text each command prints (`atlas <command> --help`) by `bun run docs`. Do not edit.
|
|
4
4
|
|
|
5
|
-
Add `--format json` (alias `--json`) to `init`, `report publish`, `stream`, `merge`, `doctor` and `
|
|
5
|
+
Add `--format json` (alias `--json`) to `init`, `report publish`, `stream`, `merge`, `doctor`, `status` and every `ui-catalog` command for a JSON document on standard output (schemas in `@crvouga/atlas/schemas`). Exit codes: `0` it worked, `1` it ran and the answer is no, `2` the command was wrong.
|
|
6
6
|
|
|
7
7
|
## atlas
|
|
8
8
|
|
|
@@ -21,6 +21,7 @@ atlas — executable statecharts
|
|
|
21
21
|
atlas dev [--port 5180] develop the Atlas server and client on one host
|
|
22
22
|
atlas init [--server <url>] [--write] integrate this project: detect its suites, print the plan (atlas init --help)
|
|
23
23
|
atlas report publish|stream|status|merge|doctor|policy … layered evidence for any kind of spec (atlas report --help)
|
|
24
|
+
atlas ui-catalog plan|publish|status|diff|export … the UI catalog of captured states (atlas ui-catalog --help)
|
|
24
25
|
|
|
25
26
|
--workers path attempts that run at once (default: one per CPU, capped by the driver)
|
|
26
27
|
--seed only paths that start from this seed ("start" for paths setup starts); repeatable
|
|
@@ -172,3 +173,57 @@ atlas report — layered evidence for any kind of spec
|
|
|
172
173
|
|
|
173
174
|
Exit codes: 0 ok, 1 the report does not pass (or a check failed), 2 the command was wrong.
|
|
174
175
|
```
|
|
176
|
+
|
|
177
|
+
## atlas ui-catalog
|
|
178
|
+
|
|
179
|
+
```text
|
|
180
|
+
atlas ui-catalog — a browsable inventory of captured UI states, from any collector
|
|
181
|
+
|
|
182
|
+
atlas ui-catalog plan --states <file.json> --producer <name> [--store <dir|s3://bucket/prefix|https://…>]
|
|
183
|
+
[--environment <name>] [--param key=value]... [--shard <label>]
|
|
184
|
+
[--branch <b>] [--sha <s>] [--run-url <u>] [--run-id <id>] [--run-title <t>] [--recorded-at <iso|ms>]
|
|
185
|
+
atlas ui-catalog publish --captures <file.json> --producer <name> [the same flags as plan]
|
|
186
|
+
[--attach-root <dir>]... [--max-picture-mb <n>]
|
|
187
|
+
atlas ui-catalog status [--store <dir|s3://…|id=url>]... [--backends <module>] [filters] [--branch <name>]...
|
|
188
|
+
[--ignore-modified] [--format text|json|markdown]
|
|
189
|
+
atlas ui-catalog diff --from <commit> --to <commit> [--store …]... [--backends <module>] [filters]
|
|
190
|
+
[--fail-on-change] [--format text|json|markdown]
|
|
191
|
+
atlas ui-catalog export --out <file.html> [--store …]... [--backends <module>] [filters]
|
|
192
|
+
[--images inline|none] [--max-mb <n>] [--manifest <file.json>] [--title <t>]
|
|
193
|
+
|
|
194
|
+
filters (status, diff, export): --q <words> --coverage a,b --profile <key|label>... --area a --route r --cohort c
|
|
195
|
+
--flag f --surface s --producer p
|
|
196
|
+
|
|
197
|
+
plan Write one layer of kind ui-plan: the states (and with "inventory": true, the surfaces) that should exist under
|
|
198
|
+
this profile. Every entry is not-run, so a plan never counts as a pass. The file is {"states":[{"id":…,
|
|
199
|
+
"title"?,"route"?,"fixture"?,"cohort"?,"area"?,"flags"?,"covers"?,"inventory"?}]} or just that list.
|
|
200
|
+
publish Write one layer of kind ui-state: the states one collector or shard captured under this profile. The file is
|
|
201
|
+
{"states":[{"id":…,"title"?,"status"?:"passed|failed|running|not-run","detail"?,"route"?,"fixture"?,"cohort"?,
|
|
202
|
+
"area"?,"flags"?,"covers"?,"labels"?,"tags"?,"durationMs"?,"screenshot"?:"file.png"|{"path"|"uri",
|
|
203
|
+
"width"?,"height"?,"contentType"?}}]}. A picture file is read from beside the file and each --attach-root, never
|
|
204
|
+
from a hidden file or a link out. A passed state whose picture cannot be read is published as failed, with the
|
|
205
|
+
reason: a state without its picture is not shown as captured.
|
|
206
|
+
--environment and each --param key=value are the profile (a viewport, a color scheme, a device): another
|
|
207
|
+
profile is another result side by side. --shard only describes the layer; it never splits a result.
|
|
208
|
+
--recorded-at defaults to the modification time of the file, so publishing the same file twice is one layer.
|
|
209
|
+
status Read every store and print the catalog's summary. Exit 1 unless the catalog holds a state and there is nothing
|
|
210
|
+
to attend to: a failed capture, a state a plan expects with nothing captured, a capture still in progress, a
|
|
211
|
+
backend or layer that could not be read. Without a plan nothing is expected, so nothing is missing.
|
|
212
|
+
--profile takes a profile's key or its label, as printed by status.
|
|
213
|
+
diff Set the catalog recorded at one commit (--from) against another (--to): each state in each profile is paired
|
|
214
|
+
with itself and is unchanged, changed in coverage, changed in picture (the stored bytes differ), unverified (a
|
|
215
|
+
side has no content hash), only in --to (added) or only in --from (removed). A commit is the whole id or at
|
|
216
|
+
least 7 digits of it. Exit 0 unless a store could not be read, or, with --fail-on-change, anything differs.
|
|
217
|
+
export Write the catalog as one HTML file with its pictures embedded and its manifest in it: no server, no network.
|
|
218
|
+
It is the catalog at the moment it was made. --images none leaves the pictures out. --max-mb (default 32) limits
|
|
219
|
+
the embedded pictures; those past it are listed in the page as left out, and the manifest stays complete.
|
|
220
|
+
--manifest writes the catalog's JSON manifest too.
|
|
221
|
+
|
|
222
|
+
--format json (alias --json) every subcommand prints one JSON document instead of prose
|
|
223
|
+
(schemas: ui-catalog-publish-result, ui-catalog-status, ui-catalog-diff-result, ui-catalog-export-result).
|
|
224
|
+
A command that is wrong is {"ok":false,"error":{"message":…,"code":"usage"}} on standard error.
|
|
225
|
+
A store is a directory, s3://bucket/prefix or an Atlas server (https://…), exactly as for atlas report; select a project
|
|
226
|
+
with --project <slug> or ATLAS_PROJECT. When no --store is given, ATLAS_STORE is used.
|
|
227
|
+
|
|
228
|
+
Exit codes: 0 ok, 1 the answer is no (see each command), 2 the command was wrong.
|
|
229
|
+
```
|
|
@@ -694,6 +694,58 @@ Scoped to one project. The same project may be selected on the unprefixed API us
|
|
|
694
694
|
|
|
695
695
|
Responses: `201` The recorded entry.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `413` Request too large.; `422` Not a tag edit: a bad target, an empty or over-long tag, or nothing to change.
|
|
696
696
|
|
|
697
|
+
### Which stored recordings the server last read back, and which were gone or damaged
|
|
698
|
+
|
|
699
|
+
`GET /api/reports/recordings/verification` · role: read
|
|
700
|
+
|
|
701
|
+
The newest read-back the server did of stored videos, per layer: how many it checked, how many passed and the `gaps` (a blob that is gone, bytes of another size or hash than were published, or a file that does not begin like its container), each naming the check that holds the video. `run` is the key of the run the layer belongs to (a `RunSummary.key`), so a viewer can mark that run's recordings as stored-then-unavailable beside its test results.
|
|
702
|
+
|
|
703
|
+
This route never reads a blob: it returns what a publish token's `POST` found earlier. A layer that was never read back is absent, which means "not verified", not "fine"; the result is kept in the server's memory only, so a restart empties it. Only layers a store still holds are listed (one removed by garbage collection drops out). No status, slot or winner depends on it. It checks stored bytes, not browser playback. Narrow with `?run=` (a run key) and `?layer=` (an id prefix of at least 7 hex digits, repeatable).
|
|
704
|
+
|
|
705
|
+
- `run` (query): Only layers of this run (a key from `GET /api/reports/runs`).
|
|
706
|
+
- `layer` (query): Only layers whose id starts with this prefix. Repeatable or comma-separated.
|
|
707
|
+
- `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
708
|
+
|
|
709
|
+
Responses: `200` The held read-backs.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.
|
|
710
|
+
|
|
711
|
+
### Which stored recordings the server last read back, and which were gone or damaged
|
|
712
|
+
|
|
713
|
+
`GET /api/projects/{project}/reports/recordings/verification` · role: read
|
|
714
|
+
|
|
715
|
+
The newest read-back the server did of stored videos, per layer: how many it checked, how many passed and the `gaps` (a blob that is gone, bytes of another size or hash than were published, or a file that does not begin like its container), each naming the check that holds the video. `run` is the key of the run the layer belongs to (a `RunSummary.key`), so a viewer can mark that run's recordings as stored-then-unavailable beside its test results.
|
|
716
|
+
|
|
717
|
+
This route never reads a blob: it returns what a publish token's `POST` found earlier. A layer that was never read back is absent, which means "not verified", not "fine"; the result is kept in the server's memory only, so a restart empties it. Only layers a store still holds are listed (one removed by garbage collection drops out). No status, slot or winner depends on it. It checks stored bytes, not browser playback. Narrow with `?run=` (a run key) and `?layer=` (an id prefix of at least 7 hex digits, repeatable).
|
|
718
|
+
|
|
719
|
+
Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
|
|
720
|
+
|
|
721
|
+
- `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
722
|
+
- `run` (query): Only layers of this run (a key from `GET /api/reports/runs`).
|
|
723
|
+
- `layer` (query): Only layers whose id starts with this prefix. Repeatable or comma-separated.
|
|
724
|
+
|
|
725
|
+
Responses: `200` The held read-backs.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.
|
|
726
|
+
|
|
727
|
+
### Read stored recordings back and check them (publish token)
|
|
728
|
+
|
|
729
|
+
`POST /api/reports/recordings/verification` · role: publish
|
|
730
|
+
|
|
731
|
+
Reads the videos of the newest layers (`limit`, default 20, at most 200; or the layers `layers` names) back from storage and checks that each is still stored, still has the size and SHA-256 published, and still begins like its media type's container. This reads blobs, so it needs a publish (or admin) token in a Bearer header; a reader-only token, a browser sign-in box and an anonymous reader are refused. One read-back runs at a time (`409` otherwise). It writes nothing to storage and changes no status. The answer is the same document `GET` returns, with `verified`: how many layers this pass read back.
|
|
732
|
+
|
|
733
|
+
- `project` (query): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
734
|
+
|
|
735
|
+
Responses: `200` The read-backs now held.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `409` A read-back is already running.; `413` Request too large.; `422` Not a verification request.; `502` A store could not be read.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
|
|
736
|
+
|
|
737
|
+
### Read stored recordings back and check them (publish token)
|
|
738
|
+
|
|
739
|
+
`POST /api/projects/{project}/reports/recordings/verification` · role: publish
|
|
740
|
+
|
|
741
|
+
Reads the videos of the newest layers (`limit`, default 20, at most 200; or the layers `layers` names) back from storage and checks that each is still stored, still has the size and SHA-256 published, and still begins like its media type's container. This reads blobs, so it needs a publish (or admin) token in a Bearer header; a reader-only token, a browser sign-in box and an anonymous reader are refused. One read-back runs at a time (`409` otherwise). It writes nothing to storage and changes no status. The answer is the same document `GET` returns, with `verified`: how many layers this pass read back.
|
|
742
|
+
|
|
743
|
+
Scoped to one project. The same project may be selected on the unprefixed API using ?project= or X-Atlas-Project. Unknown projects return 404 on reads and are created by an authenticated upload.
|
|
744
|
+
|
|
745
|
+
- `project` (path, required): Project slug. Alternatively send X-Atlas-Project or use the project-prefixed route. Omit for the default project. Conflicting selectors are refused.
|
|
746
|
+
|
|
747
|
+
Responses: `200` The read-backs now held.; `401` No token, or an unknown one. Send `Authorization: Bearer <token>`.; `403` The token is known but its role may not do this.; `409` A read-back is already running.; `413` Request too large.; `422` Not a verification request.; `502` A store could not be read.; `503` The server is still reading its backends. Retry after the number of seconds in `Retry-After`.
|
|
748
|
+
|
|
697
749
|
### Review collections: curated sets of recordings, newest first
|
|
698
750
|
|
|
699
751
|
`GET /api/reports/review-collections` · role: read
|