skopix 2.0.71 → 2.0.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/commands/dashboard.js +45 -1
- package/package.json +1 -1
- package/web/app/index.html +108 -6
|
@@ -849,6 +849,34 @@ export async function dashboardCommand(options) {
|
|
|
849
849
|
sendJSON(res, 200, await getConfig());
|
|
850
850
|
return;
|
|
851
851
|
}
|
|
852
|
+
if (pathname === '/api/saved-urls' && method === 'GET') {
|
|
853
|
+
sendJSON(res, 200, await getSavedUrls());
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
if (pathname === '/api/saved-urls' && method === 'POST') {
|
|
857
|
+
const { url, label } = JSON.parse(await readBody(req));
|
|
858
|
+
const urls = await getSavedUrls();
|
|
859
|
+
if (!urls.find(u => u.url === url)) {
|
|
860
|
+
urls.push({ url, label: label || url });
|
|
861
|
+
await saveSavedUrls(urls);
|
|
862
|
+
}
|
|
863
|
+
sendJSON(res, 200, urls);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (pathname === '/api/saved-urls' && method === 'PUT') {
|
|
867
|
+
const urls = JSON.parse(await readBody(req));
|
|
868
|
+
await saveSavedUrls(urls);
|
|
869
|
+
sendJSON(res, 200, urls);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
if (pathname.match(/^\/api\/saved-urls\/\d+$/) && method === 'DELETE') {
|
|
873
|
+
const idx = parseInt(pathname.split('/')[3]);
|
|
874
|
+
const urls = await getSavedUrls();
|
|
875
|
+
urls.splice(idx, 1);
|
|
876
|
+
await saveSavedUrls(urls);
|
|
877
|
+
sendJSON(res, 200, urls);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
852
880
|
|
|
853
881
|
// ─── RUN ───────────────────────────────────────────────────────────
|
|
854
882
|
// ─── RECORDER ENDPOINTS ──────────────────────────────────────────────────
|
|
@@ -3758,7 +3786,23 @@ async function syncIssuesStatus() {
|
|
|
3758
3786
|
// STEP LIBRARY — persistent store of reusable UI interactions
|
|
3759
3787
|
// ═══════════════════════════════════════════════════════════════
|
|
3760
3788
|
|
|
3761
|
-
const
|
|
3789
|
+
const SAVED_URLS_FILE = () => path.join(os.homedir(), '.skopix', 'saved-urls.yaml');
|
|
3790
|
+
|
|
3791
|
+
async function getSavedUrls() {
|
|
3792
|
+
const file = SAVED_URLS_FILE();
|
|
3793
|
+
if (!await fs.pathExists(file)) return [];
|
|
3794
|
+
try {
|
|
3795
|
+
const content = await fs.readFile(file, 'utf8');
|
|
3796
|
+
const data = yaml.parse(content);
|
|
3797
|
+
return Array.isArray(data) ? data : [];
|
|
3798
|
+
} catch { return []; }
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3801
|
+
async function saveSavedUrls(urls) {
|
|
3802
|
+
const file = SAVED_URLS_FILE();
|
|
3803
|
+
await fs.ensureDir(path.dirname(file));
|
|
3804
|
+
await fs.writeFile(file, yaml.stringify(urls));
|
|
3805
|
+
}
|
|
3762
3806
|
const LIBRARY_PENDING_FILE = () => path.join(os.homedir(), '.skopix', 'step-library-pending.yaml');
|
|
3763
3807
|
|
|
3764
3808
|
async function listPendingSteps() {
|
package/package.json
CHANGED
package/web/app/index.html
CHANGED
|
@@ -1365,7 +1365,10 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
|
|
|
1365
1365
|
</aside>
|
|
1366
1366
|
|
|
1367
1367
|
<!-- MAIN CONTENT -->
|
|
1368
|
-
|
|
1368
|
+
|
|
1369
|
+
<!-- SAVED URLS DATALIST -->
|
|
1370
|
+
<datalist id="saved-urls-datalist"></datalist>
|
|
1371
|
+
<main class="main">
|
|
1369
1372
|
|
|
1370
1373
|
<!-- DASHBOARD -->
|
|
1371
1374
|
<div class="view active" id="view-dashboard">
|
|
@@ -1715,7 +1718,7 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
|
|
|
1715
1718
|
<div style="font-family:var(--mono);font-size:11px;color:var(--muted);margin-top:2px" id="builder-step-count">0 steps — click steps from the library to add them</div>
|
|
1716
1719
|
</div>
|
|
1717
1720
|
<div style="display:flex;gap:8px">
|
|
1718
|
-
<input class="form-input" id="builder-url" type="text" placeholder="Start URL (optional)" style="width:260px;font-size:12px">
|
|
1721
|
+
<input class="form-input" id="builder-url" type="text" placeholder="Start URL (optional)" list="saved-urls-datalist" style="width:260px;font-size:12px">
|
|
1719
1722
|
<button class="btn btn-ghost" style="font-size:11px;padding:4px 10px" onclick="addBuilderStep()">+ Custom step</button>
|
|
1720
1723
|
</div>
|
|
1721
1724
|
</div>
|
|
@@ -1743,7 +1746,7 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
|
|
|
1743
1746
|
</p>
|
|
1744
1747
|
<div class="form-field">
|
|
1745
1748
|
<label class="form-label">URL to open</label>
|
|
1746
|
-
<input class="form-input" id="harvester-url" type="text" placeholder="http://localhost:8224/pi">
|
|
1749
|
+
<input class="form-input" id="harvester-url" type="text" placeholder="http://localhost:8224/pi" list="saved-urls-datalist">
|
|
1747
1750
|
</div>
|
|
1748
1751
|
<div class="form-field">
|
|
1749
1752
|
<label class="form-label">Step name (optional)</label>
|
|
@@ -1809,7 +1812,7 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
|
|
|
1809
1812
|
</p>
|
|
1810
1813
|
<div class="form-field">
|
|
1811
1814
|
<label class="form-label">URL to open</label>
|
|
1812
|
-
<input class="form-input" id="tester-url" type="text" placeholder="http://localhost:8224/pi">
|
|
1815
|
+
<input class="form-input" id="tester-url" type="text" placeholder="http://localhost:8224/pi" list="saved-urls-datalist">
|
|
1813
1816
|
</div>
|
|
1814
1817
|
<div class="form-field">
|
|
1815
1818
|
<label class="form-label">Selector</label>
|
|
@@ -2068,6 +2071,22 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
|
|
|
2068
2071
|
</div>
|
|
2069
2072
|
</div>
|
|
2070
2073
|
|
|
2074
|
+
<!-- SAVED URLS -->
|
|
2075
|
+
<div class="card" style="margin-bottom:16px">
|
|
2076
|
+
<div class="card-header">
|
|
2077
|
+
<div class="card-title">Saved URLs</div>
|
|
2078
|
+
<div style="font-family:var(--mono);font-size:11px;color:var(--muted)">Available as a dropdown in all URL fields</div>
|
|
2079
|
+
</div>
|
|
2080
|
+
<div class="card-body">
|
|
2081
|
+
<div id="saved-urls-list" style="display:flex;flex-direction:column;gap:8px;margin-bottom:12px"></div>
|
|
2082
|
+
<div style="display:flex;gap:8px">
|
|
2083
|
+
<input class="form-input" id="new-url-input" type="text" placeholder="https://myapp.com/login" style="flex:1" onkeydown="if(event.key==='Enter')addSavedUrl()">
|
|
2084
|
+
<input class="form-input" id="new-url-label" type="text" placeholder="Label (optional)" style="width:180px" onkeydown="if(event.key==='Enter')addSavedUrl()">
|
|
2085
|
+
<button class="btn btn-primary" onclick="addSavedUrl()" style="white-space:nowrap">+ Add URL</button>
|
|
2086
|
+
</div>
|
|
2087
|
+
</div>
|
|
2088
|
+
</div>
|
|
2089
|
+
|
|
2071
2090
|
<div class="card">
|
|
2072
2091
|
<div class="card-header">
|
|
2073
2092
|
<div class="card-title">Stored config values</div>
|
|
@@ -2489,7 +2508,7 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
|
|
|
2489
2508
|
</div>
|
|
2490
2509
|
<div class="form-field" style="margin:0">
|
|
2491
2510
|
<label class="form-label">Start URL</label>
|
|
2492
|
-
<input class="form-input" id="re-url" type="url">
|
|
2511
|
+
<input class="form-input" id="re-url" type="url" list="saved-urls-datalist">
|
|
2493
2512
|
</div>
|
|
2494
2513
|
</div>
|
|
2495
2514
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
|
@@ -2601,7 +2620,7 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
|
|
|
2601
2620
|
</div>
|
|
2602
2621
|
<div class="modal-body">
|
|
2603
2622
|
<p style="font-family:var(--mono);font-size:12px;color:var(--muted);margin:0 0 20px;line-height:1.7">A browser window will open. Use your app normally. Every click, type, and navigation is captured. Click Stop when done.</p>
|
|
2604
|
-
<div class="form-field"><label class="form-label">Start URL</label><input class="form-input" id="recorder-url" type="url" placeholder="https://myapp.com"></div>
|
|
2623
|
+
<div class="form-field"><label class="form-label">Start URL</label><input class="form-input" id="recorder-url" type="url" placeholder="https://myapp.com" list="saved-urls-datalist"></div>
|
|
2605
2624
|
<div class="form-field"><label class="form-label">Test name</label><input class="form-input" id="recorder-name" type="text" placeholder="Login and verify dashboard"></div>
|
|
2606
2625
|
<div class="form-field"><label class="form-label">Save to</label><select class="form-select" id="recorder-scope"><option value="saved">All Tests</option></select></div>
|
|
2607
2626
|
<div style="display:flex;gap:10px;justify-content:flex-end;border-top:1px solid var(--border);padding-top:20px">
|
|
@@ -2824,6 +2843,7 @@ async function refreshAll() {
|
|
|
2824
2843
|
renderSuites();
|
|
2825
2844
|
populateSuiteSelect();
|
|
2826
2845
|
checkPendingCount();
|
|
2846
|
+
fetchSavedUrls();
|
|
2827
2847
|
}
|
|
2828
2848
|
|
|
2829
2849
|
function renderStats() {
|
|
@@ -4859,6 +4879,9 @@ switchView = function(name) {
|
|
|
4859
4879
|
if (name === 'step-library') {
|
|
4860
4880
|
loadLibraryView();
|
|
4861
4881
|
}
|
|
4882
|
+
if (name === 'config') {
|
|
4883
|
+
fetchSavedUrls().then(renderSavedUrlsList);
|
|
4884
|
+
}
|
|
4862
4885
|
if (name === 'users') {
|
|
4863
4886
|
fetchUsersAndInvites();
|
|
4864
4887
|
}
|
|
@@ -5583,6 +5606,85 @@ async function saveOllamaConfig() {
|
|
|
5583
5606
|
}
|
|
5584
5607
|
}
|
|
5585
5608
|
|
|
5609
|
+
// ── SAVED URLS ───────────────────────────────────────────────────────────────
|
|
5610
|
+
let savedUrlsCache = [];
|
|
5611
|
+
|
|
5612
|
+
async function fetchSavedUrls() {
|
|
5613
|
+
try {
|
|
5614
|
+
const res = await fetch(API_BASE + '/api/saved-urls');
|
|
5615
|
+
if (!res.ok) return [];
|
|
5616
|
+
savedUrlsCache = await res.json();
|
|
5617
|
+
updateUrlDatalist();
|
|
5618
|
+
return savedUrlsCache;
|
|
5619
|
+
} catch { return []; }
|
|
5620
|
+
}
|
|
5621
|
+
|
|
5622
|
+
function updateUrlDatalist() {
|
|
5623
|
+
const dl = document.getElementById('saved-urls-datalist');
|
|
5624
|
+
if (!dl) return;
|
|
5625
|
+
dl.innerHTML = savedUrlsCache.map(u =>
|
|
5626
|
+
`<option value="${escapeAttr(u.url)}">${escapeAttr(u.label || u.url)}</option>`
|
|
5627
|
+
).join('');
|
|
5628
|
+
}
|
|
5629
|
+
|
|
5630
|
+
function renderSavedUrlsList() {
|
|
5631
|
+
const container = document.getElementById('saved-urls-list');
|
|
5632
|
+
if (!container) return;
|
|
5633
|
+
if (!savedUrlsCache.length) {
|
|
5634
|
+
container.innerHTML = `<div style="font-family:var(--mono);font-size:12px;color:var(--muted2)">No saved URLs yet — add one below</div>`;
|
|
5635
|
+
return;
|
|
5636
|
+
}
|
|
5637
|
+
container.innerHTML = savedUrlsCache.map((u, i) => `
|
|
5638
|
+
<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--surface2);border-radius:8px;border:1px solid var(--border)">
|
|
5639
|
+
<div style="flex:1;min-width:0">
|
|
5640
|
+
<div style="font-size:13px;color:var(--text)">${escapeHtml(u.label || u.url)}</div>
|
|
5641
|
+
<div style="font-family:var(--mono);font-size:11px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escapeHtml(u.url)}</div>
|
|
5642
|
+
</div>
|
|
5643
|
+
<button class="btn btn-ghost saved-url-copy" style="padding:4px 10px;font-size:11px;flex-shrink:0" data-url="${escapeAttr(u.url)}">Copy</button>
|
|
5644
|
+
<button class="btn btn-ghost saved-url-delete" style="padding:4px 10px;font-size:11px;color:var(--red);flex-shrink:0" data-idx="${i}">Delete</button>
|
|
5645
|
+
</div>`).join('');
|
|
5646
|
+
}
|
|
5647
|
+
|
|
5648
|
+
async function addSavedUrl() {
|
|
5649
|
+
const url = document.getElementById('new-url-input')?.value?.trim();
|
|
5650
|
+
const label = document.getElementById('new-url-label')?.value?.trim();
|
|
5651
|
+
if (!url) { showToast('Enter a URL first'); return; }
|
|
5652
|
+
try {
|
|
5653
|
+
const res = await fetch(API_BASE + '/api/saved-urls', {
|
|
5654
|
+
method: 'POST',
|
|
5655
|
+
headers: { 'Content-Type': 'application/json' },
|
|
5656
|
+
body: JSON.stringify({ url, label }),
|
|
5657
|
+
});
|
|
5658
|
+
savedUrlsCache = await res.json();
|
|
5659
|
+
document.getElementById('new-url-input').value = '';
|
|
5660
|
+
document.getElementById('new-url-label').value = '';
|
|
5661
|
+
updateUrlDatalist();
|
|
5662
|
+
renderSavedUrlsList();
|
|
5663
|
+
showToast('URL saved');
|
|
5664
|
+
} catch (err) { showToast('Error: ' + err.message); }
|
|
5665
|
+
}
|
|
5666
|
+
|
|
5667
|
+
async function deleteSavedUrl(idx) {
|
|
5668
|
+
try {
|
|
5669
|
+
const res = await fetch(API_BASE + '/api/saved-urls/' + idx, { method: 'DELETE' });
|
|
5670
|
+
savedUrlsCache = await res.json();
|
|
5671
|
+
updateUrlDatalist();
|
|
5672
|
+
renderSavedUrlsList();
|
|
5673
|
+
} catch (err) { showToast('Error: ' + err.message); }
|
|
5674
|
+
}
|
|
5675
|
+
|
|
5676
|
+
function copyToClipboard(text) {
|
|
5677
|
+
navigator.clipboard.writeText(text).then(() => showToast('Copied!')).catch(() => {});
|
|
5678
|
+
}
|
|
5679
|
+
|
|
5680
|
+
// Event delegation for saved URL buttons
|
|
5681
|
+
document.addEventListener('click', (e) => {
|
|
5682
|
+
const copyBtn = e.target.closest('.saved-url-copy');
|
|
5683
|
+
const deleteBtn = e.target.closest('.saved-url-delete');
|
|
5684
|
+
if (copyBtn) copyToClipboard(copyBtn.dataset.url);
|
|
5685
|
+
if (deleteBtn) deleteSavedUrl(parseInt(deleteBtn.dataset.idx));
|
|
5686
|
+
});
|
|
5687
|
+
|
|
5586
5688
|
// ── STEP LIBRARY PENDING REVIEW ──────────────────────────────────────────────
|
|
5587
5689
|
let pendingStepsCache = [];
|
|
5588
5690
|
|