Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions webapp/public/coi-serviceworker.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 13 additions & 13 deletions webapp/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,21 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- modified for WISPR-lab/data-export-gui -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="csrf-token" content="{{ csrf_token() }}" />
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<meta
http-equiv="Content-Security-Policy"
content="
default-src 'self';
script-src 'self' 'unsafe-eval' 'wasm-unsafe-eval' blob:;
connect-src 'self' blob: data: <%= process.env.NODE_ENV === 'development' ? 'ws: wss: http: https:' : '' %>;
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' data:;
worker-src 'self' blob:;
"
>
<!-- Disabled CSP meta tag to prevent breaking on client environments with restricted sources. Ceiling: permissive security; upgrade path: restore fine-grained CSP rules. -->
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title>LEStrADE</title>
<script>
if (window.crossOriginIsolated) {
sessionStorage.removeItem('coi_reload_attempted');
}
window.coi = {
doReload: () => {
const key = 'coi_reload_attempted';
Expand All @@ -46,8 +39,15 @@
}
}
};
if (navigator.serviceWorker && !navigator.serviceWorker.controller) {
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (!window.crossOriginIsolated) {
window.location.reload();
}
});
}
</script>
<script src="<%= BASE_URL %>vendor/coi-serviceworker.min.js"></script>
<script src="<%= BASE_URL %>coi-serviceworker.min.js"></script>
</head>
<body>
<div id="app"></div>
Expand Down
34 changes: 34 additions & 0 deletions webapp/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ limitations under the License.
</template>
</v-snackbar>

<!-- OPFS / cross-origin isolation incompatibility warning -->
<opfs-compatibility-dialog v-model="opfsDialog" />

<!-- Main router view -->
<router-view></router-view>

Expand All @@ -44,19 +47,43 @@ import EventBus from './event-bus.js'
import { initShutdownDetection } from '@/utils/shutdownDetection.js'
import DemoOverlay from '@/components/Demo/DemoOverlay.vue'
import CompareEventsDialog from '@/components/Events/CompareEventsDialog.vue'
import OpfsCompatibilityDialog from '@/components/OpfsCompatibilityDialog.vue'

export default {
name: 'app',
components: {
DemoOverlay,
CompareEventsDialog,
OpfsCompatibilityDialog,
},
data: function() {
return {
opfsDialog: false,
}
},
computed: {
snackbar() {
return this.$store.state.snackbar
},
},
watch: {
$route: function() {
if (this.checkOpfsIncompatibility()) {
this.opfsDialog = true
}
}
},
methods: {
checkOpfsIncompatibility: function() {
if (window.crossOriginIsolated) return false

const sabMissing = typeof SharedArrayBuffer === 'undefined'
const reloadAttempted = !!sessionStorage.getItem('coi_reload_attempted')
const currentRoute = this.$route
const routeRequiresOpfs = currentRoute && currentRoute.meta && currentRoute.meta.requiresOpfs

return sabMissing || reloadAttempted || routeRequiresOpfs
},
setErrorSnackBar: function (message) {
const snackbar = {
message: message,
Expand All @@ -72,6 +99,12 @@ export default {
mounted() {
// Listen on errors from REST API calls
EventBus.$on('errorSnackBar', this.setErrorSnackBar)
// Show modal if DB layer or router guard emits opfsUnavailable
EventBus.$on('opfsUnavailable', function() { this.opfsDialog = true; }.bind(this))

if (window.opfsUnavailable || this.checkOpfsIncompatibility()) {
this.opfsDialog = true;
}

const isDark = localStorage.getItem('isDarkTheme')
if (isDark) {
Expand All @@ -88,6 +121,7 @@ export default {
},
beforeDestroy() {
EventBus.$off('errorSnackBar')
EventBus.$off('opfsUnavailable')
},
}
</script>
Expand Down
9 changes: 9 additions & 0 deletions webapp/src/components/Import/PlatformSelector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Added for WISPR Lab / data-export-gui project
import { PLATFORM_METADATA } from '../../utils/uploadFormUtils.js'
import DiscordIcon from '../DiscordIcon.vue'
import ImportZone from './ImportZone.vue'
import EventBus from '@/event-bus.js'

export default {
name: 'PlatformSelector',
Expand All @@ -96,6 +97,14 @@ export default {
selectedPlatformId: null,
}
},
watch: {
dialog(val) {
if (val && !window.crossOriginIsolated) {
this.dialog = false
EventBus.$emit('opfsUnavailable')
}
}
},
computed: {
platforms() {
return Object.entries(PLATFORM_METADATA).map(([id, meta]) => ({ id, ...meta }))
Expand Down
160 changes: 160 additions & 0 deletions webapp/src/components/OpfsCompatibilityDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<template>
<v-dialog :value="dialogOpen" @input="setDialogOpen($event)" max-width="500px" persistent :z-index="25000">
<v-card>
<v-card-title class="headline">
Browser Incompatibility
<v-spacer></v-spacer>
<v-btn icon small @click="close"><v-icon>mdi-close</v-icon></v-btn>
</v-card-title>

<v-card-text>
<p class="mb-3">
This browser window doesn't support local data storage through OPFS (common in private browsing or restricted browser settings). You can browse documentation, but data import and event analysis are disabled.
</p>
<p class="mb-3">
Please try clicking <strong>Clear app data &amp; reload</strong> below. If the issue persists, try opening the app in a <strong>different browser</strong> or a non-private window.
</p>

<div v-if="opfsDiag" class="mb-4">
<strong>Reason:</strong> {{ opfsDiag.cause }}
</div>

<v-expansion-panels flat tile class="mb-3">
<v-expansion-panel>
<v-expansion-panel-header class="px-0 font-weight-medium" style="min-height: 36px">
Technical details
</v-expansion-panel-header>
<v-expansion-panel-content>
<v-simple-table dense>
<tbody>
<tr>
<td class="pl-0">crossOriginIsolated</td>
<td>false</td>
</tr>
<tr v-if="opfsDiag">
<td class="pl-0">SharedArrayBuffer</td>
<td>{{ opfsDiag.sabAvailable ? 'available' : 'unavailable' }}</td>
</tr>
<tr v-if="opfsDiag">
<td class="pl-0">Service worker supported</td>
<td>{{ opfsDiag.swSupported ? 'yes' : 'no' }}</td>
</tr>
<tr v-if="opfsDiag">
<td class="pl-0">coi-serviceworker registered</td>
<td>{{ opfsDiag.swRegistered ? 'yes' : 'no' }}</td>
</tr>
<tr v-if="opfsDiag">
<td class="pl-0">Intercepting this page load</td>
<td>{{ opfsDiag.swControlling ? 'yes' : 'no' }}</td>
</tr>
</tbody>
</v-simple-table>

<div class="mt-3 font-weight-bold">More documentation:</div>
<ul class="mt-1 pl-4">
<li>
<a href="https://sqlite.org/wasm/doc/trunk/persistence.md#coop-coep" target="_blank" rel="noopener">
SQLite WASM &amp; OPFS persistence
</a>
</li>
<li>
<a href="https://github.com/gzuidhof/coi-serviceworker" target="_blank" rel="noopener">
coi-serviceworker library
</a>
</li>
<li>
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API" target="_blank" rel="noopener">
MDN Service Worker API
</a>
</li>
</ul>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</v-card-text>

<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="error" text @click="clearAndReload">Clear app data &amp; reload</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>

<script>
import { diagnoseOpfsFailure } from '@/utils/opfsDiagnostics.js'
import EventBus from '@/event-bus.js'

export default {
name: 'OpfsCompatibilityDialog',
props: {
value: {
type: Boolean,
default: false
}
},
data: function() {
return {
internalOpen: false,
opfsDiag: null
}
},
computed: {
dialogOpen: function() {
return this.value || this.internalOpen
}
},
watch: {
dialogOpen: function(val) {
if (val && !this.opfsDiag) {
this.runDiagnostics()
}
}
},
mounted: function() {
var self = this
this._handler = function() {
self.internalOpen = true
self.runDiagnostics()
}
EventBus.$on('opfsUnavailable', this._handler)

if (this.dialogOpen) {
this.runDiagnostics()
}
},
beforeDestroy: function() {
if (this._handler) {
EventBus.$off('opfsUnavailable', this._handler)
}
},
methods: {
setDialogOpen: function(val) {
this.internalOpen = val
this.$emit('input', val)
},
runDiagnostics: function() {
var self = this
diagnoseOpfsFailure().then(function(diag) {
self.opfsDiag = diag
})
},
close: function() {
this.setDialogOpen(false)
},
clearAndReload: function() {
// ponytail: Unregisters stale service workers, clears local/session storage and reloads page
var p = navigator.serviceWorker
? navigator.serviceWorker.getRegistrations().then(function(regs) {
return Promise.all(regs.map(function(r) { return r.unregister(); }));
})
: Promise.resolve();
p.then(function() {
localStorage.clear();
sessionStorage.clear();
window.location.reload();
});
}
}
}
</script>
8 changes: 8 additions & 0 deletions webapp/src/components/SafeExitButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<script>
import { terminatePyodideWorker } from '@/pyodide/pyodide-client.js'
import { OPFSManager } from '@/storage/opfs_manager';
import EventBus from '@/event-bus.js'

export default {
name: 'SafeExitButton',
Expand All @@ -40,6 +41,12 @@ export default {
try {
console.log('[SafeExit] Initiating safe exit...')

// Unregister service workers (including coi-serviceworker) so they don't interfere on next visit
if (navigator.serviceWorker) {
const regs = await navigator.serviceWorker.getRegistrations()
await Promise.all(regs.map(function(r) { return r.unregister(); }))
}

terminatePyodideWorker();
const opfsManager = new OPFSManager();
await opfsManager.nukeAll();
Expand All @@ -55,6 +62,7 @@ export default {
window.location.replace('https://www.google.com')
} catch (error) {
console.error('[SafeExit] Error during safe exit:', error)
EventBus.$emit('opfsUnavailable')
window.close()
}
}
Expand Down
13 changes: 9 additions & 4 deletions webapp/src/database/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as uploads from './queries/uploads.js';
import * as comments from './queries/comments.js';
import * as metadata from './queries/metadata.js';
import { loadConfig } from '../utils/config.js';
import EventBus from '../event-bus.js';


let worker = null;
Expand Down Expand Up @@ -33,8 +34,6 @@ function callPyodideWorker(method, args) {
});
}

let cachedPaths = null;

async function getDbPaths() {
const cfg = await loadConfig();
const dbFilename = cfg.database.db_path.split('/').pop(); // e.g., "userdata.db"
Expand All @@ -57,11 +56,17 @@ export async function getDB() {
return {
async exec(sql, options) {
const { schemaPath, dbPath } = await getDbPaths();
return callPyodideWorker('exec', {
sql,
return callPyodideWorker('exec', {
sql,
options: options || {},
schemaPath,
dbPath,
}).catch(function(err) {
// OpfsDb not a constructor = OPFS unavailable (crossOriginIsolated false at worker init time)
if (err && err.message && err.message.indexOf('OpfsDb') !== -1 && err.message.indexOf('constructor') !== -1) {
EventBus.$emit('opfsUnavailable');
}
throw err;
});
}
};
Expand Down
4 changes: 3 additions & 1 deletion webapp/src/database/sqlite-worker.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// modified for WISPR-lab/data-export-gui

let sqlite3 = null;
let initializedDbs = new Set(); // Track which DBs have been initialized

Expand Down Expand Up @@ -44,7 +46,7 @@ self.onmessage = async (e) => {

const result = db.exec(args.sql, args.options);

db.close();
db.close();

self.postMessage({ id, result, success: true });
} catch (error) {
Expand Down
Loading
Loading