diff --git a/frontend/docs.html b/frontend/docs.html index a34eaa0..1e33173 100644 --- a/frontend/docs.html +++ b/frontend/docs.html @@ -1 +1 @@ -Schematic API reference

API V1

Schematic rendering API

Authenticate uploads with X-API-Key. Native binary OLE and ASCII SchDoc format 5.0 are accepted. Files are parsed asynchronously; HTTP 202 confirms admission, not rendering fidelity.

Upload

POST /api/v1/schematics/render

Multipart fields: file (required), access (private or unlisted; defaults to private), expires_in (optional seconds, bounded by server policy).

Poll

GET /api/v1/schematics/{id}/status

States: queued, processing, completed, failed. Private reads require X-Viewer-Token from the upload response, or the service API key. Completed responses include compatibility and diagnostics.

Retrieve

GET /api/v1/schematics/{id}/data returns the versioned structured document. GET /api/v1/schematics/{id}/svg returns the generated vector drawing.

View

Open the exact viewer_url from the upload response. The private token is in its fragment. It is stored for this browser tab and sent only as a request header. The viewer has no panels or toolbar.

Compare two schematics

POST /api/v1/comparisons (alias /api/compare) accepts multipart old_file and new_file with the same API key. It returns HTTP 202 and a private comparison URL. Poll GET /api/v1/comparisons/{id} using the returned read token for the completed summary and structural changes. Open comparison upload.

Errors

JSON errors include success: false and error.code / error.message. Common HTTP codes: 401 invalid service key; 404 missing or inaccessible schematic; 409 still processing; 410 expired; 413 too large; 415 wrong extension; 422 invalid schematic; 503 storage unavailable or queue full.

Download OpenAPI schema · Upload a schematic

+Altium API reference

API V1

Altium rendering API

Authenticate uploads with X-API-Key. Schematic endpoints accept native binary OLE and ASCII SchDoc format 5.0. Separate PCB endpoints accept the diagnosed native PcbDoc V6 subset. Files are parsed asynchronously; HTTP 202 confirms admission, not rendering fidelity.

Upload

POST /api/v1/schematics/render

Multipart fields: file (required), access (private or unlisted; defaults to private), expires_in (optional seconds, bounded by server policy).

Poll

GET /api/v1/schematics/{id}/status

States: queued, processing, completed, failed. Private reads require X-Viewer-Token from the upload response, or the service API key. Completed responses include compatibility and diagnostics.

Retrieve

GET /api/v1/schematics/{id}/data returns the versioned structured document. GET /api/v1/schematics/{id}/svg returns the generated vector drawing.

View

Open the exact viewer_url from the upload response. The private token is in its fragment. It is stored for this browser tab and sent only as a request header. The viewer has no panels or toolbar.

Compare two schematics

POST /api/v1/comparisons (alias /api/compare) accepts multipart old_file and new_file with the same API key. It returns HTTP 202 and a private comparison URL. Poll GET /api/v1/comparisons/{id} using the returned read token for the completed summary and structural changes. Open comparison upload.

Preview a PCB

POST /api/v1/pcbs/render accepts multipart file (.PcbDoc), access and optional expires_in. It returns HTTP 202 with viewer_url, status_url and a private viewer_token when applicable.

Poll GET /api/v1/pcbs/{id}/status. Read GET /api/v1/pcbs/{id}/data or GET /api/v1/pcbs/{id}/svg. The viewer is /view/pcb/{id}.

Compare two PCBs

POST /api/v1/pcbs/comparisons accepts old_file and new_file (.PcbDoc) with the same upload authentication, access and expiry options. Poll GET /api/v1/pcbs/comparisons/{id}. Read GET /api/v1/pcbs/comparisons/{id}/svg/{side}, where side is old or new. Open the returned /compare/pcb/{id}#token=… URL for the side-by-side viewer.

PCB preview and comparison have layer controls behind a menu button. Compatibility is partial: fonts, some native shapes, plane reconstruction and mask expansions have limitations. Structural comparison is not DRC or proof of electrical equivalence. Private reads require X-Viewer-Token or the service key.

Errors

JSON errors include success: false and error.code / error.message. Common HTTP codes: 401 invalid service key; 404 missing or inaccessible schematic; 409 still processing; 410 expired; 413 too large; 415 wrong extension; 422 invalid schematic; 503 storage unavailable or queue full.

Download OpenAPI schema · API playground and file upload

diff --git a/frontend/index.html b/frontend/index.html index 47bd9db..f829b74 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,2 +1,20 @@ -Altium schematic upload

NATIVE ALTIUM · SCHDOC

Open your schematic.

Upload a native schematic to create a private, expiring viewer link.

Compatibility diagnostics are returned by the API. Review them before relying on visual fidelity.

Compare two schematics · API reference
+Altium API playground
+

NATIVE ALTIUM · SCHDOC + PCBDOC

Render or compare your design.

Test the rendering APIs with your own files. Uploads create expiring viewer links.

+

Test an API

+

POST /api/v1/schematics/render

+

Used for this upload only; never saved in browser storage.

+
+ +
+
+ +

Compatibility is partial. Review parser diagnostics before relying on visual fidelity. Files are processed asynchronously; a queued response confirms acceptance.

+

Available APIs

Uploads use multipart/form-data and X-API-Key. Select a capability to test it.

+
POST /api/v1/schematics/render

File field: file · .SchDoc

GET /api/v1/schematics/{id}/statusGET /api/v1/schematics/{id}/dataGET /api/v1/schematics/{id}/svgGET /view/{id}
+
POST /api/v1/comparisons

File fields: old_file, new_file · .SchDoc

GET /api/v1/comparisons/{id}GET /api/v1/comparisons/{id}/svg/{side}GET /compare/{id}

Compatible aliases: POST /api/compare, GET /api/compare/{id}.

+
POST /api/v1/pcbs/render

File field: file · .PcbDoc

GET /api/v1/pcbs/{id}/statusGET /api/v1/pcbs/{id}/dataGET /api/v1/pcbs/{id}/svgGET /view/pcb/{id}
+
POST /api/v1/pcbs/comparisons

File fields: old_file, new_file · .PcbDoc

GET /api/v1/pcbs/comparisons/{id}GET /api/v1/pcbs/comparisons/{id}/svg/{side}GET /compare/pcb/{id}
+

Read private results with X-Viewer-Token or the service key. SVG side is old or new. Upload options: access, expires_in.

+ +
diff --git a/frontend/upload.css b/frontend/upload.css index 28e5ae0..49a0904 100644 --- a/frontend/upload.css +++ b/frontend/upload.css @@ -13,3 +13,34 @@ button:disabled { opacity: .5; cursor: wait; } output { display: block; font-size: 14px; overflow-wrap: anywhere; line-height: 1.6; } a { color: #244d49; font-size: 13px; } .note { margin-top: 28px; font-size: 12px; } + +/* Home API playground only: existing upload and docs layouts stay independent. */ +.playground { align-items: start; padding: clamp(20px,4vw,56px); } +.playground main { width: min(100%,1120px); margin: 0 auto; } +.playground h1 { max-width: 750px; } +.playground-grid { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr); gap: clamp(24px,5vw,64px); margin-top: 28px; } +.playground h2 { font-size: 20px; margin: 0 0 16px; } +.playground form { margin-top: 0; } +.playground fieldset { border: 0; margin: 0; padding: 0; display: grid; gap: 10px; min-width: 0; } +.playground select { width: 100%; padding: 13px; border: 1px solid #ccd4ce; background: white; color: inherit; font: inherit; border-radius: 5px; } +.playground label { display: block; margin-bottom: 6px; } +.playground [hidden] { display: none !important; } +.playground .endpoint { margin: 0; font-size: 12px; overflow-wrap: anywhere; } +.playground .field-hint { font-size: 12px; margin: 0; } +.playground .upload-options { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } +.playground .api-list { min-width: 0; } +.playground .api-list > p { font-size: 13px; } +.playground article { margin: 18px 0; padding: 16px; border: 1px solid #d5ddd6; border-radius: 6px; background: #ffffff80; } +.playground article > code { display: block; font-size: 12px; line-height: 1.8; overflow-wrap: anywhere; } +.playground article p { font-size: 12px; margin: 8px 0; } +.playground .api-choice { display: inline-block; padding: 0; margin: 0 0 10px; background: transparent; color: #244d49; font-weight: 650; text-align: left; border-radius: 0; } +.playground .api-choice[aria-pressed="true"] { text-decoration: underline; text-underline-offset: 4px; } +.playground button:focus-visible, .playground select:focus-visible, .playground a:focus-visible { outline: 2px solid #51706d; outline-offset: 3px; } +.playground #viewer-link { margin-top: 12px; } +.playground #viewer-link a { display: inline-block; padding: 10px 14px; border: 1px solid #9bb4a7; border-radius: 4px; } +.playground details { margin-top: 14px; font-size: 12px; } +.playground summary { cursor: pointer; } +.playground pre { max-height: 320px; overflow: auto; padding: 14px; background: #e9eee8; border-radius: 5px; white-space: pre-wrap; overflow-wrap: anywhere; } +.playground .page-links { display: flex; flex-wrap: wrap; gap: 18px; padding: 24px 0; border-top: 1px solid #d5ddd6; } +@media (max-width: 760px) { .playground-grid { grid-template-columns: 1fr; } } +@media (max-width: 400px) { .playground .upload-options { grid-template-columns: 1fr; } } diff --git a/frontend/upload.js b/frontend/upload.js index f363df3..dbca059 100644 --- a/frontend/upload.js +++ b/frontend/upload.js @@ -1,16 +1,90 @@ -const form = document.querySelector('#upload'), result = document.querySelector('#result'); -form.addEventListener('submit',async e => { - e.preventDefault(); - const button = form.querySelector('button'); button.disabled = true; result.textContent = 'Uploading…'; - const headers = {}, key = document.querySelector('#key').value; - if (key) headers['X-API-Key'] = key; - const data = new FormData(); data.append('file',document.querySelector('#file').files[0]); - try { - const response = await fetch('/api/v1/schematics/render',{ method:'POST',headers,body:data }); - const body = await response.json(); - if (!response.ok) throw new Error(body.error?.message || 'Upload failed'); - result.textContent = 'Queued. '; - const link = document.createElement('a'); link.href = body.viewer_url; link.textContent = 'Open schematic'; result.append(link); - } catch (e) { result.textContent = e.message; } - finally { button.disabled = false; } +const operations = { + schematic: { endpoint:'/api/v1/schematics/render', suffix:'.SchDoc', label:'Render schematic', fileLabel:'Schematic file', comparison:false }, + 'schematic-comparison': { endpoint:'/api/v1/comparisons', suffix:'.SchDoc', label:'Compare schematics', comparison:true }, + pcb: { endpoint:'/api/v1/pcbs/render', suffix:'.PcbDoc', label:'Render PCB', fileLabel:'PCB file', comparison:false }, + 'pcb-comparison': { endpoint:'/api/v1/pcbs/comparisons', suffix:'.PcbDoc', label:'Compare PCBs', comparison:true } +}; +const form=document.querySelector('#upload'), result=document.querySelector('#result'); +const operation=document.querySelector('#operation'), button=document.querySelector('#submit-upload'); +const fields=document.querySelector('#upload-fields'), viewerLink=document.querySelector('#viewer-link'); +const details=document.querySelector('#response-details'), json=document.querySelector('#response-json'); +let polling=null, uploading=false; +function reset(){ + polling?.abort();polling=null;result.textContent='';viewerLink.replaceChildren();details.hidden=true;json.textContent=''; +} +function configure(){ + if(uploading)return; + reset();const config=operations[operation.value]; + document.querySelector('#selected-endpoint').textContent=`POST ${config.endpoint}`; + document.querySelector('#single-input').hidden=config.comparison; + document.querySelector('#comparison-inputs').hidden=!config.comparison; + document.querySelector('#file-label').textContent=config.fileLabel||'Schematic file'; + for(const [selector,active]of [['#file',!config.comparison],['#old-file',config.comparison],['#new-file',config.comparison]]){ + const input=document.querySelector(selector);input.accept=config.suffix;input.required=active;input.disabled=!active;input.value=''; + } + button.textContent=config.label+' ↗'; + for(const choice of document.querySelectorAll('[data-operation]'))choice.setAttribute('aria-pressed',String(choice.dataset.operation===operation.value)); +} +operation.addEventListener('change',configure); +for(const choice of document.querySelectorAll('[data-operation]'))choice.addEventListener('click',()=>{ + if(uploading)return;operation.value=choice.dataset.operation;configure();operation.focus(); }); +configure(); +function showResponse(body){json.textContent=JSON.stringify(body,null,2);details.hidden=false;} +async function decode(response){ + let body; + try{body=await response.json();}catch{throw new Error(`Server returned HTTP ${response.status} without a JSON response.`);} + if(!response.ok)throw new Error(body.error?.message||`Request failed (HTTP ${response.status}).`); + return body; +} +function pause(signal){ + return new Promise((resolve,reject)=>{ + const abort=()=>{clearTimeout(timer);reject(new DOMException('Stopped','AbortError'));}; + const timer=setTimeout(()=>{signal.removeEventListener('abort',abort);resolve();},1000); + signal.addEventListener('abort',abort,{once:true}); + if(signal.aborted)abort(); + }); +} +async function poll(created,controller){ + const headers=created.viewer_token?{'X-Viewer-Token':created.viewer_token}:{}; + const status=new URL(created.status_url,location.origin); + if(status.origin!==location.origin)throw new Error('Status URL must use this service origin. Check PUBLIC_BASE_URL.'); + for(;;){ + await pause(controller.signal); + const body=await decode(await fetch(status.pathname+status.search,{headers,signal:controller.signal,cache:'no-store'})); + if(controller.signal.aborted)return; + showResponse(body); + if(body.status==='failed')throw new Error(body.error?.message||'Processing failed.'); + if(body.status==='completed'){ + const summary=body.summary?` ${Object.entries(body.summary).map(([name,count])=>`${count} ${name}`).join(' · ')}.`:''; + result.textContent=`Completed.${summary} Open the viewer below.`;return; + } + result.textContent=body.status==='processing'?'Processing your files…':'Queued. Waiting for a worker…'; + } +} +form.addEventListener('submit',async event=>{ + event.preventDefault();if(uploading)return;reset(); + const config=operations[operation.value],data=new FormData(); + const inputs=config.comparison?[['old_file','#old-file'],['new_file','#new-file']]:[['file','#file']]; + for(const [name,selector]of inputs){ + const file=document.querySelector(selector).files[0]; + if(!file||!file.name.toLowerCase().endsWith(config.suffix.toLowerCase())){result.textContent=`Select ${config.suffix} files for this operation.`;return;} + data.append(name,file); + } + data.append('access',document.querySelector('#access').value); + const expiry=document.querySelector('#expires-in').value; + if(expiry){if(!/^\d+$/.test(expiry)||Number(expiry)<1){result.textContent='Expiry must be a positive whole number of seconds.';return;}data.append('expires_in',expiry);} + const headers={},key=document.querySelector('#key').value;if(key)headers['X-API-Key']=key; + uploading=true;button.disabled=true;fields.disabled=true;result.textContent='Uploading…'; + try{ + const created=await decode(await fetch(config.endpoint,{method:'POST',headers,body:data}));showResponse(created); + const url=new URL(created.viewer_url||created.url,location.origin); + if(url.origin!==location.origin)throw new Error('Viewer URL must use this service origin. Check PUBLIC_BASE_URL.'); + const link=document.createElement('a');link.href=url.href;link.target='_blank';link.rel='noopener noreferrer';link.textContent=config.comparison?'Open comparison ↗':'Open viewer ↗';viewerLink.append(link); + result.textContent='Queued. You can open the viewer while processing continues.'; + const controller=new AbortController();polling=controller; + poll(created,controller).catch(error=>{if(error.name!=='AbortError'&&!controller.signal.aborted)result.textContent=`${error.message} The accepted upload is not retried automatically.`;}); + }catch(error){result.textContent=`${error.message} No automatic upload retry was made.`;} + finally{uploading=false;button.disabled=false;fields.disabled=false;} +}); +addEventListener('pagehide',()=>polling?.abort(),{once:true});