Repository navigation
Correct the OpenAPI spec for Blume and redesign the dashboard API pages - #67
leoisadev1 wants to merge 8 commits into
Conversation
List the error codes each endpoint really returns (401 vs 403, the framework's plain-text 400/415/422, 500 and 502), add tags, operation descriptions, examples and reusable responses, and describe job statuses and failure codes with x-enum-descriptions. Facts come from routes.rs, jobs.rs, api_keys.rs and the worker. Synced to the web copy. Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
A tabbed, highlighted code panel with a remembered language choice, and quick-start programs in cURL, Node.js, Python and the CLI built from apiBaseUrl. The public reference itself now lives in the Blume site. Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
/dashboard/api gets a spend meter, inline key creation with a copy-once callout, two-step revoke, the shared quick-start panel and the base URL. /dashboard/api/convert becomes a two-step dropzone and target picker with a progress stepper, cancel and retry. Drops the unused CodeSample. Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
Checks that the web copy matches convt-server's spec, that the reference lists exactly the routed operations, and that every code sample calls a documented path on the configured base URL. Gives unauthorized the same 'status. text' description as every other error code. Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
The revoke confirmation's accessible name now contains its visible text, the converter only shows an attempt number on a retry, and a format name that repeats its id is hidden. Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
…arget The create request body and its 200 response are now CreateJob and JobReservation components, so their five field descriptions target those. The id parameter targets the JobId component, and the shared 401, 403, 404, 429 and 502 wildcards are dropped: those responses are components with their own descriptions, and the 403 wildcard overwrote create's. Blume skips unmatched targets silently, so generate.ts now fails the build when any overlay target resolves to nothing. Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
…ence code Docs links go to /docs, /docs/api, and the quick start, errors, limits and formats pages instead of anchors on the removed single page. The unit test reads crates/convt-server/openapi.json directly and checks that each linked page exists in apps/docs and that both apps show the same API host. Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
The dev server's regenerated route tree came along in the previous commit; main's copy is the one the build expects. Co-authored-by: Leo <leoisadev1@users.noreply.github.com>
f0fc942 to
5fe7438
Compare
| "category": { | ||
| "type": "string", | ||
| "enum": ["image", "video", "audio", "document"], | ||
| "example": "image" |
There was a problem hiding this comment.
🔴 Format responses fail their published schema
For formats such as SVG and PDF, Format.category excludes values returned by the server. Schema-validating clients reject legitimate format lists.
| "category": { | |
| "type": "string", | |
| "enum": ["image", "video", "audio", "document"], | |
| "example": "image" | |
| "category": { | |
| "type": "string", | |
| "enum": ["image", "vector", "video", "audio", "pdf", "document", "presentation", "spreadsheet"], | |
| "example": "image" | |
| }, |
Was this helpful? React with 👍 or 👎 to provide feedback.
| function choose(next: File | null) { | ||
| setFile(next); | ||
| const reachable = targetsFor(detect(next)?.id); | ||
| setTarget((current) => (current && reachable.includes(current) ? current : null)); | ||
| setResult(null); | ||
| setError(""); | ||
| setJob(null); | ||
| setStage("idle"); | ||
| } |
There was a problem hiding this comment.
🔴 Unconfirmed cancellation blocks new conversions
After an unconfirmed cancellation, choose hides the retry prompt but leaves pendingCancel set. Selecting another file disables conversion until reload.
Learn more
The SDK raises ConvtCancellationError when it cannot confirm cancellation of an already reserved job in convert. The converter stores that job ID in pendingCancel and disables conversion while it exists. Selecting another file clears the error and stage, so the retry button disappears, but the pending ID remains and keeps the convert button disabled.
Example: A cancellation request fails during a network outage. The user selects a different PNG to try again. The new file and targets appear, but Convert remains disabled and no retry prompt appears.
Recommended fix: Preserve the pending cancellation UI across file changes and keep retry available, or prevent changing files until retryCancel resolves the outstanding job. Do not discard the ID without providing a way to release the reservation.
Was this helpful? React with 👍 or 👎 to provide feedback.
| case $status in succeeded|failed|cancelled) break ;; esac | ||
| sleep 2 | ||
| done | ||
|
|
||
| # 4. Download the output | ||
| curl -sS --fail-with-body "$API/v1/jobs/$id/download" -H "$AUTH" \\ | ||
| | jq -r '.outputs[0].url' | xargs curl -sS --fail-with-body -o photo.webp`, |
There was a problem hiding this comment.
🟡 Failed jobs reach the download step
When status is failed or cancelled, the cURL sample still requests download. Users receive not_ready instead of the job failure.
Learn more
The poll loop stops on every terminal status. The server only permits download for a succeeded job in download, returning 400 not_ready otherwise. The shell script gives a misleading download error instead of the actual conversion status.
Example: A bad PNG fails conversion with status=failed. The loop breaks and the next request fails with not_ready rather than reporting failed.
Recommended fix: Check that status equals succeeded before downloading and report the failed or cancelled status separately.
Was this helpful? React with 👍 or 👎 to provide feedback.
| # 4. Download the output | ||
| url = api("GET", f"/v1/jobs/{job_id}/download")["outputs"][0]["url"] | ||
| open("photo.webp", "wb").write(requests.get(url).content)`, |
There was a problem hiding this comment.
🟡 Failed downloads become WebP files
When a signed URL expires or storage fails, the Python sample writes requests.get(url).content without checking the response. It saves an error body as photo.webp.
Learn more
The server provides presigned URLs valid for at most five minutes in download. Python requests.get does not raise for HTTP errors by default. The sample writes the response body to disk without checking its status, so an expired download is saved as an apparently successful output.
Example: The user pauses for more than five minutes after receiving the URL; storage returns an expiry error, and the script creates photo.webp containing that error.
Recommended fix: Call raise_for_status() on the GET response before writing its content. Apply the same status check to the Node sample's output fetch.
Was this helpful? React with 👍 or 👎 to provide feedback.
| onDragLeave={() => setDragging(false)} | ||
| onDrop={(e) => { | ||
| e.preventDefault(); | ||
| setDragging(false); | ||
| choose(e.dataTransfer.files[0] ?? null); |
There was a problem hiding this comment.
🟡 Previous file cannot be reselected after drop
After browsing for A and later dropping B, choose leaves the hidden input holding A. Choosing A again emits no change, so B stays selected.
Learn more
The browser file input only fires change when its selection changes. The drop handler calls choose directly without updating or clearing fileInput, and the Change button later clicks that same input. When the user selects its original file again, the input's value stays the same and the converter continues showing the dropped file.
Example: Browse first.png, drop second.png after the first conversion, then click Change and select first.png. The converter continues showing second.png.
Recommended fix: Clear fileInput.current.value when accepting a dropped file, and reset the input after file selection if repeated selection must work.
Was this helpful? React with 👍 or 👎 to provide feedback.
| Walk through it in the docs | ||
| </a> | ||
| </div> | ||
| <CodePanel samples={quickStart(apiBaseUrl)} maxHeight="max-h-[420px]" /> |
| const unmatched = overlay.actions | ||
| .map((a) => a.target) | ||
| .filter((target) => resolve(target) === undefined); | ||
| if (unmatched.length) { | ||
| throw new Error(`openapi/public.yaml targets nothing in the spec:\n ${unmatched.join("\n ")}`); | ||
| } | ||
| console.log(`public.yaml: ${overlay.actions.length} overlay targets match the spec`); |
| // 4. Download the output | ||
| const { outputs } = await api(\`/v1/jobs/\${job.id}/download\`); | ||
| const output = await fetch(outputs[0].url); | ||
| await writeFile("photo.webp", Buffer.from(await output.arrayBuffer()));`, |
|
| "name": { "type": "string", "example": "WebP" }, | ||
| "category": { | ||
| "type": "string", | ||
| "enum": ["image", "video", "audio", "document"], |
There was a problem hiding this comment.
Normal format categories are excluded
The new Format.category enum omits vector, pdf, presentation and spreadsheet. GET /v1/formats returns all four today, so clients generated from this spec can reject normal responses. Include all eight values from Category.
Confidence: 5/5.
| "enum": ["image", "video", "audio", "document"], | |
| "enum": ["image", "vector", "video", "audio", "pdf", "document", "presentation", "spreadsheet"], |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| while :; do | ||
| status=$(curl -sS --fail-with-body "$API/v1/jobs/$id" -H "$AUTH" | jq -r .status) | ||
| case $status in succeeded|failed|cancelled) break ;; esac | ||
| sleep 2 | ||
| done | ||
|
|
||
| # 4. Download the output | ||
| curl -sS --fail-with-body "$API/v1/jobs/$id/download" -H "$AUTH" \\ | ||
| | jq -r '.outputs[0].url' | xargs curl -sS --fail-with-body -o photo.webp`, |
There was a problem hiding this comment.
Quick start loops after errors
The cURL sample continues after failed requests. With an invalid key, the server returns an error object, jq -r .status yields null, and the poll loop never stops. It also tries to download after failed or cancelled.
Stop on command and pipeline failures, and require succeeded before downloading.
Confidence: 5/5.
| const output = await fetch(outputs[0].url); | ||
| await writeFile("photo.webp", Buffer.from(await output.arrayBuffer()));`, |
There was a problem hiding this comment.
Download errors overwrite output files
The Node.js and Python samples save download responses without checking whether the download succeeded. An expired signed URL or storage error can overwrite photo.webp with an error body and still appear successful.
Check output.ok in Node.js and call raise_for_status() in Python before writing the file.
Confidence: 5/5.
| "content": { | ||
| "application/json": { | ||
| "schema": { | ||
| "$ref": "#/components/schemas/Error" | ||
| } | ||
| } | ||
| "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } |
There was a problem hiding this comment.
Plain-text errors remain undocumented
The corrected 400 description says malformed JSON returns plain text, but content still declares only a JSON Error. Clients reading the spec cannot account for that response body. The overlay also replaces the explanation with only the two JSON error codes.
Add text/plain with a string schema, and retain the malformed-JSON case in the overlay description.
Confidence: 5/5.
| const [copied, setCopied] = useState(false); | ||
| const timer = useRef<ReturnType<typeof setTimeout>>(undefined); | ||
| useEffect(() => () => clearTimeout(timer.current), []); |
There was a problem hiding this comment.
CopyCode keeps copied when the selected sample changes. Copy cURL and switch to Python within 1.5 seconds: Python says “Copied,” but the clipboard still holds cURL.
Reset copied and its timer when value changes.
Confidence: 5/5.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| <span className="col-start-1 text-xs/4 text-ink-2 md:col-start-auto md:text-[13px]/4"> | ||
| <span className="md:hidden">Created </span> | ||
| {formatDate(key.created)} | ||
| </span> | ||
| <span className="col-start-1 text-xs/4 text-ink-2 md:col-start-auto md:text-[13px]/4"> | ||
| <span className="md:hidden">Last used </span> | ||
| {key.lastUsed} |
There was a problem hiding this comment.
Replacing the table with a list removes the date labels for screen readers on desktop. The header is aria-hidden, while md:hidden also removes “Created” and “Last used.” Users hear two dates without knowing which is which.
Use md:sr-only for those labels instead.
Confidence: 5/5.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
#61 (Blume docs) is merged. This branch is rebased onto it.
apps/docs, is the only docs home:/docsand/docs/*. convt-web has no/docsroutes; this PR doesn't add any back, and it doesn't add the old single-page/docs/api.crates/convt-server/openapi.json, which Blume renders throughapps/docs/openapi/public.yaml./dashboard/apiand/dashboard/api/convert.Fixed in the rebase
apps/web/src/routes/_site/docs/api.tsxandapps/web/src/generated/openapi.json. I also dropped this PR's own retired single-page reference (components/site/api-reference.tsx,lib/openapi.ts, the per-endpoint sample generator, and the/docswide-nav tweak). The corrected server spec is kept. No generated copy is needed: Blume and the web unit test both readcrates/convt-server/openapi.jsondirectly.apps/docs/openapi/public.yaml).$.components.schemas.CreateJob.properties.*and$.components.schemas.JobReservation.properties.*.idparameter entry now targets$.components.parameters.JobId. Every operation uses that shared component, so the old filter matched nothing.apps/docs/scripts/check-overlay.ts, run fromgenerate.tsbefore every docs build, now fails the build when a target matches nothing. I confirmed it fails on main's old overlay and passes on this one./docs,/docs/api,/docs/quick-start,/docs/reference/errors,/docs/reference/limitsand/docs/reference/formats. A unit test checks that each linked page exists inapps/docs/content, that/docs/apiis Blume's OpenAPI route on the server spec, and that the dashboard'sapiBaseUrlequals the docs'apiBase.What the spec correction changes in Blume
input_bytesfrom 1 to 2,000,000,000.Every operation page keeps its URL. Every listed status and error code was checked against a real local convt-server (results in the earlier description, in this PR's history).
Blume: Create a job, on this branch
Dashboard API Cloud converter, dark
Converter queued against the local API Two-step revoke
How to preview
There's no Vercel preview for this PR. The Vercel project (
convt, opencoredev team) builds only the static coming-soon landing (apps/webbuild:landing, which prerenders just/). It has deployed once, to production, from6ee857c, and no PR in this repo has had a Vercel check. The dashboard is a Cloudflare Worker needing Postgres, auth and the billing binding. The docs are the separateconvt-docsWorker. Neither runs on that Vercel project.Docs (verified on this branch):
Dashboard (verified on this branch):
The dashboard's docs links 404 on
localhost:3000, because/docsis the other Worker. Open them on the Blume preview port instead. To run real cloud conversions, also start convt-server and MinIO and setCONVT_API_URLandCONVT_WEB_TOKEN_SECRET(see.agents/skills/test-convt-webandtest-convt-server). Without them, the converter shows its not-configured state.Hosted docs preview (not run here: needs Cloudflare credentials):
convt-docshaspreview_urls: true, so a version upload gives a preview URL without touching production:Verification
bun run check,bun run check-typesandbun run buildpass. The build includes the docs build and the overlay check.blume validatereports no broken links, andblume buildpasses.apps/webunit tests (108) andcargo test -p convt-server(26) pass./dashboard/api,/dashboard/api/convertand/dashboardrender after the rebase, light and dark, at 1280 and 390 px, with no overflow.blume preview.Left for a follow-up
api.convt.appis still NXDOMAIN (CNV-36). Both apps use the Railway host, and the new test keeps the two copies in sync.@convt/sdkisn't on npm./dashboard/billing(CNV-35) is untouched.To show artifacts inline, enable in settings.