first commit

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

111
AMO.md Normal file
View File

@ -0,0 +1,111 @@
# CrowdProof Observe - Firefox Add-on Submission
## Extension Details
**Name:** CrowdProof Observe
**Summary:**
Quick-add observations to your CrowdProof instance. Submit photos, URLs, emails, logs, and statements directly from your browser sidebar.
**Category:** Privacy & Security
**Tags:** productivity, evidence, documentation, observation, crowdproof
---
## Description
CrowdProof Observe is the official browser companion for CrowdProof, a collaborative evidence, observation management, intelligence collection, and publication platform.
### Features
- **Sidebar Interface** - Submit observations without leaving your current page
- **Multiple Observation Types** - Photos, URLs, emails (.msg/.eml), logs, and statements
- **Quick Capture** - Right-click images or links to send directly to CrowdProof
- **Paste to Submit** - Paste screenshots directly from clipboard
- **Access Management** - Set public access levels for your observations
- **Server Flexibility** - Connect to any CrowdProof instance
### How It Works
1. Configure your CrowdProof server URL in settings
2. Log in with your CrowdProof credentials
3. Use the sidebar or right-click context menus to submit observations
4. Manage public access settings for your observations
Note: This extension defaults to use the public instance of CrowdProof by design and intent, however, requires the configurability to point to other instances for SaaS and self-hosted clients and their users.
---
## Permissions Justification
| Permission | Reason |
|------------|--------|
| `storage` | Stores the user's configured server URL locally in browser storage. |
| `activeTab` | Captures the current page's URL and title when user clicks "Capture Current Page" for URL observations. |
| `cookies` | Maintains authenticated session cookies with the user's CrowdProof server. |
| `theme` | Adapts the sidebar appearance to match the user's Firefox theme. |
| `contextMenus` | Provides right-click menu options ("Send Image to CrowdProof", "Send Link to CrowdProof"). |
| `menus` | Required for `getTargetElement()` to access the clicked image element for capture. |
| `<all_urls>` | Necessary to communicate with user-configured CrowdProof server instances hosted at arbitrary domains. The extension does not access or modify page content on other sites. |
---
## Privacy Policy
**Last Updated:** 2025-12-30
### Data Collection
CrowdProof Observe does not collect, store, or transmit any user data to third parties.
### Server Communication
The extension communicates exclusively with the CrowdProof server URL configured by the user. All data submitted through the extension (observations, images, credentials) is sent only to this user-specified server.
### Local Storage
The extension stores only the following data locally in browser storage:
- The configured server URL
- Temporary pending image/URL data for context menu actions (cleared after use)
### Cookies
Session cookies are used to maintain authentication with the configured CrowdProof server. These cookies are managed by the browser and scoped to the server domain.
### No Tracking
This extension does not include any analytics, telemetry, or tracking functionality.
### Contact
For privacy inquiries, contact SILO GROUP at `pr@silogroup.org`
---
## Screenshots
Recommended screenshots for submission:
1. **Main sidebar** - Showing the photo observation form with tabs visible
2. **Access management** - Showing the observation list with access toggles
3. **Context menu** - Showing right-click "Send Image to CrowdProof" option
4. **Login screen** - Showing the branded login interface
---
## Technical Notes for Reviewers
- Source code is unobfuscated and readable
- No external scripts or CDN dependencies
- No data exfiltration - only communicates with user-configured server
- Standard WebExtension APIs only
- Server URL defaults to `https://crowdproof.silogroup.org` but is user-configurable
---
## Links
- **Homepage:** https://crowdproof.silogroup.org
- **Support:** `crowdproof-support@silogroup.org`
- **Privacy Policy:** https://www.silogroup.org/legal/privacy/

119
README.md Normal file
View File

@ -0,0 +1,119 @@
# CrowdProof Observe
A Firefox browser extension for quickly submitting observations to your CrowdProof instance.
## Features
- **Sidebar Interface** - Submit observations without leaving your current page
- **Multiple Observation Types** - Photos, URLs, emails (.msg/.eml), logs, and statements
- **Quick Capture** - Right-click images or links to send directly to CrowdProof
- **Paste to Submit** - Paste screenshots directly from clipboard
- **Access Management** - Set public access levels for your observations
- **Theme Integration** - Adapts to your Firefox theme
## Installation
### From Firefox Add-ons (Recommended)
Visit the [Firefox Add-ons page](#) and click "Add to Firefox".
### Manual Installation (Development)
1. Open Firefox and navigate to `about:debugging`
2. Click "This Firefox" in the sidebar
3. Click "Load Temporary Add-on"
4. Select the `manifest.json` file from this directory
## Usage
### Initial Setup
1. Click the CrowdProof Observe icon in the toolbar to open the sidebar
2. The extension defaults to `https://crowdproof.silogroup.org`
3. To use a different instance, click "Settings" and enter your server URL
4. Log in with your CrowdProof credentials
### Submitting Observations
**Photos:**
- Select a file using the file picker, OR
- Click the paste zone and press Ctrl+V to paste from clipboard, OR
- Right-click any image on a webpage and select "Send Image to CrowdProof"
**URLs:**
- Click "Capture Current Page" to grab the active tab's URL, OR
- Enter a URL manually, OR
- Right-click any link and select "Send Link to CrowdProof"
**Emails:**
- Select a .msg or .eml file, OR
- Drag and drop an email file onto the drop zone
**Logs & Statements:**
- Fill in the form fields and submit
### Managing Access
Click the "Access" button to view your observations and set public access levels:
- **None** - Only explicitly added users can access
- **Editor** - All active CrowdProof users can view and edit
- **Viewer** - All active CrowdProof users can view only
## Development
### Project Structure
```
crowdproof-observe/
├── manifest.json # Extension manifest
├── background/
│ └── background.js # Context menu and background tasks
├── sidebar/
│ ├── sidebar.html # Sidebar UI structure
│ ├── sidebar.css # Sidebar styles
│ └── sidebar.js # Sidebar logic
├── options/
│ ├── options.html # Options page
│ └── options.js # Options logic
├── icons/ # Extension icons
├── README.md # This file
└── AMO.md # Firefox Add-ons submission info
```
### Building
No build step required. The extension runs directly from source.
### Testing
1. Load the extension temporarily via `about:debugging`
2. Make changes to source files
3. Click "Reload" in `about:debugging` to apply changes
### Packaging
```bash
cd crowdproof-observe
zip -r crowdproof-observe.zip . -x "*.git*" -x "*.md"
```
## Permissions
| Permission | Purpose |
|------------|---------|
| `storage` | Store configured server URL |
| `activeTab` | Capture current page URL/title |
| `cookies` | Maintain server authentication |
| `theme` | Match Firefox theme colors |
| `contextMenus` | Right-click menu integration |
| `menus` | Access clicked elements for capture |
| `<all_urls>` | Connect to user-configured servers |
## Privacy
- No data collection or third-party transmission
- Communicates only with your configured CrowdProof server
- No analytics or tracking
See [AMO.md](AMO.md) for the full privacy policy.
## Support
- **Email:** crowdproof-support@silogroup.org
- **Homepage:** https://crowdproof.silogroup.org
## License
Copyright SILO GROUP. All rights reserved.

162
background/background.js Normal file
View File

@ -0,0 +1,162 @@
// CrowdProof Observe - Background Script
// Handles sidebar toggle and API communication
// Toggle sidebar when browser action button is clicked
browser.browserAction.onClicked.addListener(() => {
browser.sidebarAction.toggle();
});
// Create context menu for links
browser.contextMenus.create({
id: "send-url-to-crowdproof",
title: "Send URL to CrowdProof",
contexts: ["link"]
});
// Create context menu for images
browser.contextMenus.create({
id: "send-image-to-crowdproof",
title: "Send Image to CrowdProof",
contexts: ["image"]
});
// Handle context menu click
browser.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId === "send-url-to-crowdproof") {
// Open sidebar immediately (must be synchronous for user action context)
browser.sidebarAction.open();
// Then store the URL
browser.storage.local.set({ pendingUrl: info.linkUrl });
} else if (info.menuItemId === "send-image-to-crowdproof") {
// Open sidebar immediately
browser.sidebarAction.open();
// Then capture and store the image
const targetElementId = info.targetElementId;
browser.tabs.executeScript(tab.id, {
code: `
(function() {
try {
const img = browser.menus.getTargetElement(${targetElementId});
if (!img || img.tagName !== 'IMG') return null;
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
return canvas.toDataURL('image/png');
} catch (e) {
return null;
}
})();
`
}).then((results) => {
const dataUrl = results && results[0];
browser.storage.local.set({
pendingImage: {
dataUrl: dataUrl,
sourceUrl: info.srcUrl
}
});
});
}
});
const API = {
async getServerUrl() {
const result = await browser.storage.local.get('serverUrl');
return result.serverUrl || null;
},
async request(endpoint, options = {}) {
const serverUrl = await this.getServerUrl();
if (!serverUrl) {
throw new Error('Server URL not configured. Please set it in extension options.');
}
const url = `${serverUrl}${endpoint}`;
const fetchOptions = {
credentials: 'include',
...options
};
const response = await fetch(url, fetchOptions);
return response;
},
async login(username, password) {
const formData = new FormData();
formData.append('username', username);
formData.append('password', password);
const response = await this.request('/login', {
method: 'POST',
body: formData
});
// Login redirects on success, returns to login page on failure
// We check if we got redirected to a non-login page
return {
success: response.ok && !response.url.includes('/login'),
response
};
},
async logout() {
const response = await this.request('/logout', {
method: 'GET'
});
return response.ok;
},
async checkAuth() {
// Try to access a protected page to check if we're logged in
const response = await this.request('/profile', {
method: 'GET'
});
// If we get redirected to login, we're not authenticated
return !response.url.includes('/login');
},
async createObservation(type, formData) {
const response = await this.request(`/create_observation/${type}`, {
method: 'POST',
body: formData
});
return {
success: response.ok,
response
};
}
};
// Handle messages from popup
browser.runtime.onMessage.addListener(async (message, sender) => {
try {
switch (message.action) {
case 'getServerUrl':
return { serverUrl: await API.getServerUrl() };
case 'checkAuth':
const isAuthenticated = await API.checkAuth();
return { authenticated: isAuthenticated };
case 'login':
const loginResult = await API.login(message.username, message.password);
return { success: loginResult.success };
case 'logout':
const logoutSuccess = await API.logout();
return { success: logoutSuccess };
case 'createObservation':
const obsResult = await API.createObservation(message.type, message.formData);
return { success: obsResult.success };
default:
return { error: 'Unknown action' };
}
} catch (error) {
return { error: error.message };
}
});

BIN
icons/icon-48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

BIN
icons/icon-96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 B

51
manifest.json Normal file
View File

@ -0,0 +1,51 @@
{
"manifest_version": 2,
"name": "CrowdProof Observe",
"version": "1.0.0",
"description": "Send observations to CrowdProof from any webpage",
"permissions": [
"storage",
"activeTab",
"cookies",
"theme",
"contextMenus",
"menus",
"<all_urls>"
],
"browser_specific_settings": {
"gecko": {
"id": "crowdproof-observe@proofpoint.local"
}
},
"icons": {
"48": "icons/icon-48.png",
"96": "icons/icon-96.png"
},
"sidebar_action": {
"default_icon": {
"48": "icons/icon-48.png"
},
"default_title": "CrowdProof Observe",
"default_panel": "sidebar/sidebar.html"
},
"browser_action": {
"default_icon": {
"48": "icons/icon-48.png"
},
"default_title": "Toggle CrowdProof Observe"
},
"options_ui": {
"page": "options/options.html",
"browser_style": true
},
"background": {
"scripts": ["background/background.js"]
}
}

85
options/options.html Normal file
View File

@ -0,0 +1,85 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>CrowdProof Observe Settings</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
padding: 20px;
max-width: 500px;
margin: 0 auto;
}
h1 {
font-size: 1.5em;
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
input[type="url"] {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
input[type="url"]:focus {
outline: none;
border-color: #0066cc;
}
.help-text {
font-size: 12px;
color: #666;
margin-top: 5px;
}
button {
margin-top: 15px;
padding: 10px 20px;
background: #0066cc;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #0055aa;
}
.status {
margin-top: 10px;
padding: 10px;
border-radius: 4px;
display: none;
}
.status.success {
display: block;
background: #d4edda;
color: #155724;
}
.status.error {
display: block;
background: #f8d7da;
color: #721c24;
}
</style>
</head>
<body>
<h1>CrowdProof Observe Settings</h1>
<div>
<label for="serverUrl">Server URL</label>
<input type="url" id="serverUrl" placeholder="https://your-crowdproof-server.com">
<p class="help-text">Enter the full URL of your CrowdProof server (e.g., https://crowdproof.example.com)</p>
</div>
<button id="save">Save Settings</button>
<div id="status" class="status"></div>
<script src="options.js"></script>
</body>
</html>

42
options/options.js Normal file
View File

@ -0,0 +1,42 @@
// Load saved settings on page load
document.addEventListener('DOMContentLoaded', async () => {
const result = await browser.storage.local.get('serverUrl');
if (result.serverUrl) {
document.getElementById('serverUrl').value = result.serverUrl;
}
});
// Save settings
document.getElementById('save').addEventListener('click', async () => {
const serverUrl = document.getElementById('serverUrl').value.trim();
const statusEl = document.getElementById('status');
// Validate URL
if (!serverUrl) {
statusEl.textContent = 'Please enter a server URL';
statusEl.className = 'status error';
return;
}
try {
new URL(serverUrl);
} catch {
statusEl.textContent = 'Please enter a valid URL';
statusEl.className = 'status error';
return;
}
// Remove trailing slash for consistency
const normalizedUrl = serverUrl.replace(/\/+$/, '');
// Save to storage
await browser.storage.local.set({ serverUrl: normalizedUrl });
statusEl.textContent = 'Settings saved!';
statusEl.className = 'status success';
// Hide status after 2 seconds
setTimeout(() => {
statusEl.className = 'status';
}, 2000);
});

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();
});