Skip to content

Correct the OpenAPI spec for Blume and redesign the dashboard API pages - #67

Open
leoisadev1 wants to merge 8 commits into
mainfrom
cursor/api-docs-redesign-9551
Open

leoisadev1 wants to merge 8 commits into
mainfrom
cursor/api-docs-redesign-9551

Conversation

@leoisadev1

@leoisadev1 leoisadev1 commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

#61 (Blume docs) is merged. This branch is rebased onto it.

  • Docs: Blume, in apps/docs, is the only docs home: /docs and /docs/*. convt-web has no /docs routes; this PR doesn't add any back, and it doesn't add the old single-page /docs/api.
  • Spec: the PR corrects crates/convt-server/openapi.json, which Blume renders through apps/docs/openapi/public.yaml.
  • Dashboard: the PR redesigns /dashboard/api and /dashboard/api/convert.

Fixed in the rebase

  • Conflicts. I took docs: rebuild convt.app/docs on Blume with a full API reference #61's deletions of apps/web/src/routes/_site/docs/api.tsx and apps/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 /docs wide-nav tweak). The corrected server spec is kept. No generated copy is needed: Blume and the web unit test both read crates/convt-server/openapi.json directly.
  • Overlay (apps/docs/openapi/public.yaml).
    • The five create-job field descriptions now target $.components.schemas.CreateJob.properties.* and $.components.schemas.JobReservation.properties.*.
    • The id parameter entry now targets $.components.parameters.JobId. Every operation uses that shared component, so the old filter matched nothing.
    • I dropped the shared wildcard entries for 401, 403, 404, 429 and 502. Those responses are spec components with their own descriptions. The 403 wildcard was also overwriting create's more specific 403 text.
    • All 34 targets now resolve.
  • Guard against this recurring. Blume skips unmatched overlay targets without a warning, which is how this went unnoticed. apps/docs/scripts/check-overlay.ts, run from generate.ts before 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.
  • Dashboard links. They pointed at anchors on the removed page. They now go to Blume pages: /docs, /docs/api, /docs/quick-start, /docs/reference/errors, /docs/reference/limits and /docs/reference/formats. A unit test checks that each linked page exists in apps/docs/content, that /docs/api is Blume's OpenAPI route on the server spec, and that the dashboard's apiBaseUrl equals the docs' apiBase.

What the spec correction changes in Blume

  • Create now lists 415, 422 and 500 and no longer lists a 404 it can't return.
  • Download no longer claims a billing 403.
  • The 429 entry explains the fixed window and the missing Retry-After header.
  • Fields show their bounds, for example input_bytes from 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/web build:landing, which prerenders just /). It has deployed once, to production, from 6ee857c, 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 separate convt-docs Worker. Neither runs on that Vercel project.

Docs (verified on this branch):

bun install
cd apps/docs && bun run build && bun run preview -- --port 4400
# open http://localhost:4400/docs, /docs/api and /docs/api/jobs/reserve-job

Dashboard (verified on this branch):

bun run dev:web   # web + billing on http://localhost:3000, with local Postgres, Mailpit and mocks
bun run db:seed
# sign in as pro@convt.test (the code arrives in Mailpit; dev:web prints its URL)
# open /dashboard/api and /dashboard/api/convert

The dashboard's docs links 404 on localhost:3000, because /docs is the other Worker. Open them on the Blume preview port instead. To run real cloud conversions, also start convt-server and MinIO and set CONVT_API_URL and CONVT_WEB_TOKEN_SECRET (see .agents/skills/test-convt-web and test-convt-server). Without them, the converter shows its not-configured state.

Hosted docs preview (not run here: needs Cloudflare credentials): convt-docs has preview_urls: true, so a version upload gives a preview URL without touching production:

cd apps/docs && bash scripts/deploy.sh --dry-run && bunx wrangler versions upload
# then open <preview URL>/docs

Verification

  • Root checks: bun run check, bun run check-types and bun run build pass. The build includes the docs build and the overlay check.
  • Blume: blume validate reports no broken links, and blume build passes.
  • Tests: apps/web unit tests (108) and cargo test -p convt-server (26) pass.
  • Dashboard: /dashboard/api, /dashboard/api/convert and /dashboard render after the rebase, light and dark, at 1280 and 390 px, with no overflow.
  • Docs pages: every page the dashboard links to returns 200 from blume preview.

Left for a follow-up

  • Price: this spec doesn't state the API price, which is pending approval. docs: rebuild convt.app/docs on Blume with a full API reference #61's overlay still says "1 cent" in two places; that's Leo's call.
  • Start before upload: returns 502 instead of a 4xx (CNV-40); documented as it behaves.
  • Base URL: api.convt.app is still NXDOMAIN (CNV-36). Both apps use the Railway host, and the new test keeps the two copies in sync.
  • SDK: samples use plain HTTP because @convt/sdk isn't on npm.
  • Billing page: /dashboard/billing (CNV-35) is untouched.

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

cursoragent and others added 8 commits October 7, 2026 13:19
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>
@cursor
cursor Bot force-pushed the cursor/api-docs-redesign-9551 branch from f0fc942 to 5fe7438 Compare October 7, 2026 13:31
@cursor cursor Bot changed the title Redesign /docs/api and the dashboard API pages from an accurate OpenAPI spec Correct the OpenAPI spec for Blume and redesign the dashboard API pages Oct 7, 2026
@leoisadev1
leoisadev1 marked this pull request as ready for review October 7, 2026 13:37

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 8 potential issues.

Devin Review

Comment on lines +409 to +412
"category": {
"type": "string",
"enum": ["image", "video", "audio", "document"],
"example": "image"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Suggested change
"category": {
"type": "string",
"enum": ["image", "video", "audio", "document"],
"example": "image"
"category": {
"type": "string",
"enum": ["image", "vector", "video", "audio", "pdf", "document", "presentation", "spreadsheet"],
"example": "image"
},

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +73 to +81
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");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +38 to +44
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`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +120 to +122
# 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)`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +255 to +259
onDragLeave={() => setDragging(false)}
onDrop={(e) => {
e.preventDefault();
setDragging(false);
choose(e.dataTransfer.files[0] ?? null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Devin Review


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]" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Language preference is not persisted

The dashboard renders CodePanel outside LanguageProvider. Its tab choice stays local and resets on remount despite the new persistence support.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +29 to +35
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`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Overlay check has a narrow guarantee

resolve verifies target existence, not whether updates appear in the rendered reference. An overlay-output check is needed to catch application failures.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +82 to +85
// 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()));`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Samples discard additional outputs

Each dashboard HTTP sample saves only outputs[0]. PNG-to-WebP uses one output, but adapting these snippets for multipage conversions drops the rest.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@greptile-apps

greptile-apps Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5 Tier: plus

[Medium risk] Redesigns dashboard API pages and updates OpenAPI spec.

Fix the formats response schema before merging; the remaining findings are smaller sample and UI fixes.

Fix All in Claude CodeFindings

  1. P1 Normal format categories are excluded ▶
  2. P2 Quick start loops after errors ▶
  3. P2 Download errors overwrite output files ▶
  4. P2 Plain-text errors remain undocumented ▶
  5. P2 New tab says copied ▶
  6. P2 Key dates lose their labels ▶

Summary

This PR corrects the server OpenAPI spec, repairs Blume overlay targets, adds a build-time target check, and redesigns the dashboard API pages.

  • The formats enum excludes four categories the server returns. Fix this before merging.
  • Smaller fixes cover sample error handling, plain-text 400 responses, copy confirmation, and screen-reader date labels.
  • Source inspection only: tests and browser previews were not run. Browser launch requires explicit permission.
  • leoisadev1 explicitly deferred the existing one-cent overlay price pending approval, start-before-upload returning 502 (CNV-40), the unresolved canonical API host (CNV-36), npm publication of @convt/sdk, and the untouched billing page (CNV-35).
  • leoisadev1 explicitly acknowledged local dashboard docs links returning 404 because docs use another Worker, and no Vercel preview because that project builds only the landing page.
  • leoisadev1 explicitly made Blume the only docs home and intentionally removed retired web reference files and generated spec copies.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Spec["Server OpenAPI spec"] --> Check["Check public overlay targets"]
  Overlay["Public overlay"] --> Check
  Check --> Build["Blume build"]
  Spec --> Build
  Overlay --> Build
  Build --> Docs["Docs Worker: /docs/*"]
  Dashboard["Dashboard API pages"] -->|Plain links| Docs
  Samples["HTTP quick-start samples"] --> Dashboard
Loading

Reviews (1) · Last reviewed commit: "web: keep the committed route tree" · Reviewed by Greptile

"name": { "type": "string", "example": "WebP" },
"category": {
"type": "string",
"enum": ["image", "video", "audio", "document"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Suggested change
"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!

Fix in Claude Code

Comment on lines +36 to +44
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`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Fix in Claude Code

Comment on lines +84 to +85
const output = await fetch(outputs[0].url);
await writeFile("photo.webp", Buffer.from(await output.arrayBuffer()));`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Fix in Claude Code

Comment on lines 54 to +55
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
"application/json": { "schema": { "$ref": "#/components/schemas/Error" } }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Fix in Claude Code

Comment on lines +148 to +150
const [copied, setCopied] = useState(false);
const timer = useRef<ReturnType<typeof setTimeout>>(undefined);
useEffect(() => () => clearTimeout(timer.current), []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 New tab says copied

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!

Fix in Claude Code

Comment on lines +306 to +312
<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}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Key dates lose their labels

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!

Fix in Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants