-
-
Notifications
You must be signed in to change notification settings - Fork 81
Description
ArchiveWeb.page Version
v0.15.7
What did you expect to happen? What happened instead?
When starting an archiving session via the "go" button (on index.html) or starting an archive on about:blank, to add entry to archive and not break ordered by date function.
However, sometimes about:blank (or other chrome pages) gets included in the archive with a timestamp (ts) of 0. While usually about:blank is not part of the archive and the first URL is, this occasional inclusion:
- breaks date ordering and
- causes new URLs added afterward to seemingly not appear (only in UI).
How can I tell it’s happened?
Querying the collection data via:
chrome-extension://fpeoodllldobpkbkabpblcfaogecpndd/w/api/c/${collectionId}?all=1
reveals an entry like:
{
"url": "about:blank",
"ts": 0,
...
}Additionally, the console throws this error:
Uncaught (in promise) RangeError: Invalid time value
at Date.toISOString ()
at mt (ui.js:136:2457)
...
Pointing to the following method:
function mt(e) {
try {
t = new Date(e.ts || [e.date]);
} catch (e) {}
const i = t && t instanceof Date ? bt(t.toISOString()) : "";
return {
date: t,
timestamp: i
}
}Step-by-step reproduction.
This issue appears rarely and intermittently and under different conditions some include:
- starting archiving from about:blank rather than a direct URL (ie. via go button).
- Or clicking a link on currently archiving page with 'target="_blank"'.
Why start from about:blank?
- Beginning on about:blank lets you capture intermediate and redirected URLs cleanly are included in the archive. (e.g 301 http response codes).
- Also the "go" button (on index.html) seems to utilize this.
Additional details
I "fixed" my archive by going to extension page clicking into archive then clicking "inspect" then console. Pasted the following code and then deleting any pages (e.g about:blank) with ts=0 and deleting it manually. (Note: Although I have used this many times so far and checked the database use at own risk.)
(async () => {
try {
const extensionId = window.location.host;
// Extract collection ID from URL params
const urlObj = new URL(window.location.href);
const sourceParam = urlObj.searchParams.get('source');
const collectionId = sourceParam ? sourceParam.replace('local://', '') : null;
if (!collectionId) throw new Error('Collection ID not found in URL');
// Container setup
const containerId = 'page-delete-ui';
let container = document.getElementById(containerId);
if (!container) {
container = document.createElement('div');
container.id = containerId;
container.style.position = 'fixed';
container.style.bottom = '10px';
container.style.right = '10px';
container.style.width = '440px';
container.style.maxHeight = '580px';
container.style.overflow = 'hidden';
container.style.backgroundColor = 'rgba(255,255,255,0.96)';
container.style.border = '1px solid #ccc';
container.style.padding = '10px';
container.style.fontFamily = 'Arial, sans-serif';
container.style.fontSize = '13px';
container.style.zIndex = '1000000';
container.style.borderRadius = '6px';
container.style.boxShadow = '0 2px 12px rgba(0,0,0,0.2)';
document.body.appendChild(container);
}
// Tabs container
container.innerHTML = '';
const tabsBar = document.createElement('div');
tabsBar.style.display = 'flex';
tabsBar.style.marginBottom = '8px';
tabsBar.style.gap = '10px';
const tabs = {
pages: { label: 'Pages in Collection' },
files: { label: 'Individual File Search' }
};
const tabButtons = {};
for (const key in tabs) {
const btn = document.createElement('button');
btn.textContent = tabs[key].label;
btn.style.flex = '1';
btn.style.cursor = 'pointer';
btn.style.padding = '6px';
btn.style.borderRadius = '4px';
btn.style.border = '1px solid #888';
btn.style.backgroundColor = key === 'pages' ? '#4CAF50' : '#eee';
btn.style.color = key === 'pages' ? 'white' : 'black';
tabButtons[key] = btn;
tabsBar.appendChild(btn);
}
container.appendChild(tabsBar);
const contentDivs = {};
for (const key in tabs) {
const div = document.createElement('div');
div.style.height = '480px';
div.style.overflowY = 'auto';
div.style.display = key === 'pages' ? 'block' : 'none';
container.appendChild(div);
contentDivs[key] = div;
}
// === TAB 1: PAGES IN COLLECTION WITH DETAILED MULTI-MATCH SEARCH ===
let pagesData = null;
let pagesSortOldestFirst = true;
let pagesFilterTsZero = false;
let pagesSearchQuery = '';
async function fetchPages() {
const fetchUrl = `chrome-extension://${extensionId}/w/api/c/${collectionId}?all=1`;
const response = await fetch(fetchUrl);
if (!response.ok) throw new Error(`Failed fetching pages. Status: ${response.status}`);
const data = await response.json();
if (!data.pages || !Array.isArray(data.pages)) throw new Error('Pages array missing');
pagesData = data.pages;
}
function formatTs(ts) {
try { return ts === 0 ? '0' : new Date(ts).toLocaleString(); }
catch { return 'Invalid Date'; }
}
// Highlight and snippet generator: returns up to maxMatches snippets per text
function findHighlightedSnippets(text, query, maxMatches = 5) {
if (!text) return [];
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(escapedQuery, 'ig');
let matches = [];
let match;
while ((match = regex.exec(text)) !== null && matches.length < maxMatches) {
const matchIndex = match.index;
const matchLength = match[0].length;
if (text.includes('\n')) {
const lines = text.split('\n');
let charCount = 0;
for (const line of lines) {
if (matchIndex >= charCount && matchIndex < charCount + line.length) {
const hlLine = line.replace(new RegExp(escapedQuery, 'ig'), m => `<mark style="background-color: yellow; padding:0;">${m}</mark>`);
matches.push(hlLine);
break;
}
charCount += line.length + 1;
}
} else {
const startSnippet = Math.max(0, matchIndex - 15);
const endSnippet = Math.min(text.length, matchIndex + matchLength + 15);
let snippet = text.substring(startSnippet, endSnippet);
if (startSnippet > 0) snippet = '...' + snippet;
if (endSnippet < text.length) snippet = snippet + '...';
const relativeMatchIndex = snippet.toLowerCase().indexOf(match[0].toLowerCase());
if (relativeMatchIndex !== -1) {
snippet = snippet.substring(0, relativeMatchIndex)
+ `<mark style="background-color: yellow; padding:0;">`
+ snippet.substring(relativeMatchIndex, relativeMatchIndex + matchLength)
+ `</mark>`
+ snippet.substring(relativeMatchIndex + matchLength);
}
matches.push(snippet);
}
}
return matches;
}
const defaultVisibleAttrs = ['title', 'url', 'ts'];
function renderPagesToolbar(parent) {
parent.innerHTML = '';
const header = document.createElement('h3');
header.style.marginTop = '0';
header.textContent = `Pages in Collection: ${collectionId}`;
parent.appendChild(header);
const toolbar = document.createElement('div');
toolbar.style.marginBottom = '12px';
toolbar.style.display = 'flex';
toolbar.style.gap = '8px';
toolbar.style.alignItems = 'center';
const sortBtn = document.createElement('button');
function updateSortBtnLabel() {
sortBtn.textContent = pagesSortOldestFirst ? 'Sort: Oldest → Newest' : 'Sort: Newest → Oldest';
}
updateSortBtnLabel();
sortBtn.style.flex = '1';
sortBtn.style.cursor = 'pointer';
sortBtn.style.padding = '6px';
sortBtn.style.borderRadius = '4px';
sortBtn.style.border = '1px solid #888';
sortBtn.style.backgroundColor = '#f0f0f0';
sortBtn.onclick = () => {
pagesSortOldestFirst = !pagesSortOldestFirst;
updateSortBtnLabel();
renderPagesContent(parent);
};
const filterBtn = document.createElement('button');
function updateFilterBtnLabel() {
filterBtn.textContent = pagesFilterTsZero ? 'Showing ts=0 Only' : 'Show entries with ts=0';
filterBtn.style.backgroundColor = pagesFilterTsZero ? '#d0f0d0' : '#f0f0f0';
}
updateFilterBtnLabel();
filterBtn.style.flex = '1.5';
filterBtn.style.cursor = 'pointer';
filterBtn.style.padding = '6px';
filterBtn.style.borderRadius = '4px';
filterBtn.style.border = '1px solid #888';
filterBtn.onclick = () => {
pagesFilterTsZero = !pagesFilterTsZero;
updateFilterBtnLabel();
renderPagesContent(parent);
};
const searchInput = document.createElement('input');
pagesSearchQuery = '';
searchInput.type = 'search';
searchInput.placeholder = 'Search...';
searchInput.value = pagesSearchQuery;
searchInput.style.flex = '2';
searchInput.style.padding = '6px 8px';
searchInput.style.borderRadius = '4px';
searchInput.style.border = '1px solid #888';
searchInput.autocomplete = 'off';
searchInput.addEventListener('input', e => {
pagesSearchQuery = e.target.value.trim();
renderPagesContent(parent);
});
toolbar.appendChild(sortBtn);
toolbar.appendChild(filterBtn);
toolbar.appendChild(searchInput);
parent.appendChild(toolbar);
}
function renderPagesContent(parent) {
let pagesListDiv = parent.querySelector('.pages-list-container');
if (pagesListDiv) pagesListDiv.remove();
pagesListDiv = document.createElement('div');
pagesListDiv.className = 'pages-list-container';
pagesListDiv.style.maxHeight = '400px';
pagesListDiv.style.overflowY = 'auto';
parent.appendChild(pagesListDiv);
if (!pagesData) {
pagesListDiv.textContent = 'Loading...';
return;
}
let filtered = pagesData;
if (pagesFilterTsZero) filtered = filtered.filter(p => p.ts === 0);
if (pagesSearchQuery) {
const q = pagesSearchQuery.toLowerCase();
// Filter for pages that match any attribute containing query
filtered = filtered.filter(page =>
Object.values(page).some(v => v && String(v).toLowerCase().includes(q))
);
}
filtered.sort((a, b) => pagesSortOldestFirst ? a.ts - b.ts : b.ts - a.ts);
if (filtered.length === 0) {
const noPages = document.createElement('div');
noPages.style.fontStyle = 'italic';
noPages.textContent = 'No pages to display.';
pagesListDiv.appendChild(noPages);
return;
}
filtered.forEach(page => {
const pageDiv = document.createElement('div');
pageDiv.style.marginBottom = '10px';
pageDiv.style.paddingBottom = '6px';
pageDiv.style.borderBottom = '1px solid #ddd';
// Capitalize attribute names for labels
const capitalize = s => s.charAt(0).toUpperCase() + s.slice(1);
// Render attribute with up to 5 highlighted snippets or normal values
function renderAttribute(label, key, val, searchActive, query) {
if (val == null) return '';
if (key === 'title' && !val) val = '(No title)';
if (key === 'ts') val = formatTs(val);
if (searchActive && val && typeof val === 'string' && val.toLowerCase().includes(query.toLowerCase())) {
let snippets = findHighlightedSnippets(val, query, 5);
if (snippets.length === 0) snippets = [val]; // fallback whole text
// Compose HTML block with label and all snippets each in separate div
return `<div><strong>${label}:</strong>${snippets.map(snip => `<div style="margin-left:10px;">${snip}</div>`).join('')}</div>`;
} else if (!searchActive) {
if (key === 'url') {
// URL with open upload button in non-search mode or unmatched
return `<div style="display:flex; align-items:center; gap:6px; flex-wrap: wrap;">
<strong>${label}:</strong>
<a href="${val}" target="_blank" rel="noopener noreferrer" style="flex:1; min-width:0;">${val}</a>
<button title="Open Upload" style="
padding: 2px 6px;
font-size: 12px;
cursor: pointer;
border-radius: 3px;
border: 1px solid #777;
background-color: #4CAF50;
color: white;
flex: none;
">↗</button></div>`;
}
return `<div><strong>${label}:</strong> ${val}</div>`;
} else {
// For non-string or unmatched, just text
if (key === 'url') {
return `<div style="display:flex; align-items:center; gap:6px; flex-wrap: wrap;">
<strong>${label}:</strong>
<a href="${val}" target="_blank" rel="noopener noreferrer" style="flex:1; min-width:0;">${val}</a>
<button title="Open Upload" style="
padding: 2px 6px;
font-size: 12px;
cursor: pointer;
border-radius: 3px;
border: 1px solid #777;
background-color: #4CAF50;
color: white;
flex: none;
">↗</button></div>`;
}
return `<div><strong>${label}:</strong> ${val}</div>`;
}
}
// Compose the HTML for this page, showing default visible attrs always with highlighted matches if searching
let innerHTML = '';
const searchActive = pagesSearchQuery.length > 0;
const q = pagesSearchQuery;
// Show Title, URL, Timestamp always
innerHTML += renderAttribute('Title', 'title', page.title, searchActive, q);
innerHTML += renderAttribute('URL', 'url', page.url, searchActive, q);
innerHTML += renderAttribute('Timestamp', 'ts', page.ts, searchActive, q);
// If searching, show other matched attributes with up to 5 snippets each
if (searchActive) {
const matchedAttrs = [];
for (const [key, val] of Object.entries(page)) {
if (!defaultVisibleAttrs.includes(key) && val != null) {
if (typeof val === 'string' && val.toLowerCase().includes(q.toLowerCase())) {
matchedAttrs.push(key);
}
}
}
matchedAttrs.forEach(attr => {
innerHTML += renderAttribute(capitalize(attr), attr, page[attr], true, q);
});
}
pageDiv.innerHTML = innerHTML;
// Attach open upload button's click handler for the URL attribute's upload button
const uploadBtn = pageDiv.querySelector('button[title="Open Upload"]');
if (uploadBtn) {
uploadBtn.onclick = () => {
const openUrl = `chrome-extension://${extensionId}/index.html?source=local://${collectionId}#view=pages&url=${encodeURIComponent(page.url)}&ts=${page.ts}`;
window.open(openUrl, '_blank');
};
}
// Delete button
const delBtn = document.createElement('button');
delBtn.textContent = 'Delete';
delBtn.style.marginTop = '6px';
delBtn.style.padding = '4px 8px';
delBtn.style.backgroundColor = '#e74c3c';
delBtn.style.color = 'white';
delBtn.style.border = 'none';
delBtn.style.borderRadius = '3px';
delBtn.style.cursor = 'pointer';
delBtn.onclick = async () => {
const confirmed = window.confirm(`Confirm deletion of page:\n\n${page.title}\n${page.url}`);
if (!confirmed) return;
try {
delBtn.disabled = true;
delBtn.textContent = 'Deleting...';
const deleteUrl = `chrome-extension://${extensionId}/w/api/c/${collectionId}/page/${page.id}`;
const delResp = await fetch(deleteUrl, { method: 'DELETE' });
if (delResp.ok) {
const idx = pagesData.findIndex(p => p.id === page.id);
if (idx !== -1) pagesData.splice(idx, 1);
renderPagesContent(parent);
alert('Page deleted successfully.');
} else {
alert(`Failed to delete page. Status: ${delResp.status}`);
delBtn.disabled = false;
delBtn.textContent = 'Delete';
}
} catch (err) {
alert('Error deleting page: ' + err.message);
delBtn.disabled = false;
delBtn.textContent = 'Delete';
}
};
pageDiv.appendChild(delBtn);
pagesListDiv.appendChild(pageDiv);
});
}
// === TAB 2: INDIVIDUAL FILE SEARCH ===
let filesData = null;
let filesSortOldestFirst = true;
let filesSearchQuery = '';
let filesMimeFilter = '';
function formatDateISO(dateString) {
try {
const d = new Date(dateString);
return d.toLocaleString();
} catch {
return 'Invalid Date';
}
}
async function fetchFiles() {
const mimeQuery = filesMimeFilter || '';
const url = `chrome-extension://${extensionId}/w/api/c/${collectionId}/urls?mime=${mimeQuery}&url=&prefix=0&count=100`;
const resp = await fetch(url);
if (!resp.ok) throw new Error(`Failed fetching files. Status: ${resp.status}`);
const obj = await resp.json();
if (!obj.urls || !Array.isArray(obj.urls)) throw new Error('urls array missing');
filesData = obj.urls.map(u => ({
...u,
ts: Number(u.ts) || 0,
dateObj: new Date(u.date),
}));
}
function renderFilesToolbar(parent) {
parent.innerHTML = '';
const header = document.createElement('h3');
header.style.marginTop = '0';
header.textContent = `Individual Files in Collection: ${collectionId}`;
parent.appendChild(header);
const toolbar = document.createElement('div');
toolbar.style.marginBottom = '12px';
toolbar.style.display = 'flex';
toolbar.style.gap = '8px';
toolbar.style.alignItems = 'center';
const sortBtn = document.createElement('button');
function updateSortBtnLabel() {
sortBtn.textContent = filesSortOldestFirst ? 'Sort: Oldest → Newest' : 'Sort: Newest → Oldest';
}
updateSortBtnLabel();
sortBtn.style.flex = '1.2';
sortBtn.style.cursor = 'pointer';
sortBtn.style.padding = '6px';
sortBtn.style.borderRadius = '4px';
sortBtn.style.border = '1px solid #888';
sortBtn.style.backgroundColor = '#f0f0f0';
sortBtn.onclick = () => {
filesSortOldestFirst = !filesSortOldestFirst;
updateSortBtnLabel();
renderFilesContent(parent);
};
const mimeFilter = document.createElement('select');
mimeFilter.style.flex = '1.5';
mimeFilter.style.padding = '6px 8px';
mimeFilter.style.borderRadius = '4px';
mimeFilter.style.border = '1px solid #888';
mimeFilter.title = 'Filter by MIME type';
const options = [
{ label: 'All URLs', value: '' },
{ label: 'Videos & Audio', value: 'audio%2F%2Cvideo%2F' }
];
options.forEach(opt => {
const option = document.createElement('option');
option.value = opt.value;
option.textContent = opt.label;
if (opt.value === filesMimeFilter) option.selected = true;
mimeFilter.appendChild(option);
});
mimeFilter.onchange = () => {
filesMimeFilter = mimeFilter.value;
fetchAndRenderFiles();
};
const searchInput = document.createElement('input');
filesSearchQuery = '';
searchInput.type = 'search';
searchInput.placeholder = 'Search URLs or MIME types...';
searchInput.value = filesSearchQuery;
searchInput.style.flex = '2';
searchInput.style.padding = '6px 8px';
searchInput.style.borderRadius = '4px';
searchInput.style.border = '1px solid #888';
searchInput.autocomplete = 'off';
searchInput.addEventListener('input', e => {
filesSearchQuery = e.target.value.trim();
renderFilesContent(parent);
});
toolbar.appendChild(sortBtn);
toolbar.appendChild(mimeFilter);
toolbar.appendChild(searchInput);
parent.appendChild(toolbar);
}
function renderFilesContent(parent) {
let filesListDiv = parent.querySelector('.files-list-container');
if (filesListDiv) filesListDiv.remove();
filesListDiv = document.createElement('div');
filesListDiv.className = 'files-list-container';
filesListDiv.style.maxHeight = '400px';
filesListDiv.style.overflowY = 'auto';
parent.appendChild(filesListDiv);
if (!filesData) {
filesListDiv.textContent = 'Loading...';
return;
}
let filtered = filesData;
if (filesSearchQuery) {
const q = filesSearchQuery.toLowerCase();
filtered = filtered.filter(f =>
(f.url && f.url.toLowerCase().includes(q)) ||
(f.mime && f.mime.toLowerCase().includes(q)) ||
(String(f.ts).includes(q)) ||
(String(f.status).includes(q))
);
}
filtered.sort((a, b) => filesSortOldestFirst ? a.ts - b.ts : b.ts - a.ts);
if (filtered.length === 0) {
const noFiles = document.createElement('div');
noFiles.style.fontStyle = 'italic';
noFiles.textContent = 'No files to display.';
filesListDiv.appendChild(noFiles);
return;
}
filtered.forEach(file => {
const fileDiv = document.createElement('div');
fileDiv.style.marginBottom = '10px';
fileDiv.style.paddingBottom = '6px';
fileDiv.style.borderBottom = '1px solid #ddd';
const urlContainer = document.createElement('div');
urlContainer.style.display = 'flex';
urlContainer.style.alignItems = 'center';
urlContainer.style.gap = '6px';
const urlLink = document.createElement('a');
urlLink.href = file.url;
urlLink.target = '_blank';
urlLink.rel = 'noopener noreferrer';
urlLink.style.flex = '1';
urlLink.style.minWidth = '0';
urlLink.textContent = file.url;
const uploadBtn = document.createElement('button');
uploadBtn.title = 'Open Upload';
uploadBtn.textContent = '↗';
uploadBtn.style.padding = '2px 6px';
uploadBtn.style.fontSize = '12px';
uploadBtn.style.cursor = 'pointer';
uploadBtn.style.borderRadius = '3px';
uploadBtn.style.border = '1px solid #777';
uploadBtn.style.backgroundColor = '#4CAF50';
uploadBtn.style.color = 'white';
uploadBtn.onclick = () => {
const openUrl = `chrome-extension://${extensionId}/index.html?source=local://${collectionId}#view=pages&url=${encodeURIComponent(file.url)}&ts=${file.ts}`;
window.open(openUrl, '_blank');
};
urlContainer.appendChild(urlLink);
urlContainer.appendChild(uploadBtn);
fileDiv.appendChild(urlContainer);
const mimeDiv = document.createElement('div');
mimeDiv.innerHTML = `<strong>MIME:</strong> ${file.mime || 'N/A'}`;
fileDiv.appendChild(mimeDiv);
const dateDiv = document.createElement('div');
dateDiv.innerHTML = `<strong>Date:</strong> ${formatDateISO(file.date)}`;
fileDiv.appendChild(dateDiv);
const tsDiv = document.createElement('div');
tsDiv.innerHTML = `<strong>TS:</strong> ${file.ts}`;
fileDiv.appendChild(tsDiv);
const statusDiv = document.createElement('div');
statusDiv.innerHTML = `<strong>Status:</strong> ${file.status}`;
fileDiv.appendChild(statusDiv);
filesListDiv.appendChild(fileDiv);
});
}
async function fetchAndRenderFiles() {
try {
filesData = null;
renderFilesToolbar(contentDivs.files);
renderFilesContent(contentDivs.files);
const filesListDiv = contentDivs.files.querySelector('.files-list-container');
if (filesListDiv) filesListDiv.textContent = 'Loading...';
await fetchFiles();
renderFilesContent(contentDivs.files);
} catch (err) {
contentDivs.files.innerHTML = `<div style="color:red;">Error loading files: ${err.message}</div>`;
}
}
async function initialize() {
await fetchPages();
renderPagesToolbar(contentDivs.pages);
renderPagesContent(contentDivs.pages);
renderFilesToolbar(contentDivs.files);
let filesLoaded = false;
for (const key in tabButtons) {
tabButtons[key].onclick = async () => {
for (const t in tabs) {
tabButtons[t].style.backgroundColor = (t === key) ? '#4CAF50' : '#eee';
tabButtons[t].style.color = (t === key) ? 'white' : 'black';
contentDivs[t].style.display = (t === key) ? 'block' : 'none';
}
if (key === 'files' && !filesLoaded) {
await fetchAndRenderFiles();
filesLoaded = true;
}
};
}
}
await initialize();
} catch (error) {
console.error('Error:', error);
alert('Error: ' + error.message);
}
})();Since this bug disrupts/breaks the chronological order in the archive UI and effectively blocks new URLs from showing after about:blank is added due to the invalid timestamp. These may be related issues to: