first commit

This commit is contained in:
Chris Punches
2025-12-30 04:34:38 -05:00
commit 637b67d971
11 changed files with 2170 additions and 0 deletions

530
sidebar/sidebar.css Normal file
View File

@@ -0,0 +1,530 @@
/* Reset */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* Base */
html, body {
height: 100%;
background: var(--theme-bg, #1c1b22);
color: var(--theme-text, #fbfbfe);
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
font-size: 14px;
line-height: 1.5;
}
/* Utility */
.hidden {
display: none !important;
}
/* Screens */
.screen {
padding: 16px;
}
/* Header */
.header {
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 12px;
margin-bottom: 16px;
border-bottom: 1px solid var(--theme-border, #5b5b66);
}
.header-title {
font-size: 16px;
font-weight: 600;
text-align: center;
}
.header-logo {
height: 80px;
max-width: 240px;
object-fit: contain;
}
.header-logo.hidden {
display: none;
}
.header-row {
flex-direction: row;
justify-content: space-between;
}
.header-actions {
display: flex;
gap: 8px;
align-self: flex-end;
margin-bottom: 12px;
}
.instance-name {
font-size: 14px;
font-weight: 500;
opacity: 0.8;
margin-top: 8px;
}
/* Tabs */
.tabs {
display: flex;
gap: 4px;
margin-bottom: 20px;
padding: 4px;
background: rgba(128, 128, 128, 0.15);
border-radius: 6px;
}
.tab {
flex: 1;
padding: 8px 6px;
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
color: var(--theme-text, #fbfbfe) !important;
font-family: inherit;
font-size: 12px;
font-weight: 500;
cursor: pointer;
opacity: 0.7;
transition: all 0.15s;
text-align: center;
}
.tab:hover {
opacity: 1;
background: rgba(128, 128, 128, 0.2);
}
.tab.active {
opacity: 1;
background: rgba(10, 132, 255, 0.2);
border-color: #0a84ff;
color: #0a84ff !important;
}
/* Forms */
.form {
display: flex;
flex-direction: column;
gap: 16px;
}
/* Form Groups */
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-group label {
font-size: 12px;
font-weight: 500;
color: var(--theme-text, #fbfbfe);
opacity: 0.85;
}
/* Inputs */
input[type="text"],
input[type="url"],
input[type="password"],
input[type="email"],
textarea,
select {
width: 100%;
padding: 10px 12px;
background: var(--theme-button-bg, #42414d);
color: var(--theme-text, #fbfbfe);
border: 1px solid var(--theme-border, #5b5b66);
border-radius: 4px;
font-family: inherit;
font-size: 13px;
transition: border-color 0.15s, box-shadow 0.15s;
}
input[type="text"]:focus,
input[type="url"]:focus,
input[type="password"]:focus,
input[type="email"]:focus,
textarea:focus,
select:focus {
outline: none;
border-color: var(--theme-highlight, #00ddff);
box-shadow: 0 0 0 1px var(--theme-highlight, #00ddff);
}
input::placeholder,
textarea::placeholder {
color: var(--theme-text, #fbfbfe);
opacity: 0.4;
}
textarea {
min-height: 80px;
resize: vertical;
}
input[type="file"] {
padding: 8px 0;
font-size: 13px;
border: none;
background: none;
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 16px;
background: rgba(128, 128, 128, 0.2);
color: inherit;
border: 1px solid rgba(128, 128, 128, 0.4);
border-radius: 4px;
font-family: inherit;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.15s;
}
.btn:hover {
background: rgba(128, 128, 128, 0.3);
}
.btn:active {
background: rgba(128, 128, 128, 0.4);
}
.btn-primary {
background: #0a84ff;
color: #ffffff;
border-color: #0a84ff;
}
.btn-primary:hover {
background: #0060df;
border-color: #0060df;
}
.btn-primary:active {
background: #003eaa;
border-color: #003eaa;
}
.btn-block {
width: 100%;
}
.btn-sm {
padding: 6px 12px;
font-size: 12px;
}
/* Submit button specific */
.btn-submit {
margin-top: 8px;
padding: 12px 16px;
font-size: 14px;
}
/* Loading */
#loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 200px;
gap: 12px;
opacity: 0.6;
}
/* Setup screen */
#setup {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 48px 24px;
gap: 16px;
}
#setup .header-title {
font-size: 18px;
}
#setup p {
opacity: 0.7;
max-width: 240px;
}
/* Login form */
#login .header {
justify-content: center;
}
/* Error messages */
.error-message {
color: #ff6b6b;
font-size: 13px;
padding: 8px 0;
}
/* Status messages */
.status {
margin-top: 16px;
padding: 12px;
border-radius: 4px;
text-align: center;
font-size: 13px;
font-weight: 500;
}
.status.success {
background: var(--theme-highlight, #00ddff);
color: var(--theme-highlight-text, #1c1b22);
}
.status.error {
background: rgba(255, 107, 107, 0.15);
border: 1px solid rgba(255, 107, 107, 0.3);
color: #ff6b6b;
}
/* Observation forms */
.obs-form {
display: flex;
flex-direction: column;
gap: 16px;
}
/* Form divider */
.form-divider {
border: none;
border-top: 1px solid rgba(128, 128, 128, 0.3);
margin: 4px 0;
}
/* Paste target */
.paste-target {
display: flex;
align-items: center;
justify-content: center;
min-height: 80px;
border: 2px dashed rgba(128, 128, 128, 0.5);
border-radius: 4px;
color: inherit;
opacity: 0.6;
cursor: pointer;
transition: border-color 0.15s, opacity 0.15s;
text-align: center;
font-size: 13px;
}
.paste-target:hover {
border-color: #0a84ff;
opacity: 1;
}
.paste-target:focus {
border-color: #30d158;
border-style: solid;
background: rgba(48, 209, 88, 0.1);
opacity: 1;
outline: none;
}
.paste-target.ready {
border-color: #0a84ff;
border-style: solid;
background: rgba(10, 132, 255, 0.1);
}
/* Drop target */
.drop-target {
display: flex;
align-items: center;
justify-content: center;
min-height: 80px;
border: 2px dashed rgba(128, 128, 128, 0.5);
border-radius: 4px;
color: inherit;
opacity: 0.6;
cursor: pointer;
transition: border-color 0.15s, opacity 0.15s;
text-align: center;
font-size: 13px;
}
.drop-target:hover,
.drop-target:focus,
.drop-target.dragover {
border-color: #0a84ff;
opacity: 1;
outline: none;
}
.drop-target.ready {
border-color: #0a84ff;
border-style: solid;
background: rgba(10, 132, 255, 0.1);
}
/* Image preview */
.image-preview {
display: flex;
justify-content: center;
padding: 8px 0;
}
.image-preview img {
max-width: 100%;
max-height: 150px;
border-radius: 4px;
object-fit: contain;
}
/* Access screen */
.access-hint {
font-size: 13px;
opacity: 0.7;
margin-bottom: 16px;
}
.observations-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.loading-text {
text-align: center;
opacity: 0.6;
padding: 24px 0;
}
.observation-item {
padding: 12px;
background: rgba(128, 128, 128, 0.1);
border: 1px solid rgba(128, 128, 128, 0.2);
border-radius: 6px;
}
.observation-item.disabled {
opacity: 0.5;
}
.observation-header {
display: flex;
gap: 10px;
margin-bottom: 8px;
}
.observation-thumb {
width: 60px;
height: 60px;
border-radius: 4px;
object-fit: cover;
flex-shrink: 0;
background: rgba(128, 128, 128, 0.2);
}
.observation-info {
flex: 1;
min-width: 0;
}
.observation-name {
display: block;
font-size: 13px;
font-weight: 500;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #0a84ff;
text-decoration: none;
}
.observation-name:hover {
text-decoration: underline;
}
.observation-type {
font-size: 11px;
opacity: 0.6;
text-transform: capitalize;
margin-bottom: 4px;
}
.observation-detail {
font-size: 11px;
opacity: 0.7;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 2px;
}
.observation-detail strong {
opacity: 0.9;
}
.access-toggle {
display: flex;
gap: 4px;
}
.access-toggle button {
flex: 1;
padding: 6px 8px;
background: rgba(128, 128, 128, 0.2);
border: 1px solid rgba(128, 128, 128, 0.3);
border-radius: 4px;
color: inherit;
font-family: inherit;
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
}
.access-toggle button:hover:not(:disabled) {
background: rgba(128, 128, 128, 0.3);
}
.access-toggle button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.access-toggle button.active-none {
background: rgba(255, 179, 0, 0.25);
border-color: #ffb300;
color: #ffb300;
}
.access-toggle button.active-editor {
background: rgba(255, 179, 0, 0.25);
border-color: #ffb300;
color: #ffb300;
}
.access-toggle button.active-viewer {
background: rgba(48, 209, 88, 0.2);
border-color: #30d158;
color: #30d158;
}
.no-observations {
text-align: center;
opacity: 0.6;
padding: 24px 0;
font-size: 13px;
}

243
sidebar/sidebar.html Normal file
View File

@@ -0,0 +1,243 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>CrowdProof Observe</title>
<link rel="stylesheet" href="sidebar.css">
</head>
<body>
<!-- Loading -->
<div id="loading" class="screen">
<p>Loading...</p>
</div>
<!-- Setup -->
<div id="setup" class="screen hidden">
<h1 class="header-title">CrowdProof Observe</h1>
<p>Configure the server URL in extension settings to get started.</p>
<button id="openSettings" class="btn btn-primary">Open Settings</button>
</div>
<!-- Settings -->
<div id="settings" class="screen hidden">
<header class="header header-row">
<h1 class="header-title">Settings</h1>
<button id="settingsBackBtn" class="btn btn-sm">Back</button>
</header>
<form id="settingsForm" class="form">
<div class="form-group">
<label for="settings-serverUrl">Server URL</label>
<input type="url" id="settings-serverUrl" name="serverUrl" required placeholder="https://your-crowdproof-server.com">
</div>
<button type="submit" class="btn btn-primary btn-block btn-submit">Save</button>
</form>
<div id="settingsStatus" class="status hidden"></div>
</div>
<!-- Access -->
<div id="access" class="screen hidden">
<header class="header header-row">
<h1 class="header-title">Manage Access</h1>
<button id="accessBackBtn" class="btn btn-sm">Back</button>
</header>
<p class="access-hint">Set public access for observations you own.</p>
<div id="observations-list" class="observations-list">
<p class="loading-text">Loading...</p>
</div>
</div>
<!-- Login -->
<div id="login" class="screen hidden">
<header class="header">
<img id="login-logo" class="header-logo" src="" alt="">
<h1 id="login-title" class="header-title hidden">Login</h1>
<p id="login-instance-name" class="instance-name"></p>
</header>
<form id="loginForm" class="form">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary btn-block btn-submit">Login</button>
<div id="loginError" class="error-message hidden"></div>
<a id="createAccountLink" href="#" class="btn btn-block btn-submit" target="_blank">Create Account</a>
<button type="button" id="loginSettingsBtn" class="btn btn-block btn-submit">Settings</button>
</form>
</div>
<!-- Main -->
<div id="main" class="screen hidden">
<header class="header">
<div class="header-actions">
<button id="accessBtn" class="btn btn-sm">Access</button>
<button id="mainSettingsBtn" class="btn btn-sm">Settings</button>
<button id="logoutBtn" class="btn btn-sm">Logout</button>
</div>
<img id="main-logo" class="header-logo" src="" alt="">
<h1 id="main-title" class="header-title hidden">CrowdProof</h1>
<p id="main-instance-name" class="instance-name"></p>
</header>
<nav class="tabs">
<button type="button" class="tab active" data-type="photo">Photo</button>
<button type="button" class="tab" data-type="url">URL</button>
<button type="button" class="tab" data-type="statement">Statement</button>
<button type="button" class="tab" data-type="log">Log</button>
<button type="button" class="tab" data-type="email">Email</button>
</nav>
<!-- URL Form -->
<form id="form-url" class="obs-form hidden">
<div class="form-group">
<button type="button" id="captureUrl" class="btn btn-block">Capture Current Page</button>
</div>
<hr class="form-divider">
<div class="form-group">
<label for="url-reference_url">URL</label>
<input type="url" id="url-reference_url" name="reference_url" required>
</div>
<hr class="form-divider">
<div class="form-group">
<label for="url-observation_name">Title (optional)</label>
<input type="text" id="url-observation_name" name="observation_name" maxlength="256">
</div>
<div class="form-group">
<label for="url-url_description">Description (optional)</label>
<textarea id="url-url_description" name="url_description" rows="3"></textarea>
</div>
<div class="form-group">
<label for="url-url_date">Date (optional)</label>
<input type="text" id="url-url_date" name="url_date" placeholder="YYYY-MM-DD">
</div>
<hr class="form-divider">
<button type="submit" class="btn btn-primary btn-block btn-submit">Send</button>
</form>
<!-- Statement Form -->
<form id="form-statement" class="obs-form hidden">
<div class="form-group">
<label for="statement-statement_issuer">Issuer</label>
<input type="text" id="statement-statement_issuer" name="statement_issuer" required>
</div>
<div class="form-group">
<label for="statement-statement_recipient">Recipient (optional)</label>
<input type="text" id="statement-statement_recipient" name="statement_recipient">
</div>
<div class="form-group">
<label for="statement-statement_date">Date (optional)</label>
<input type="text" id="statement-statement_date" name="statement_date" placeholder="YYYY-MM-DD">
</div>
<div class="form-group">
<label for="statement-statement_content">Statement</label>
<textarea id="statement-statement_content" name="statement_content" rows="4" required></textarea>
</div>
<hr class="form-divider">
<div class="form-group">
<label for="statement-observation_name">Title (optional)</label>
<input type="text" id="statement-observation_name" name="observation_name" maxlength="256">
</div>
<div class="form-group">
<label for="statement-statement_description">Description (optional)</label>
<textarea id="statement-statement_description" name="statement_description" rows="2"></textarea>
</div>
<button type="submit" class="btn btn-primary btn-block btn-submit">Send</button>
</form>
<!-- Log Form -->
<form id="form-log" class="obs-form hidden">
<div class="form-group">
<label for="log-log_source">Source</label>
<input type="text" id="log-log_source" name="log_source" required placeholder="Email, IRC, System logs, etc.">
</div>
<hr class="form-divider">
<div class="form-group">
<label>Import from File</label>
<input type="file" id="log-file-import" accept=".txt,.log,.csv,.json">
</div>
<div class="form-group">
<label for="log-log_content">Log Content</label>
<textarea id="log-log_content" name="log_content" rows="5" required></textarea>
</div>
<div class="form-group">
<label for="log-log_date">Date (optional)</label>
<input type="text" id="log-log_date" name="log_date" placeholder="YYYY-MM-DD">
</div>
<hr class="form-divider">
<div class="form-group">
<label for="log-observation_name">Title (optional)</label>
<input type="text" id="log-observation_name" name="observation_name" maxlength="256">
</div>
<div class="form-group">
<label for="log-log_description">Description (optional)</label>
<textarea id="log-log_description" name="log_description" rows="2"></textarea>
</div>
<hr class="form-divider">
<button type="submit" class="btn btn-primary btn-block btn-submit">Send</button>
</form>
<!-- Photo Form -->
<form id="form-photo" class="obs-form">
<div class="form-group">
<label for="photo-photo_url">Reference URL (optional)</label>
<input type="url" id="photo-photo_url" name="photo_url">
</div>
<hr class="form-divider">
<div class="form-group">
<label>Select file</label>
<input type="file" id="photo-file" name="file" accept="image/*">
</div>
<div id="paste-target" class="paste-target" tabindex="0">
<span>Click to enable paste</span>
</div>
<div id="image-preview" class="image-preview hidden">
<img id="image-preview-img" src="" alt="Preview">
</div>
<hr class="form-divider">
<div class="form-group">
<label for="photo-observation_name">Title (optional)</label>
<input type="text" id="photo-observation_name" name="observation_name" maxlength="256">
</div>
<div class="form-group">
<label for="photo-photo_description">Description (optional)</label>
<textarea id="photo-photo_description" name="photo_description" rows="3"></textarea>
</div>
<div class="form-group">
<label for="photo-photo_date">Date (optional)</label>
<input type="text" id="photo-photo_date" name="photo_date" placeholder="YYYY-MM-DD">
</div>
<hr class="form-divider">
<button type="submit" class="btn btn-primary btn-block btn-submit">Send</button>
</form>
<!-- Email Form -->
<form id="form-email" class="obs-form hidden">
<div class="form-group">
<label>Email File</label>
<input type="file" id="email-file" name="msg_file" accept=".msg,.eml">
</div>
<div id="email-drop-target" class="drop-target" tabindex="0">
<span>Drop .msg or .eml file to submit</span>
</div>
<hr class="form-divider">
<div class="form-group">
<label for="email-observation_name">Title (optional)</label>
<input type="text" id="email-observation_name" name="observation_name" maxlength="256">
</div>
<div class="form-group">
<label for="email-email_description">Description (optional)</label>
<textarea id="email-email_description" name="email_description" rows="3"></textarea>
</div>
<hr class="form-divider">
<button type="submit" class="btn btn-primary btn-block btn-submit">Send</button>
</form>
<div id="status" class="status hidden"></div>
</div>
<script src="sidebar.js"></script>
</body>
</html>

827
sidebar/sidebar.js Normal file
View File

@@ -0,0 +1,827 @@
// CrowdProof Observe - Sidebar Script
let serverUrl = null;
let pendingImageFile = null; // Stores image file from context menu
// Apply browser theme
async function applyTheme() {
const theme = await browser.theme.getCurrent();
const colors = theme.colors || {};
document.documentElement.style.setProperty('--theme-bg', colors.popup || colors.frame || '');
document.documentElement.style.setProperty('--theme-text', colors.popup_text || colors.tab_text || '');
document.documentElement.style.setProperty('--theme-border', colors.popup_border || colors.toolbar_top_separator || '');
document.documentElement.style.setProperty('--theme-button-bg', colors.button_background_active || colors.toolbar || '');
document.documentElement.style.setProperty('--theme-highlight', colors.tab_line || colors.ntp_text || colors.popup_highlight || '');
document.documentElement.style.setProperty('--theme-highlight-text', colors.popup_highlight_text || colors.toolbar_text || colors.popup_text || '');
}
browser.theme.onUpdated.addListener(applyTheme);
applyTheme();
// Screen management
function showScreen(screenId) {
document.querySelectorAll('.screen').forEach(s => s.classList.add('hidden'));
document.getElementById(screenId).classList.remove('hidden');
}
function showStatus(message, isError = false) {
const status = document.getElementById('status');
status.textContent = message;
status.className = `status ${isError ? 'error' : 'success'}`;
setTimeout(() => {
status.className = 'status hidden';
}, 4000);
}
// API helper
async function apiRequest(endpoint, options = {}) {
if (!serverUrl) {
throw new Error('Server URL not configured');
}
const url = `${serverUrl}${endpoint}`;
const fetchOptions = {
credentials: 'include',
...options
};
return fetch(url, fetchOptions);
}
// Form submission handler
async function handleFormSubmit(e, type) {
e.preventDefault();
console.log('=== FORM SUBMIT START ===');
console.log('Type:', type);
console.log('Server URL:', serverUrl);
const form = e.target;
const formData = new FormData(form);
// For photo submissions, use pendingImageFile if available
if (type === 'photo' && pendingImageFile) {
formData.set('file', pendingImageFile);
pendingImageFile = null;
hideImagePreview();
}
console.log('Form data entries:');
for (let [key, value] of formData.entries()) {
console.log(' ', key, '=', value);
}
const submitBtn = form.querySelector('button[type="submit"]');
const originalText = submitBtn.textContent;
submitBtn.textContent = 'Creating...';
submitBtn.disabled = true;
const url = `${serverUrl}/create_observation/${type}`;
console.log('Fetching URL:', url);
try {
console.log('About to fetch...');
const response = await fetch(url, {
method: 'POST',
body: formData,
credentials: 'include',
headers: {
'Accept': 'application/json'
}
});
console.log('Fetch completed');
console.log('Response status:', response.status);
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
const result = await response.json();
if (result.success) {
showStatus('Observation created successfully!');
form.reset();
if (type === 'photo') hideImagePreview();
} else if (result.error === 'login_required') {
showStatus('Session expired. Please login again.', true);
showScreen('login');
} else {
showStatus(result.error || 'Failed to create observation', true);
}
} else {
// Fallback to old redirect-based detection
if (response.url.includes('/login')) {
showStatus('Session expired. Please login again.', true);
showScreen('login');
} else if (response.url.includes('/observations')) {
showStatus('Observation created successfully!');
form.reset();
if (type === 'photo') hideImagePreview();
} else {
showStatus('Failed to create observation', true);
}
}
} catch (error) {
console.error('Fetch error:', error);
showStatus('Error: ' + error.message, true);
} finally {
submitBtn.textContent = originalText;
submitBtn.disabled = false;
console.log('=== FORM SUBMIT END ===');
}
}
// Check for pending URL from context menu
async function checkPendingUrl() {
const result = await browser.storage.local.get('pendingUrl');
if (result.pendingUrl) {
// Clear it
await browser.storage.local.remove('pendingUrl');
// Switch to URL tab
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelector('.tab[data-type="url"]').classList.add('active');
document.querySelectorAll('.obs-form').forEach(f => f.classList.add('hidden'));
document.getElementById('form-url').classList.remove('hidden');
// Populate URL field
document.getElementById('url-reference_url').value = result.pendingUrl;
}
}
// Check for pending image from context menu
async function checkPendingImage() {
const result = await browser.storage.local.get('pendingImage');
if (result.pendingImage) {
const { dataUrl, sourceUrl } = result.pendingImage;
// Clear it
await browser.storage.local.remove('pendingImage');
// Switch to Photo tab
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelector('.tab[data-type="photo"]').classList.add('active');
document.querySelectorAll('.obs-form').forEach(f => f.classList.add('hidden'));
document.getElementById('form-photo').classList.remove('hidden');
// Populate the reference URL field
if (sourceUrl) {
document.getElementById('photo-photo_url').value = sourceUrl;
}
// Convert dataUrl to File and store it
if (dataUrl) {
const response = await fetch(dataUrl);
const blob = await response.blob();
pendingImageFile = new File([blob], `image-${Date.now()}.png`, { type: 'image/png' });
// Show preview
document.getElementById('image-preview-img').src = dataUrl;
document.getElementById('image-preview').classList.remove('hidden');
showStatus('Image captured and ready to submit!');
} else {
showStatus('Could not capture image data', true);
}
}
}
// Show image preview
function showImagePreview(src) {
document.getElementById('image-preview-img').src = src;
document.getElementById('image-preview').classList.remove('hidden');
}
// Hide image preview
function hideImagePreview() {
document.getElementById('image-preview').classList.add('hidden');
document.getElementById('image-preview-img').src = '';
}
// Update logos and instance name from server
async function updateLogos() {
if (!serverUrl) return;
const loginLogo = document.getElementById('login-logo');
const mainLogo = document.getElementById('main-logo');
const loginTitle = document.getElementById('login-title');
const mainTitle = document.getElementById('main-title');
const loginInstanceName = document.getElementById('login-instance-name');
const mainInstanceName = document.getElementById('main-instance-name');
// Fetch instance name from login page
try {
const pageResponse = await fetch(`${serverUrl}/login`, {
credentials: 'include'
});
if (pageResponse.ok) {
const html = await pageResponse.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const brand = doc.querySelector('.navbar-brand');
if (brand) {
const instanceName = brand.textContent.trim();
loginInstanceName.textContent = instanceName;
mainInstanceName.textContent = instanceName;
} else {
throw new Error('Brand not found');
}
} else {
throw new Error('Page fetch failed');
}
} catch (e) {
loginInstanceName.textContent = 'CrowdProof (Error)';
mainInstanceName.textContent = 'CrowdProof (Error)';
}
// Fetch logo
try {
const response = await fetch(`${serverUrl}/static/logo.svg`, {
credentials: 'include'
});
if (response.ok) {
const svgText = await response.text();
const dataUrl = 'data:image/svg+xml;base64,' + btoa(svgText);
loginLogo.src = dataUrl;
mainLogo.src = dataUrl;
loginLogo.classList.remove('hidden');
mainLogo.classList.remove('hidden');
loginTitle.classList.add('hidden');
mainTitle.classList.add('hidden');
} else {
throw new Error('Logo not found');
}
} catch (e) {
// Fallback to text
loginLogo.classList.add('hidden');
mainLogo.classList.add('hidden');
loginTitle.classList.remove('hidden');
mainTitle.classList.remove('hidden');
}
}
// Load observations list for access screen
async function loadObservations() {
const listEl = document.getElementById('observations-list');
listEl.innerHTML = '<p class="loading-text">Loading...</p>';
try {
const response = await fetch(`${serverUrl}/observations`, {
credentials: 'include',
headers: {
'Accept': 'application/json'
}
});
if (response.url.includes('/login')) {
showScreen('login');
return;
}
const data = await response.json();
if (data.success && data.observations) {
renderObservationsList(data.observations);
} else {
listEl.innerHTML = '<p class="no-observations">Failed to load observations</p>';
}
} catch (error) {
console.error('Load observations error:', error);
listEl.innerHTML = '<p class="no-observations">Error loading observations</p>';
}
}
// Render observations list with access toggles
function renderObservationsList(observations) {
const listEl = document.getElementById('observations-list');
if (!observations || observations.length === 0) {
listEl.innerHTML = '<p class="no-observations">No observations found</p>';
return;
}
listEl.innerHTML = observations.map(obs => {
const isOwner = obs.is_owner;
const disabledClass = isOwner ? '' : 'disabled';
const disabledAttr = isOwner ? '' : 'disabled';
const details = obs.details || {};
// Determine which button is active based on public_access value
const noneActive = obs.public_access === 0 ? 'active-none' : '';
const editorActive = obs.public_access === 1 ? 'active-editor' : '';
const viewerActive = obs.public_access === 2 ? 'active-viewer' : '';
// Build thumbnail for photos
let thumbHtml = '';
if (obs.observation_type === 'photo' && details.image_url) {
thumbHtml = `<img class="observation-thumb" src="${serverUrl}${details.image_url}" alt="">`;
}
// Build type-specific details
let detailsHtml = '';
if (obs.observation_type === 'photo') {
if (details.description) detailsHtml += `<div class="observation-detail">${escapeHtml(details.description)}</div>`;
if (details.date) detailsHtml += `<div class="observation-detail"><strong>Date:</strong> ${escapeHtml(details.date)}</div>`;
} else if (obs.observation_type === 'email') {
if (details.subject) detailsHtml += `<div class="observation-detail"><strong>Subject:</strong> ${escapeHtml(details.subject)}</div>`;
if (details.sender) detailsHtml += `<div class="observation-detail"><strong>From:</strong> ${escapeHtml(details.sender)}</div>`;
if (details.date) detailsHtml += `<div class="observation-detail"><strong>Date:</strong> ${escapeHtml(details.date)}</div>`;
} else if (obs.observation_type === 'url') {
if (details.url) detailsHtml += `<div class="observation-detail">${escapeHtml(details.url)}</div>`;
if (details.date) detailsHtml += `<div class="observation-detail"><strong>Date:</strong> ${escapeHtml(details.date)}</div>`;
} else if (obs.observation_type === 'log') {
if (details.source) detailsHtml += `<div class="observation-detail"><strong>Source:</strong> ${escapeHtml(details.source)}</div>`;
if (details.date) detailsHtml += `<div class="observation-detail"><strong>Date:</strong> ${escapeHtml(details.date)}</div>`;
} else if (obs.observation_type === 'statement') {
if (details.issuer) detailsHtml += `<div class="observation-detail"><strong>Issuer:</strong> ${escapeHtml(details.issuer)}</div>`;
if (details.recipient) detailsHtml += `<div class="observation-detail"><strong>To:</strong> ${escapeHtml(details.recipient)}</div>`;
if (details.date) detailsHtml += `<div class="observation-detail"><strong>Date:</strong> ${escapeHtml(details.date)}</div>`;
}
return `
<div class="observation-item ${disabledClass}" data-uuid="${obs.uuid}">
<div class="observation-header">
${thumbHtml}
<div class="observation-info">
<a href="${serverUrl}/observation/${obs.uuid}" target="_blank" class="observation-name" title="${escapeHtml(obs.display_name)}">${escapeHtml(obs.display_name)}</a>
<div class="observation-type">${obs.observation_type}</div>
${detailsHtml}
</div>
</div>
<div class="access-toggle">
<button type="button" class="${noneActive}" data-level="0" ${disabledAttr}>None</button>
<button type="button" class="${editorActive}" data-level="1" ${disabledAttr}>Editor</button>
<button type="button" class="${viewerActive}" data-level="2" ${disabledAttr}>Viewer</button>
</div>
</div>
`;
}).join('');
// Add click handlers for toggle buttons
listEl.querySelectorAll('.access-toggle button:not([disabled])').forEach(btn => {
btn.addEventListener('click', async (e) => {
const item = e.target.closest('.observation-item');
const uuid = item.dataset.uuid;
const level = parseInt(e.target.dataset.level);
await setPublicAccess(uuid, level, item);
});
});
}
// Escape HTML to prevent XSS
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Set public access for an observation
async function setPublicAccess(uuid, level, itemEl) {
const buttons = itemEl.querySelectorAll('.access-toggle button');
buttons.forEach(btn => btn.disabled = true);
try {
const response = await fetch(`${serverUrl}/set_public_access/observation/${uuid}/${level}`, {
method: 'POST',
credentials: 'include'
});
if (response.ok) {
// Update button states
buttons.forEach(btn => {
btn.classList.remove('active-none', 'active-editor', 'active-viewer');
const btnLevel = parseInt(btn.dataset.level);
if (btnLevel === level) {
if (level === 0) btn.classList.add('active-none');
else if (level === 1) btn.classList.add('active-editor');
else if (level === 2) btn.classList.add('active-viewer');
}
});
} else {
showStatus('Failed to update access', true);
}
} catch (error) {
console.error('Set public access error:', error);
showStatus('Error updating access', true);
} finally {
buttons.forEach(btn => btn.disabled = false);
}
}
// Listen for storage changes to catch pending URL/image even when sidebar is already open
browser.storage.onChanged.addListener((changes, area) => {
if (area === 'local') {
if (changes.pendingUrl && changes.pendingUrl.newValue) {
checkPendingUrl();
}
if (changes.pendingImage && changes.pendingImage.newValue) {
checkPendingImage();
}
}
});
// Initialize
async function init() {
console.log('Init starting...');
const result = await browser.storage.local.get('serverUrl');
serverUrl = result.serverUrl || 'https://crowdproof.silogroup.org';
console.log('Server URL:', serverUrl);
// Set create account link
document.getElementById('createAccountLink').href = `${serverUrl}/register`;
updateLogos();
try {
const response = await apiRequest('/profile');
if (response.url.includes('/login')) {
showScreen('login');
} else {
showScreen('main');
// Check for pending URL/image after showing main screen
await checkPendingUrl();
await checkPendingImage();
}
} catch (error) {
console.error('Auth check failed:', error);
showScreen('login');
}
}
// Setup all event handlers when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
console.log('=== DOM READY ===');
// Open settings
document.getElementById('openSettings').addEventListener('click', () => {
showScreen('settings');
});
// Login form
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
console.log('Login form submitted');
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const errorEl = document.getElementById('loginError');
try {
const formData = new FormData();
formData.append('username', username);
formData.append('password', password);
const response = await apiRequest('/login', {
method: 'POST',
body: formData
});
if (!response.url.includes('/login')) {
showScreen('main');
} else {
errorEl.textContent = 'Invalid username or password';
errorEl.classList.remove('hidden');
}
} catch (error) {
errorEl.textContent = error.message;
errorEl.classList.remove('hidden');
}
});
// Login settings button
document.getElementById('loginSettingsBtn').addEventListener('click', async () => {
const result = await browser.storage.local.get('serverUrl');
document.getElementById('settings-serverUrl').value = result.serverUrl || 'https://crowdproof.silogroup.org';
showScreen('settings');
});
// Logout
document.getElementById('logoutBtn').addEventListener('click', async () => {
try {
await apiRequest('/logout');
showScreen('login');
} catch (error) {
showStatus('Logout failed: ' + error.message, true);
}
});
// Settings button on main screen
document.getElementById('mainSettingsBtn').addEventListener('click', async () => {
const result = await browser.storage.local.get('serverUrl');
document.getElementById('settings-serverUrl').value = result.serverUrl || '';
showScreen('settings');
});
// Settings back button
document.getElementById('settingsBackBtn').addEventListener('click', async () => {
// Go back to appropriate screen
if (serverUrl) {
try {
const response = await apiRequest('/profile');
if (response.url.includes('/login')) {
showScreen('login');
} else {
showScreen('main');
}
} catch {
showScreen('login');
}
} else {
showScreen('setup');
}
});
// Access button
document.getElementById('accessBtn').addEventListener('click', () => {
showScreen('access');
loadObservations();
});
// Access back button
document.getElementById('accessBackBtn').addEventListener('click', () => {
showScreen('main');
});
// Settings form
document.getElementById('settingsForm').addEventListener('submit', async (e) => {
e.preventDefault();
const newUrl = document.getElementById('settings-serverUrl').value.replace(/\/+$/, '');
await browser.storage.local.set({ serverUrl: newUrl });
serverUrl = newUrl;
document.getElementById('createAccountLink').href = `${serverUrl}/register`;
updateLogos();
const statusEl = document.getElementById('settingsStatus');
statusEl.textContent = 'Settings saved';
statusEl.className = 'status success';
statusEl.classList.remove('hidden');
setTimeout(() => {
statusEl.classList.add('hidden');
}, 2000);
});
// Tab switching
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
const type = tab.dataset.type;
console.log('Tab clicked:', type);
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
document.querySelectorAll('.obs-form').forEach(f => f.classList.add('hidden'));
document.getElementById(`form-${type}`).classList.remove('hidden');
});
});
// Capture URL button
document.getElementById('captureUrl').addEventListener('click', async () => {
console.log('Capture URL clicked');
try {
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
if (tabs[0]) {
document.getElementById('url-reference_url').value = tabs[0].url;
if (!document.getElementById('url-observation_name').value) {
document.getElementById('url-observation_name').value = tabs[0].title;
}
showStatus('URL captured!');
}
} catch (error) {
console.error('Capture URL error:', error);
showStatus('Could not capture URL', true);
}
});
// Log file import
document.getElementById('log-file-import').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const text = await file.text();
document.getElementById('log-log_content').value = text;
showStatus('File imported');
} catch (error) {
showStatus('Could not read file', true);
}
});
// Photo file input - show preview
document.getElementById('photo-file').addEventListener('change', (e) => {
const file = e.target.files[0];
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (event) => {
showImagePreview(event.target.result);
};
reader.readAsDataURL(file);
// Clear any pending context menu image since user selected a file
pendingImageFile = null;
} else {
hideImagePreview();
}
});
// Form submissions
const forms = ['url', 'statement', 'log', 'photo', 'email'];
forms.forEach(type => {
const form = document.getElementById(`form-${type}`);
console.log(`Attaching handler for form-${type}:`, form);
form.addEventListener('submit', (e) => {
console.log(`Form ${type} submit event fired`);
handleFormSubmit(e, type);
});
});
// Paste target for photo form - click to enable paste
const pasteTarget = document.getElementById('paste-target');
console.log('Paste target element:', pasteTarget);
// Click to focus (enables paste)
pasteTarget.addEventListener('click', () => {
console.log('Paste target clicked, focusing...');
pasteTarget.focus();
});
// Update text on focus/blur
pasteTarget.addEventListener('focus', () => {
console.log('Paste target focused');
pasteTarget.querySelector('span').textContent = 'Ready - paste now (Ctrl+V)';
});
pasteTarget.addEventListener('blur', () => {
console.log('Paste target blurred');
pasteTarget.querySelector('span').textContent = 'Click to enable paste';
});
// Handle paste when paste target is focused (document-level since divs don't receive paste)
document.addEventListener('paste', async (e) => {
// Only handle if paste target is focused
if (document.activeElement !== pasteTarget) return;
console.log('Paste event received, paste target is focused');
const photoForm = document.getElementById('form-photo');
if (photoForm.classList.contains('hidden')) return;
console.log('Clipboard data:', e.clipboardData);
e.preventDefault();
const items = e.clipboardData?.items;
if (!items) return;
console.log('Items:', items.length);
for (const item of items) {
console.log('Item type:', item.type);
if (item.type.startsWith('image/')) {
const blob = item.getAsFile();
console.log('Blob:', blob);
if (!blob) continue;
// Create a File from the blob
const file = new File([blob], `screenshot-${Date.now()}.png`, { type: blob.type });
console.log('File created:', file.name, file.size);
// Build FormData from the photo form
const form = document.getElementById('form-photo');
const formData = new FormData();
formData.append('file', file);
formData.append('observation_name', form.querySelector('[name="observation_name"]').value);
formData.append('photo_description', form.querySelector('[name="photo_description"]').value);
formData.append('photo_url', form.querySelector('[name="photo_url"]').value);
formData.append('photo_date', form.querySelector('[name="photo_date"]').value);
// Submit
pasteTarget.classList.add('ready');
pasteTarget.querySelector('span').textContent = 'Submitting...';
try {
const response = await fetch(`${serverUrl}/create_observation/photo`, {
method: 'POST',
body: formData,
credentials: 'include',
headers: {
'Accept': 'application/json'
}
});
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
const result = await response.json();
if (result.success) {
showStatus('Photo observation created!');
form.reset();
hideImagePreview();
pendingImageFile = null;
} else if (result.error === 'login_required') {
showStatus('Session expired. Please login again.', true);
showScreen('login');
} else {
showStatus(result.error || 'Failed to create observation', true);
}
} else {
// Fallback
if (response.url.includes('/login')) {
showStatus('Session expired. Please login again.', true);
showScreen('login');
} else if (response.url.includes('/observations')) {
showStatus('Photo observation created!');
form.reset();
hideImagePreview();
pendingImageFile = null;
} else {
showStatus('Failed to create observation', true);
}
}
} catch (error) {
showStatus('Error: ' + error.message, true);
} finally {
pasteTarget.classList.remove('ready');
pasteTarget.querySelector('span').textContent = 'Click to enable paste';
}
return;
}
}
showStatus('No image found in clipboard', true);
});
// Email drop target
const emailDropTarget = document.getElementById('email-drop-target');
emailDropTarget.addEventListener('dragover', (e) => {
e.preventDefault();
emailDropTarget.classList.add('dragover');
});
emailDropTarget.addEventListener('dragleave', () => {
emailDropTarget.classList.remove('dragover');
});
emailDropTarget.addEventListener('drop', async (e) => {
e.preventDefault();
emailDropTarget.classList.remove('dragover');
const files = e.dataTransfer?.files;
if (!files || files.length === 0) return;
const file = files[0];
const validExtensions = ['.msg', '.eml'];
const hasValidExt = validExtensions.some(ext => file.name.toLowerCase().endsWith(ext));
if (!hasValidExt) {
showStatus('Please drop a .msg or .eml file', true);
return;
}
// Build FormData from the email form
const form = document.getElementById('form-email');
const formData = new FormData();
formData.append('msg_file', file);
formData.append('observation_name', form.querySelector('[name="observation_name"]').value);
formData.append('email_description', form.querySelector('[name="email_description"]').value);
// Submit
emailDropTarget.classList.add('ready');
emailDropTarget.querySelector('span').textContent = 'Submitting...';
try {
const response = await fetch(`${serverUrl}/create_observation/email`, {
method: 'POST',
body: formData,
credentials: 'include',
headers: {
'Accept': 'application/json'
}
});
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
const result = await response.json();
if (result.success) {
showStatus('Email observation created!');
form.reset();
} else if (result.error === 'login_required') {
showStatus('Session expired. Please login again.', true);
showScreen('login');
} else {
showStatus(result.error || 'Failed to create observation', true);
}
} else {
// Fallback
if (response.url.includes('/login')) {
showStatus('Session expired. Please login again.', true);
showScreen('login');
} else if (response.url.includes('/observations')) {
showStatus('Email observation created!');
form.reset();
} else {
showStatus('Failed to create observation', true);
}
}
} catch (error) {
showStatus('Error: ' + error.message, true);
} finally {
emailDropTarget.classList.remove('ready');
emailDropTarget.querySelector('span').textContent = 'Drop .msg or .eml file to submit';
}
});
console.log('=== ALL HANDLERS ATTACHED ===');
// Initialize
init();
});