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.
@@ -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 LIBRARY_FILE = () => path.join(os.homedir(), '.skopix', 'step-library.yaml');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skopix",
3
- "version": "2.0.71",
3
+ "version": "2.0.72",
4
4
  "description": "Browser-based QA tool — record tests by using your app, replay them deterministically, generate Playwright code automatically",
5
5
  "main": "cli/index.js",
6
6
  "bin": {
@@ -1365,7 +1365,10 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
1365
1365
  </aside>
1366
1366
 
1367
1367
  <!-- MAIN CONTENT -->
1368
- <main class="main">
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