Add GamepadAPI, WebGL readPixels noise, auto-prune, import/export, badge

New fingerprint vectors:
- Gamepad API: getGamepads() returns empty (prevents controller fingerprinting)
- WebGL readPixels: seeded pixel noise on framebuffer reads

New features:
- Auto-prune: configurable automatic removal of inactive containers
- Import/export: backup and restore all settings from options page
- Toolbar badge: shows active container count
- CHANGELOG.md: version history

Bumped to v0.5.0 with 20 spoofed fingerprint vectors.
This commit is contained in:
sal
2026-03-04 21:08:45 -06:00
parent bbe40f87dc
commit d6dabb2646
9 changed files with 352 additions and 4 deletions

View File

@@ -64,6 +64,13 @@ tr:hover td { background: #2a2a3a; }
.td-actions { display: flex; gap: 4px; justify-content: flex-end; }
/* Auto-prune */
.auto-prune-row { display: flex; align-items: center; gap: 16px; }
.toggle-label { display: flex; align-items: center; gap: 8px; font-size: 12px; cursor: pointer; }
.prune-days { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #aaa; }
.days-input { width: 50px; padding: 4px 6px; background: #2a2a3a; border: 1px solid #444; border-radius: 4px; color: #e0e0e0; font-size: 12px; text-align: center; }
.days-input:focus { border-color: #4a9eff; outline: none; }
/* Bulk actions */
.bulk { display: flex; flex-direction: column; gap: 6px; }
.bulk #regen-all { width: 100%; }

View File

@@ -46,6 +46,32 @@
<div id="no-containers" class="empty" hidden>No containers yet. Browse a website to create one.</div>
</section>
<section>
<h2>Auto-Prune</h2>
<p class="desc">Automatically remove inactive containers with no open tabs after a set number of days.</p>
<div class="auto-prune-row">
<label class="toggle-label">
<input type="checkbox" id="auto-prune-enabled" class="toggle">
<span>Enable auto-prune</span>
</label>
<div class="prune-days">
<span>after</span>
<input type="number" id="auto-prune-days" min="1" max="365" value="30" class="days-input">
<span>days</span>
</div>
</div>
</section>
<section>
<h2>Import / Export</h2>
<p class="desc">Backup or restore all settings, seeds, and whitelist.</p>
<div class="bulk-row">
<button id="export-btn" class="btn">Export Settings</button>
<button id="import-btn" class="btn secondary">Import Settings</button>
<input type="file" id="import-file" accept=".json" hidden>
</div>
</section>
<section>
<h2>Bulk Actions</h2>
<div class="bulk">

View File

@@ -215,12 +215,74 @@ document.getElementById("reset").addEventListener("click", async (e) => {
}, 1200);
});
// --- Auto-Prune ---
async function loadAutoPrune() {
const settings = await browser.runtime.sendMessage({ type: "getAutoPruneSettings" });
document.getElementById("auto-prune-enabled").checked = settings.enabled;
document.getElementById("auto-prune-days").value = settings.days || 30;
}
async function saveAutoPrune() {
const enabled = document.getElementById("auto-prune-enabled").checked;
const days = parseInt(document.getElementById("auto-prune-days").value) || 30;
await browser.runtime.sendMessage({
type: "setAutoPruneSettings",
settings: { enabled, days: Math.max(1, Math.min(365, days)) }
});
}
document.getElementById("auto-prune-enabled").addEventListener("change", saveAutoPrune);
document.getElementById("auto-prune-days").addEventListener("change", saveAutoPrune);
// --- Import / Export ---
document.getElementById("export-btn").addEventListener("click", async (e) => {
e.target.textContent = "Exporting...";
const data = await browser.runtime.sendMessage({ type: "exportSettings" });
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `containsite-backup-${new Date().toISOString().slice(0, 10)}.json`;
a.click();
URL.revokeObjectURL(url);
e.target.textContent = "Exported!";
setTimeout(() => { e.target.textContent = "Export Settings"; }, 1200);
});
document.getElementById("import-btn").addEventListener("click", () => {
document.getElementById("import-file").click();
});
document.getElementById("import-file").addEventListener("change", async (e) => {
const file = e.target.files[0];
if (!file) return;
const btn = document.getElementById("import-btn");
btn.textContent = "Importing...";
try {
const text = await file.text();
const data = JSON.parse(text);
const result = await browser.runtime.sendMessage({ type: "importSettings", data });
if (result.ok) {
btn.textContent = "Imported!";
await Promise.all([loadVectors(), loadWhitelist(), loadContainers(), loadAutoPrune()]);
} else {
btn.textContent = "Error: " + (result.error || "Unknown");
}
} catch(err) {
btn.textContent = "Invalid file";
}
e.target.value = "";
setTimeout(() => { btn.textContent = "Import Settings"; }, 2000);
});
// --- Init ---
async function init() {
const manifest = browser.runtime.getManifest();
document.getElementById("version").textContent = `v${manifest.version}`;
await Promise.all([loadVectors(), loadWhitelist(), loadContainers()]);
await Promise.all([loadVectors(), loadWhitelist(), loadContainers(), loadAutoPrune()]);
}
init();