Files
backend/web/index.html
Andriy Oblivantsev 8a3cd2c27e
CI / test (push) Successful in 5s
Import pk from camera, QR visibility toggles, docs
- Add Import pk from camera: scan QR → restore pb → auto login → refresh
- Add scanner.js (jsQR) for camera QR decode
- QR visibility: pk shown by default, pb hidden by default (toggles)
- Update docs/frontend-development.md with scanner, Import pk, QR behavior

Made-with: Cursor
2026-03-01 14:05:43 +00:00

154 lines
8.6 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Momswap Geo Backend Use-Cases Test</title>
<link href="https://cdn.jsdelivr.net/npm/vuetify@3.7.7/dist/vuetify.min.css" rel="stylesheet" />
<style>
body {
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
}
.glass {
backdrop-filter: blur(8px);
background: rgba(15, 23, 42, 0.7);
border: 1px solid rgba(148, 163, 184, 0.2);
}
</style>
</head>
<body>
<div id="app">
<v-app theme="dark">
<v-container class="py-8">
<v-row>
<v-col cols="12">
<v-card class="glass rounded-xl pa-6">
<v-card-title class="text-h4 mb-2">Momswap Geo Backend Use-Cases Test</v-card-title>
<v-card-subtitle>Ed25519 auth, invitation onboarding, and user-scoped feature collections</v-card-subtitle>
</v-card>
</v-col>
</v-row>
<v-row>
<v-col cols="12" md="6">
<v-card class="glass rounded-xl pa-4">
<v-card-title>Connection & Identity</v-card-title>
<v-text-field v-model="apiBase" label="API Base URL"></v-text-field>
<v-btn color="primary" @click="rebuildClient">Apply API URL</v-btn>
<v-divider class="my-4"></v-divider>
<v-btn color="secondary" @click="ensureKeys">Ensure Keys in localStorage</v-btn>
<v-textarea v-model="state.publicKey" class="mt-3" label="Public Key (pb)" rows="2"></v-textarea>
<v-textarea v-model="state.privateKey" label="Private Key (pk, local only)" rows="2"></v-textarea>
<v-btn size="small" variant="outlined" class="mt-2" @click="restorePublicKeyFromPrivate">Restore Public Key from Private Key</v-btn>
<v-btn size="small" variant="outlined" color="primary" class="mt-2" @click="importPkFromCamera">Import pk from camera</v-btn>
<v-dialog v-model="state.showCameraDialog" max-width="400" persistent @click:outside="closeCameraDialog">
<v-card class="pa-4">
<v-card-title>Scan private key (pk) QR</v-card-title>
<v-card-text>
<video id="camera-video" autoplay playsinline muted class="rounded" style="width:100%;max-height:300px;background:#000"></video>
<v-alert v-if="state.cameraError" type="error" density="compact" class="mt-2">{{ state.cameraError }}</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="closeCameraDialog">Cancel</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-divider class="my-3"></v-divider>
<v-row v-if="state.qrPk || state.qrPb" dense>
<v-col v-if="state.qrPk" cols="auto">
<div class="text-caption text-error mb-1">QR: Private Key (pk, secret backup)</div>
<img v-show="state.showPrivateQR" :src="state.qrPk" alt="Private key QR" width="140" height="140" class="rounded" />
<v-btn size="small" variant="outlined" color="warning" @click="togglePrivateQR" class="mt-1">
{{ state.showPrivateQR ? 'Hide' : 'Show' }} private key (pk) QR
</v-btn>
</v-col>
<v-col v-if="state.qrPb" cols="auto">
<div v-if="state.showPublicQR">
<div class="text-caption text-medium-emphasis mb-1">QR: Public Key (pb)</div>
<img :src="state.qrPb" alt="Public key QR" width="140" height="140" class="rounded" />
</div>
<v-btn size="small" variant="outlined" @click="togglePublicQR" class="mt-1">
{{ state.showPublicQR ? 'Hide' : 'Show' }} public key (pb) QR
</v-btn>
</v-col>
</v-row>
<v-btn color="secondary" class="mt-2" @click="register">Register (sign service key)</v-btn>
<v-btn color="success" class="mt-2" @click="login">Login</v-btn>
</v-card>
</v-col>
<v-col v-if="state.accessToken" cols="12" md="6">
<v-card class="glass rounded-xl pa-4">
<v-card-title>Collections</v-card-title>
<v-text-field v-model="state.newCollectionName" label="New collection name" density="compact" hide-details class="mb-2"></v-text-field>
<v-btn color="primary" @click="createCollection" size="small" class="mr-2">Create Collection</v-btn>
<v-btn variant="outlined" @click="listCollections" size="small">Refresh</v-btn>
<v-list class="mt-3" density="compact">
<v-list-item
v-for="item in state.collections"
:key="item.id"
:active="state.selectedCollectionId === item.id"
@click="selectCollection(item.id)"
class="cursor-pointer rounded"
>
<v-list-item-title>{{ item.name }}</v-list-item-title>
<v-list-item-subtitle class="text-caption">{{ item.id }}</v-list-item-subtitle>
<template v-slot:append>
<v-btn icon variant="text" size="small" color="error" @click.stop="removeCollection(item.id)" aria-label="Remove">×</v-btn>
</template>
</v-list-item>
</v-list>
<v-card v-if="selectedCollection()" class="glass mt-4 pa-4" variant="tonal">
<v-card-title class="d-flex align-center flex-wrap gap-2">
<v-text-field
v-model="state.editingCollectionName"
@blur="onCollectionNameBlur"
density="compact"
hide-details
label="Collection name"
class="flex-grow-1"
style="max-width: 200px"
></v-text-field>
<v-btn variant="outlined" color="error" size="small" @click="removeCollection(selectedCollection().id)">Remove collection</v-btn>
<v-btn variant="text" size="small" @click="listFeatures(selectedCollection().id)">Refresh</v-btn>
</v-card-title>
<v-card-subtitle class="mb-2">Features (GeoJSON)</v-card-subtitle>
<v-row dense class="mb-3">
<v-col cols="6" sm="2">
<v-text-field v-model="state.newFeatureLon" label="Lon" type="number" density="compact" hide-details></v-text-field>
</v-col>
<v-col cols="6" sm="2">
<v-text-field v-model="state.newFeatureLat" label="Lat" type="number" density="compact" hide-details></v-text-field>
</v-col>
<v-col cols="8" sm="3">
<v-text-field v-model="state.newFeatureName" label="Name" density="compact" hide-details placeholder="Point"></v-text-field>
</v-col>
<v-col cols="4" sm="2">
<v-btn color="primary" @click="createFeature(selectedCollection().id)" size="small">Add feature</v-btn>
</v-col>
</v-row>
<div v-for="f in featuresFor(selectedCollection().id)" :key="f.id" class="d-flex align-center py-2 mb-1 rounded px-2" style="background: rgba(255,255,255,0.05)">
<span class="flex-grow-1">{{ formatFeature(f).name }} ({{ formatFeature(f).lon }}, {{ formatFeature(f).lat }})</span>
<v-btn variant="outlined" color="error" size="small" @click="removeFeature(selectedCollection().id, f.id)">Remove</v-btn>
</div>
<div v-if="featuresFor(selectedCollection().id).length === 0" class="text-medium-emphasis text-caption py-2">No features. Add a point above.</div>
</v-card>
</v-card>
</v-col>
<v-row>
<v-col cols="12">
<v-alert type="info" variant="tonal">{{ state.status }}</v-alert>
</v-col>
</v-row>
</v-container>
</v-app>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@3.5.13/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify@3.7.7/dist/vuetify.min.js"></script>
<script type="module" src="./app.js"></script>
</body>
</html>