اضافه شدن قابلیت share و نگهداری بلندمدت
Verify / test (push) Failing after 22s

This commit is contained in:
2026-11-09 14:55:36 +03:30
والد a4e8f0c6c0
کامیت 19824bd180
27فایلهای تغییر یافته به همراه267 افزوده شده و 51 حذف شده
+1 -1
مشاهده پرونده
@@ -1,2 +1,2 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Compare Altium schematics</title><link rel="stylesheet" href="/assets/upload.css"><script type="module" src="/assets/compare-upload.js"></script></head><body><main><p class="eyebrow">NATIVE ALTIUM · STRUCTURAL DIFF</p><h1>Compare schematics.</h1><p>Select the original and modified SchDoc files.</p><form id="compare-upload"><label for="key">API key</label><input id="key" type="password" autocomplete="off" placeholder="Configured service key"><label for="old-file">Original schematic</label><input id="old-file" type="file" accept=".SchDoc" required><label for="new-file">Modified schematic</label><input id="new-file" type="file" accept=".SchDoc" required><button type="submit">Compare schematics <span aria-hidden="true">↗</span></button></form><output id="result" role="status"></output><p class="note">Comparison uses native objects and saved geometry. Connection graphics are compared without a compiled electrical netlist.</p><a href="/">Single schematic viewer</a></main></body></html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Compare Altium schematics</title><link rel="stylesheet" href="/assets/upload.css"><script type="module" src="/assets/compare-upload.js?v=expiry-share-1"></script><link rel="stylesheet" href="/assets/share.css?v=1"></head><body><main><p class="eyebrow">NATIVE ALTIUM · STRUCTURAL DIFF</p><h1>Compare schematics.</h1><p>Select the original and modified SchDoc files.</p><form id="compare-upload"><label for="key">API key</label><input id="key" type="password" autocomplete="off" placeholder="Configured service key"><label for="old-file">Original schematic</label><input id="old-file" type="file" accept=".SchDoc" required><label for="new-file">Modified schematic</label><input id="new-file" type="file" accept=".SchDoc" required><label for="expires-in">Expires in (seconds; -1 for unlimited)</label><input id="expires-in" type="number" min="-1" step="1" placeholder="Server default"><button type="submit">Compare schematics <span aria-hidden="true">↗</span></button></form><output id="result" role="status"></output><div id="upload-share"></div><p class="note">Comparison uses native objects and saved geometry. Connection graphics are compared without a compiled electrical netlist.</p><a href="/">Single schematic viewer</a></main></body></html>
+5 -2
مشاهده پرونده
@@ -1,12 +1,15 @@
import { mountShare } from './share.js?v=1';
const form=document.querySelector('#compare-upload'),result=document.querySelector('#result');
form.addEventListener('submit',async e=>{
e.preventDefault();const button=form.querySelector('button');button.disabled=true;result.textContent='Uploading both schematics…';
e.preventDefault();document.querySelector('#upload-share').replaceChildren();const button=form.querySelector('button');button.disabled=true;result.textContent='Uploading both schematics…';
const headers={},key=document.querySelector('#key').value;
if(key)headers['X-API-Key']=key;
const body=new FormData();body.append('old_file',document.querySelector('#old-file').files[0]);body.append('new_file',document.querySelector('#new-file').files[0]);
const expiry=document.querySelector('#expires-in').value;
if(expiry)body.append('expires_in',expiry);
try {
const response=await fetch('/api/v1/comparisons',{method:'POST',headers,body});const data=await response.json();
if(!response.ok)throw new Error(data.error?.message||'Comparison upload failed');
result.textContent='Queued. ';const link=document.createElement('a');link.href=data.url;link.textContent='Open comparison';result.append(link);
result.textContent='Queued. ';const link=document.createElement('a');link.href=data.url;link.textContent='Open comparison';result.append(link);mountShare(document.querySelector('#upload-share'),data.viewer_url||data.url);
}catch(error){result.textContent=error.message;}finally{button.disabled=false;}
});
+2 -2
مشاهده پرونده
@@ -1,7 +1,7 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Schematic comparison</title><link rel="stylesheet" href="/fonts.css"><link rel="stylesheet" href="/assets/viewer.css"><link rel="stylesheet" href="/assets/compare.css?v=fullscreen-menu-3"><script type="module" src="/assets/compare.js?v=fullscreen-menu-3"></script></head><body>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Schematic comparison</title><link rel="stylesheet" href="/fonts.css"><link rel="stylesheet" href="/assets/viewer.css"><link rel="stylesheet" href="/assets/compare.css?v=fullscreen-menu-3"><script type="module" src="/assets/compare.js?v=expiry-share-1"></script><link rel="stylesheet" href="/assets/share.css?v=1"></head><body>
<button id="menu-toggle" type="button" aria-label="Show menu" title="Show menu" aria-expanded="false" aria-controls="comparison-toolbar scope comparison-changes"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg></button>
<header id="comparison-toolbar" data-comparison-menu hidden><a href="/compare">Schematic Diff</a><div id="summary" aria-live="polite"></div><div class="controls"><label><input id="sync" type="checkbox" checked> Sync views</label><label><input id="highlights" type="checkbox" checked> Highlights</label><label><input id="only-changes" type="checkbox"> Changes only</label><button id="fit" type="button">Fit</button></div></header>
<header id="comparison-toolbar" data-comparison-menu hidden><a href="/compare">Schematic Diff</a><div id="summary" aria-live="polite"></div><div class="controls"><label><input id="sync" type="checkbox" checked> Sync views</label><label><input id="highlights" type="checkbox" checked> Highlights</label><label><input id="only-changes" type="checkbox"> Changes only</label><div id="comparison-share"></div><button id="fit" type="button">Fit</button></div></header>
<p id="scope" data-comparison-menu hidden>Native object comparison · Connection graphics, not a compiled electrical netlist.</p>
<main class="workspace"><section class="pane"><h2 data-comparison-menu hidden>Original <span id="old-name"></span></h2><div id="old-surface" class="surface" tabindex="0" aria-label="Original schematic"></div></section><section class="pane"><h2 data-comparison-menu hidden>Modified <span id="new-name"></span></h2><div id="new-surface" class="surface" tabindex="0" aria-label="Modified schematic"></div></section><aside id="comparison-changes" data-comparison-menu hidden><div class="change-controls"><label for="filter">Changes</label><select id="filter"><option value="all">All</option><option value="added">Added</option><option value="removed">Removed</option><option value="modified">Modified</option><option value="moved">Moved</option></select><button id="previous" aria-label="Previous change">↑</button><button id="next" aria-label="Next change">↓</button></div><nav id="changes" aria-label="Schematic changes"></nav><pre id="details" aria-live="polite"></pre></aside></main>
<output id="state" class="loading" role="status">Loading comparison…</output></body></html>
+3 -1
مشاهده پرونده
@@ -1,3 +1,4 @@
import { mountShare, viewerURL } from './share.js?v=1';
import { bindCamera } from './interactions.js';
const menuToggle=document.querySelector('#menu-toggle');
const menuPanels=document.querySelectorAll('[data-comparison-menu]');
@@ -103,6 +104,7 @@ async function start(){
document.querySelector('#previous').addEventListener('click',()=>next(-1));document.querySelector('#next').addEventListener('click',()=>next(1));
for(const svg of svgs)svg.addEventListener('click',event=>{const id=event.target.closest('[data-change-id]')?.dataset.changeId;if(byId.has(id))select(byId.get(id));});
applyFilters();
function expire(){const remaining=result.expires_at*1000-Date.now();if(remaining<=0){for(const view of views)view.destroy();document.querySelector('.workspace').replaceChildren();state.textContent='This comparison link has expired';}else setTimeout(expire,Math.min(2147483647,remaining));}expire();
mountShare(document.querySelector('#comparison-share'),viewerURL(location.pathname,token));
function expire(){if(result.expires_at===-1)return;const remaining=result.expires_at*1000-Date.now();if(remaining<=0){for(const view of views)view.destroy();document.querySelector('.workspace').replaceChildren();state.textContent='This comparison link has expired';}else setTimeout(expire,Math.min(2147483647,remaining));}expire();
}
start().catch(error=>{state.classList.remove('loading');state.textContent=error.name==='AbortError'?'':error.message;});
+1 -1
مشاهده پرونده
@@ -1 +1 @@
<!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>
<!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: -1 for unlimited, or positive 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>Unlimited links and Share</h2><p>All upload operations accept <code>expires_in=-1</code> to retain files and links without automatic expiration. Responses use <code>expires_at: -1</code>. Zero and lower negative values are invalid. The Share button provides an iframe snippet containing the complete viewer URL, including a private read token when applicable. Sharing that code grants read access. Embedding remains restricted to this service and <code>https://git.sadentel.net</code>.</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>
+4 -4
مشاهده پرونده
@@ -1,14 +1,14 @@
<!doctype 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>
<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=expiry-share-1"></script><link rel="stylesheet" href="/assets/share.css?v=1"></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>
<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>
<p class="field-hint">Use -1 to keep the files and viewer link indefinitely. Leave blank for the server default.</p></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><div id="upload-share"></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>
+2 -2
مشاهده پرونده
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PCB Viewer</title><link rel="stylesheet" href="/fonts.css"><link rel="stylesheet" href="/assets/viewer.css"><link rel="stylesheet" href="/assets/compare.css?v=pcb-1"><link rel="stylesheet" href="/assets/pcb.css?v=1"><script type="module" src="/assets/pcb.js?v=1"></script></head><body>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>PCB Viewer</title><link rel="stylesheet" href="/fonts.css"><link rel="stylesheet" href="/assets/viewer.css"><link rel="stylesheet" href="/assets/compare.css?v=pcb-1"><link rel="stylesheet" href="/assets/pcb.css?v=1"><script type="module" src="/assets/pcb.js?v=expiry-share-1"></script><link rel="stylesheet" href="/assets/share.css?v=1"></head><body>
<button id="menu-toggle" type="button" aria-label="Show menu" aria-expanded="false" aria-controls="pcb-toolbar pcb-menu"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg></button>
<header id="pcb-toolbar" data-pcb-menu hidden><strong>PCB</strong><div id="summary"></div><div class="controls"><label id="sync-control"><input id="sync" type="checkbox" checked> Sync views</label><label id="highlight-control"><input id="highlights" type="checkbox" checked> Highlights</label><button id="fit" type="button">Fit</button></div></header>
<header id="pcb-toolbar" data-pcb-menu hidden><strong>PCB</strong><div id="summary"></div><div class="controls"><label id="sync-control"><input id="sync" type="checkbox" checked> Sync views</label><label id="highlight-control"><input id="highlights" type="checkbox" checked> Highlights</label><div id="pcb-share"></div><button id="fit" type="button">Fit</button></div></header>
<main id="pcb-workspace"><section class="pane"><div id="old-surface" class="surface" tabindex="0" aria-label="PCB"></div></section><section id="new-pane" class="pane" hidden><div id="new-surface" class="surface" tabindex="0" aria-label="Modified PCB"></div></section><aside id="pcb-menu" data-pcb-menu hidden><section><h2>Layers</h2><div id="layer-presets"><button data-preset="top" type="button">Top</button><button data-preset="bottom" type="button">Bottom</button><button data-preset="all" type="button">All</button></div><div id="layers"></div></section><section id="diff-controls" hidden><div class="change-controls"><select id="filter" aria-label="Change filter"><option value="all">All changes</option><option value="added">Added</option><option value="removed">Removed</option><option value="modified">Modified</option><option value="moved">Moved</option></select><button id="previous" aria-label="Previous change">↑</button><button id="next" aria-label="Next change">↓</button></div><label><input id="only-changes" type="checkbox"> Changes only</label><nav id="changes" aria-label="PCB changes"></nav><pre id="details"></pre></section><p id="compatibility"></p></aside></main><output id="state" class="loading" role="status">Loading PCB…</output></body></html>
+3 -1
مشاهده پرونده
@@ -1,3 +1,4 @@
import { mountShare, viewerURL } from './share.js?v=1';
import { bindCamera } from './interactions.js';
import { layerVisible, presetLayers } from './pcb-layers.js';
const comparison=location.pathname.startsWith('/compare/pcb/');
@@ -80,6 +81,7 @@ async function start(){
for(const svg of svgs)svg.addEventListener('click',e=>{const c=byId.get(e.target.closest('[data-change-id]')?.dataset.changeId);if(c)focus(c);});
apply();
}
function expire(){const remaining=result.expires_at*1000-Date.now();if(remaining<=0){for(const view of views)view.destroy();document.querySelector('#pcb-workspace').replaceChildren();state.textContent='This PCB link has expired';}else setTimeout(expire,Math.min(2147483647,remaining));}expire();
mountShare(document.querySelector('#pcb-share'),viewerURL(location.pathname,token));
function expire(){if(result.expires_at===-1)return;const remaining=result.expires_at*1000-Date.now();if(remaining<=0){for(const view of views)view.destroy();document.querySelector('#pcb-workspace').replaceChildren();state.textContent='This PCB link has expired';}else setTimeout(expire,Math.min(2147483647,remaining));}expire();
}
start().catch(e=>{state.classList.remove('loading');state.textContent=e.name==='AbortError'?'':e.message;});
+13
مشاهده پرونده
@@ -0,0 +1,13 @@
.share-widget { position: relative; display: inline-block; color: #233431; font: 13px system-ui,sans-serif; text-align: left; }
.share-widget button { display: inline-block; margin: 0; padding: 7px 12px; border: 1px solid #b6c8bf; border-radius: 4px; background: #fff; color: #244d49; font: inherit; cursor: pointer; }
.share-widget button:focus-visible, .share-widget textarea:focus-visible { outline: 2px solid #2676c5; outline-offset: 2px; }
.share-panel { position: absolute; top: calc(100% + 8px); left: 0; width: min(430px,calc(100vw - 40px)); max-height: 75vh; overflow: auto; z-index: 30; padding: 14px; border: 1px solid #cbd8d1; border-radius: 5px; background: #fff; box-shadow: 0 6px 24px #0002; }
.share-panel[hidden] { display: none !important; }
.share-panel p { margin: 0 0 10px; font: inherit; line-height: 1.5; color: inherit; }
.share-panel textarea { display: block; width: 100%; resize: vertical; padding: 10px; border: 1px solid #cbd8d1; border-radius: 3px; background: #f6f8f6; color: #233431; font: 12px/1.5 monospace; direction: ltr; box-sizing: border-box; }
.share-panel .share-note { margin-top: 10px; font-size: 11px; color: #60716b; }
.share-panel [role="status"] { display: block; margin-top: 8px; font-size: 11px; }
#viewer-share { position: fixed; top: 12px; right: 12px; z-index: 20; }
#viewer-share .share-panel { left: auto; right: 0; }
#comparison-share .share-panel, #pcb-share .share-panel { left: auto; right: 0; }
#upload-share { margin-top: 12px; }
+31
مشاهده پرونده
@@ -0,0 +1,31 @@
/** Construct inert embed markup, retaining the read capability only in the URL fragment. */
export function viewerURL(value, token, base = location.origin) {
const url = new URL(value, base);
if (!['https:', 'http:'].includes(url.protocol)) throw new Error('Invalid viewer URL');
if (url.username || url.password) throw new Error('Invalid viewer URL');
if (token) url.hash = new URLSearchParams({ token }).toString();
return url.href;
}
export function iframeCode(value, base = location.origin) {
const url = viewerURL(value, null, base);
const escaped = url.replace(/[&"<>']/g, c => ({'&':'&amp;','"':'&quot;','<':'&lt;','>':'&gt;',"'":'&#39;'}[c]));
return `<iframe\n src="${escaped}"\n width="100%"\n height="800"\n title="Altium design viewer"\n loading="lazy"\n referrerpolicy="no-referrer"\n allowfullscreen\n></iframe>`;
}
export function mountShare(host, value) {
const code = iframeCode(value), widget = document.createElement('div');widget.className = 'share-widget';
const toggle = document.createElement('button');toggle.type = 'button';toggle.className = 'share-toggle';toggle.textContent = 'Share';toggle.setAttribute('aria-expanded','false');
const panel = document.createElement('div');panel.className = 'share-panel';panel.hidden = true;
const caption = document.createElement('p');caption.textContent = 'Embed this viewer in Gitea or this service:';
const text = document.createElement('textarea');text.readOnly = true;text.value = code;text.rows = 9;text.setAttribute('aria-label','Viewer iframe code');
const note = document.createElement('p');note.className = 'share-note';
note.textContent = new URL(value,location.origin).hash.includes('token=') ? 'This code includes a read token. Anyone with the code can view this design.' : 'Embedding is allowed only on this service and https://git.sadentel.net.';
const copy = document.createElement('button');copy.type = 'button';copy.textContent = 'Copy iframe';
const status = document.createElement('span');status.setAttribute('role','status');
copy.addEventListener('click',async()=>{
try { await navigator.clipboard.writeText(code);status.textContent = 'Copied'; }
catch { text.focus();text.select();status.textContent = 'Select and copy the code manually.'; }
});
toggle.addEventListener('click',()=>{panel.hidden=!panel.hidden;toggle.setAttribute('aria-expanded',String(!panel.hidden));});
widget.addEventListener('keydown',event=>{if(event.key==='Escape'){panel.hidden=true;toggle.setAttribute('aria-expanded','false');toggle.focus();}});
panel.append(caption,text,note,copy,status);widget.append(toggle,panel);host.append(widget);
}
+46
مشاهده پرونده
@@ -0,0 +1,46 @@
import assert from 'node:assert/strict';
import {test} from 'node:test';
import {iframeCode,viewerURL,mountShare} from './share.js';
const base='https://render.sadentel.net';
for(const path of ['/view/','/compare/','/view/pcb/','/compare/pcb/']){
test(`share ${path} preserves the private capability`,()=>{
const url=viewerURL(path+'a'.repeat(48),'read-token',base);
const code=iframeCode(url,base);
assert.ok(code.includes(`src="${base}${path}${'a'.repeat(48)}#token=read-token"`));
assert.ok(code.includes('width="100%"')&&code.includes('height="800"'));
assert.equal(new URL(url).search,'');
assert.ok(!code.includes('X-API-Key'));
});
}
test('unlisted iframe does not gain a token',()=>{
assert.ok(!iframeCode('/view/'+'a'.repeat(48),base).includes('#token='));
});
test('iframe escapes attributes and rejects executable URL schemes',()=>{
const code=iframeCode(base+'/view/test?x=1&y=2#token=" onload="bad',base);
assert.ok(!code.includes(' onload="bad'));
assert.ok(code.includes('%22') && code.includes('&amp;'));
for(const url of ['javascript:alert(1)','data:text/html,hello','https://user:secret@example.test/view/id'])assert.throws(()=>iframeCode(url,base));
});
test('Share reveals the iframe and copies its read token without sending a request',async t=>{
class Element {
constructor(){this.children=[];this.events={};this.attributes={};}
append(...items){this.children.push(...items);}
setAttribute(key,value){this.attributes[key]=value;}
addEventListener(name,callback){this.events[name]=callback;}
focus(){} select(){}
}
const saved=new Map(['document','location','navigator'].map(key=>[key,Object.getOwnPropertyDescriptor(globalThis,key)]));
let copied='';
Object.defineProperty(globalThis,'document',{configurable:true,value:{createElement:()=>new Element()}});
Object.defineProperty(globalThis,'location',{configurable:true,value:{origin:base}});
Object.defineProperty(globalThis,'navigator',{configurable:true,value:{clipboard:{writeText:async code=>{copied=code;}}}});
t.after(()=>{for(const [key,descriptor]of saved)if(descriptor)Object.defineProperty(globalThis,key,descriptor);else delete globalThis[key];});
const host=new Element();mountShare(host,base+'/view/'+'a'.repeat(48)+'#token=private-read');
const [toggle,panel]=host.children[0].children;
assert.equal(panel.hidden,true);toggle.events.click();assert.equal(panel.hidden,false);
const textarea=panel.children[1],copy=panel.children[3];
assert.ok(textarea.value.includes('#token=private-read'));assert.equal(textarea.readOnly,true);
await copy.events.click();assert.equal(copied,textarea.value);
toggle.events.click();assert.equal(panel.hidden,true);
});
+4 -3
مشاهده پرونده
@@ -1,3 +1,4 @@
import { mountShare } from './share.js?v=1';
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 },
@@ -10,7 +11,7 @@ const fields=document.querySelector('#upload-fields'), viewerLink=document.query
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='';
polling?.abort();polling=null;result.textContent='';viewerLink.replaceChildren();details.hidden=true;json.textContent='';document.querySelector('#upload-share').replaceChildren();
}
function configure(){
if(uploading)return;
@@ -73,14 +74,14 @@ form.addEventListener('submit',async event=>{
}
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);}
if(expiry){if(expiry!=='-1'&&(!/^\d+$/.test(expiry)||Number(expiry)<1)){result.textContent='Expiry must be -1 (unlimited) or 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);
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);mountShare(document.querySelector('#upload-share'),url.href);
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.`;});
+1 -1
مشاهده پرونده
@@ -1,2 +1,2 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1"><title>Schematic</title><link rel="stylesheet" href="/fonts.css"><link rel="stylesheet" href="/assets/viewer.css"><script type="module" src="/assets/viewer.js"></script></head><body><main id="viewport" aria-label="Schematic viewer" tabindex="0"></main><output id="state" class="loading" role="status">Loading schematic…</output></body></html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1"><title>Schematic</title><link rel="stylesheet" href="/fonts.css"><link rel="stylesheet" href="/assets/viewer.css"><script type="module" src="/assets/viewer.js?v=expiry-share-1"></script><link rel="stylesheet" href="/assets/share.css?v=1"></head><body><main id="viewport" aria-label="Schematic viewer" tabindex="0"></main><div id="viewer-share"></div><output id="state" class="loading" role="status">Loading schematic…</output></body></html>
+3
مشاهده پرونده
@@ -1,3 +1,4 @@
import { mountShare, viewerURL } from './share.js?v=1';
import { Camera } from './camera.js';
const viewport = document.querySelector('#viewport');
const state = document.querySelector('#state');
@@ -87,7 +88,9 @@ async function start() {
camera.x = camera.width/2-center.x*camera.scale; camera.y = camera.height/2-center.y*camera.scale; paint();
}).observe(viewport);
// Enforce configured expiry even when a viewer remains open.
mountShare(document.querySelector('#viewer-share'),viewerURL(location.pathname,token));
function expire() {
if (result.expires_at === -1) return;
const remaining = result.expires_at*1000-Date.now();
if (remaining <= 0) { viewport.replaceChildren(); state.textContent = 'This schematic link has expired'; }
else setTimeout(expire,Math.min(2147483647,remaining));