This commit is contained in:
@@ -1 +1 @@
|
|||||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Schematic API reference</title><link rel="stylesheet" href="/assets/upload.css"></head><body><main><p class="eyebrow">API V1</p><h1>Schematic rendering API</h1><p>Authenticate uploads with <code>X-API-Key</code>. Native binary OLE and ASCII SchDoc format 5.0 are accepted. Files are parsed asynchronously; HTTP 202 confirms admission, not rendering fidelity.</p><h2>Upload</h2><p><code>POST /api/v1/schematics/render</code></p><p>Multipart fields: <code>file</code> (required), <code>access</code> (private or unlisted; defaults to private), <code>expires_in</code> (optional seconds, bounded by server policy).</p><h2>Poll</h2><p><code>GET /api/v1/schematics/{id}/status</code></p><p>States: queued, processing, completed, failed. Private reads require <code>X-Viewer-Token</code> from the upload response, or the service API key. Completed responses include compatibility and diagnostics.</p><h2>Retrieve</h2><p><code>GET /api/v1/schematics/{id}/data</code> returns the versioned structured document. <code>GET /api/v1/schematics/{id}/svg</code> returns the generated vector drawing.</p><h2>View</h2><p>Open the exact <code>viewer_url</code> 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.</p><h2>Compare two schematics</h2><p><code>POST /api/v1/comparisons</code> (alias <code>/api/compare</code>) accepts multipart <code>old_file</code> and <code>new_file</code> with the same API key. It returns HTTP 202 and a private comparison URL. Poll <code>GET /api/v1/comparisons/{id}</code> using the returned read token for the completed summary and structural changes. <a href="/compare">Open comparison upload</a>.</p><h2>Errors</h2><p>JSON errors include <code>success: false</code> and <code>error.code</code> / <code>error.message</code>. 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.</p><p><a href="/openapi.json">Download OpenAPI schema</a> · <a href="/">Upload a schematic</a></p></main></body></html>
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Altium API reference</title><link rel="stylesheet" href="/assets/upload.css"></head><body><main><p class="eyebrow">API V1</p><h1>Altium rendering API</h1><p>Authenticate uploads with <code>X-API-Key</code>. 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.</p><h2>Upload</h2><p><code>POST /api/v1/schematics/render</code></p><p>Multipart fields: <code>file</code> (required), <code>access</code> (private or unlisted; defaults to private), <code>expires_in</code> (optional seconds, bounded by server policy).</p><h2>Poll</h2><p><code>GET /api/v1/schematics/{id}/status</code></p><p>States: queued, processing, completed, failed. Private reads require <code>X-Viewer-Token</code> from the upload response, or the service API key. Completed responses include compatibility and diagnostics.</p><h2>Retrieve</h2><p><code>GET /api/v1/schematics/{id}/data</code> returns the versioned structured document. <code>GET /api/v1/schematics/{id}/svg</code> returns the generated vector drawing.</p><h2>View</h2><p>Open the exact <code>viewer_url</code> 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.</p><h2>Compare two schematics</h2><p><code>POST /api/v1/comparisons</code> (alias <code>/api/compare</code>) accepts multipart <code>old_file</code> and <code>new_file</code> with the same API key. It returns HTTP 202 and a private comparison URL. Poll <code>GET /api/v1/comparisons/{id}</code> using the returned read token for the completed summary and structural changes. <a href="/compare">Open comparison upload</a>.</p><h2>Preview a PCB</h2><p><code>POST /api/v1/pcbs/render</code> accepts multipart <code>file</code> (.PcbDoc), <code>access</code> and optional <code>expires_in</code>. It returns HTTP 202 with <code>viewer_url</code>, <code>status_url</code> and a private <code>viewer_token</code> when applicable.</p><p>Poll <code>GET /api/v1/pcbs/{id}/status</code>. Read <code>GET /api/v1/pcbs/{id}/data</code> or <code>GET /api/v1/pcbs/{id}/svg</code>. The viewer is <code>/view/pcb/{id}</code>.</p><h2>Compare two PCBs</h2><p><code>POST /api/v1/pcbs/comparisons</code> accepts <code>old_file</code> and <code>new_file</code> (.PcbDoc) with the same upload authentication, access and expiry options. Poll <code>GET /api/v1/pcbs/comparisons/{id}</code>. Read <code>GET /api/v1/pcbs/comparisons/{id}/svg/{side}</code>, where side is <code>old</code> or <code>new</code>. Open the returned <code>/compare/pcb/{id}#token=…</code> URL for the side-by-side viewer.</p><p>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 <code>X-Viewer-Token</code> or the service key.</p><h2>Errors</h2><p>JSON errors include <code>success: false</code> and <code>error.code</code> / <code>error.message</code>. 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.</p><p><a href="/openapi.json">Download OpenAPI schema</a> · <a href="/">API playground and file upload</a></p></main></body></html>
|
||||||
|
|||||||
+19
-1
@@ -1,2 +1,20 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Altium schematic upload</title><link rel="stylesheet" href="/assets/upload.css"><script type="module" src="/assets/upload.js"></script></head><body><main><p class="eyebrow">NATIVE ALTIUM · SCHDOC</p><h1>Open your schematic.</h1><p>Upload a native schematic to create a private, expiring viewer link.</p><form id="upload"><label for="key">API key</label><input id="key" type="password" autocomplete="off" placeholder="Configured service key"><label for="file">Schematic file</label><input id="file" type="file" accept=".SchDoc" required><button type="submit">Render schematic <span aria-hidden="true">↗</span></button></form><output id="result" role="status"></output><p class="note">Compatibility diagnostics are returned by the API. Review them before relying on visual fidelity.</p><a href="/compare">Compare two schematics</a> · <a href="/docs">API reference</a></main></body></html>
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Altium API playground</title><link rel="stylesheet" href="/assets/upload.css?v=pcb-playground-1"><script type="module" src="/assets/upload.js?v=pcb-playground-1"></script></head><body class="playground"><main>
|
||||||
|
<p class="eyebrow">NATIVE ALTIUM · SCHDOC + PCBDOC</p><h1>Render or compare your design.</h1><p>Test the rendering APIs with your own files. Uploads create expiring viewer links.</p>
|
||||||
|
<div class="playground-grid"><section aria-labelledby="test-title"><h2 id="test-title">Test an API</h2>
|
||||||
|
<form id="upload"><fieldset id="upload-fields"><label for="operation">Operation</label><select id="operation"><option value="schematic">Preview schematic (.SchDoc)</option><option value="schematic-comparison">Compare schematics (.SchDoc)</option><option value="pcb">Preview PCB (.PcbDoc)</option><option value="pcb-comparison">Compare PCBs (.PcbDoc)</option></select><p class="endpoint"><code id="selected-endpoint">POST /api/v1/schematics/render</code></p>
|
||||||
|
<label for="key">API key</label><input id="key" type="password" autocomplete="off" placeholder="Configured service key"><p class="field-hint">Used for this upload only; never saved in browser storage.</p>
|
||||||
|
<div id="single-input"><label id="file-label" for="file">Schematic file</label><input id="file" type="file" accept=".SchDoc" required></div>
|
||||||
|
<div id="comparison-inputs" hidden><label for="old-file">Original file</label><input id="old-file" type="file" accept=".SchDoc" disabled><label for="new-file">Modified file</label><input id="new-file" type="file" accept=".SchDoc" disabled></div>
|
||||||
|
<div class="upload-options"><div><label for="access">Link access</label><select id="access"><option value="private">Private (read token required)</option><option value="unlisted">Unlisted (anyone with the link)</option></select></div><div><label for="expires-in">Expires in (seconds)</label><input id="expires-in" type="number" min="1" step="1" placeholder="Server default"></div></div>
|
||||||
|
</fieldset><button id="submit-upload" type="submit">Render schematic <span aria-hidden="true">↗</span></button></form>
|
||||||
|
<output id="result" role="status" aria-live="polite"></output><div id="viewer-link"></div><details id="response-details" hidden><summary>API response</summary><pre id="response-json"></pre></details>
|
||||||
|
<p class="note">Compatibility is partial. Review parser diagnostics before relying on visual fidelity. Files are processed asynchronously; a queued response confirms acceptance.</p></section>
|
||||||
|
<section class="api-list" aria-labelledby="api-title"><h2 id="api-title">Available APIs</h2><p>Uploads use <code>multipart/form-data</code> and <code>X-API-Key</code>. Select a capability to test it.</p>
|
||||||
|
<article><button type="button" class="api-choice" data-operation="schematic">Schematic preview</button><code>POST /api/v1/schematics/render</code><p>File field: <code>file</code> · .SchDoc</p><code>GET /api/v1/schematics/{id}/status</code><code>GET /api/v1/schematics/{id}/data</code><code>GET /api/v1/schematics/{id}/svg</code><code>GET /view/{id}</code></article>
|
||||||
|
<article><button type="button" class="api-choice" data-operation="schematic-comparison">Schematic comparison</button><code>POST /api/v1/comparisons</code><p>File fields: <code>old_file</code>, <code>new_file</code> · .SchDoc</p><code>GET /api/v1/comparisons/{id}</code><code>GET /api/v1/comparisons/{id}/svg/{side}</code><code>GET /compare/{id}</code><p>Compatible aliases: <code>POST /api/compare</code>, <code>GET /api/compare/{id}</code>.</p></article>
|
||||||
|
<article><button type="button" class="api-choice" data-operation="pcb">PCB preview</button><code>POST /api/v1/pcbs/render</code><p>File field: <code>file</code> · .PcbDoc</p><code>GET /api/v1/pcbs/{id}/status</code><code>GET /api/v1/pcbs/{id}/data</code><code>GET /api/v1/pcbs/{id}/svg</code><code>GET /view/pcb/{id}</code></article>
|
||||||
|
<article><button type="button" class="api-choice" data-operation="pcb-comparison">PCB comparison</button><code>POST /api/v1/pcbs/comparisons</code><p>File fields: <code>old_file</code>, <code>new_file</code> · .PcbDoc</p><code>GET /api/v1/pcbs/comparisons/{id}</code><code>GET /api/v1/pcbs/comparisons/{id}/svg/{side}</code><code>GET /compare/pcb/{id}</code></article>
|
||||||
|
<p class="field-hint">Read private results with <code>X-Viewer-Token</code> or the service key. SVG <code>side</code> is <code>old</code> or <code>new</code>. Upload options: <code>access</code>, <code>expires_in</code>.</p></section></div>
|
||||||
|
<nav class="page-links"><a href="/docs">API reference</a><a href="/openapi.json">OpenAPI schema</a><a href="/compare">Schematic comparison upload</a></nav>
|
||||||
|
</main></body></html>
|
||||||
|
|||||||
@@ -13,3 +13,34 @@ button:disabled { opacity: .5; cursor: wait; }
|
|||||||
output { display: block; font-size: 14px; overflow-wrap: anywhere; line-height: 1.6; }
|
output { display: block; font-size: 14px; overflow-wrap: anywhere; line-height: 1.6; }
|
||||||
a { color: #244d49; font-size: 13px; }
|
a { color: #244d49; font-size: 13px; }
|
||||||
.note { margin-top: 28px; font-size: 12px; }
|
.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; } }
|
||||||
|
|||||||
+89
-15
@@ -1,16 +1,90 @@
|
|||||||
const form = document.querySelector('#upload'), result = document.querySelector('#result');
|
const operations = {
|
||||||
form.addEventListener('submit',async e => {
|
schematic: { endpoint:'/api/v1/schematics/render', suffix:'.SchDoc', label:'Render schematic', fileLabel:'Schematic file', comparison:false },
|
||||||
e.preventDefault();
|
'schematic-comparison': { endpoint:'/api/v1/comparisons', suffix:'.SchDoc', label:'Compare schematics', comparison:true },
|
||||||
const button = form.querySelector('button'); button.disabled = true; result.textContent = 'Uploading…';
|
pcb: { endpoint:'/api/v1/pcbs/render', suffix:'.PcbDoc', label:'Render PCB', fileLabel:'PCB file', comparison:false },
|
||||||
const headers = {}, key = document.querySelector('#key').value;
|
'pcb-comparison': { endpoint:'/api/v1/pcbs/comparisons', suffix:'.PcbDoc', label:'Compare PCBs', comparison:true }
|
||||||
if (key) headers['X-API-Key'] = key;
|
};
|
||||||
const data = new FormData(); data.append('file',document.querySelector('#file').files[0]);
|
const form=document.querySelector('#upload'), result=document.querySelector('#result');
|
||||||
try {
|
const operation=document.querySelector('#operation'), button=document.querySelector('#submit-upload');
|
||||||
const response = await fetch('/api/v1/schematics/render',{ method:'POST',headers,body:data });
|
const fields=document.querySelector('#upload-fields'), viewerLink=document.querySelector('#viewer-link');
|
||||||
const body = await response.json();
|
const details=document.querySelector('#response-details'), json=document.querySelector('#response-json');
|
||||||
if (!response.ok) throw new Error(body.error?.message || 'Upload failed');
|
let polling=null, uploading=false;
|
||||||
result.textContent = 'Queued. ';
|
function reset(){
|
||||||
const link = document.createElement('a'); link.href = body.viewer_url; link.textContent = 'Open schematic'; result.append(link);
|
polling?.abort();polling=null;result.textContent='';viewerLink.replaceChildren();details.hidden=true;json.textContent='';
|
||||||
} catch (e) { result.textContent = e.message; }
|
}
|
||||||
finally { button.disabled = false; }
|
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});
|
||||||
|
|||||||
مرجع در شماره جدید
مسدود کردن کاربر