Demo app, collections/features CRUD, QR codes, docs
CI / test (push) Successful in 4s

Demo app (web/):
- Collections: select, rename, remove (x button per row), delete
- Features: add point (lon/lat validation), remove, list in selected collection
- QR codes for pk (private) and pb (public) keys
- Restore public key from private key
- 409 Conflict handled for already-registered login
- Title: Momswap Geo Backend Use-Cases Test

Backend:
- PATCH /v1/collections/{id} for rename
- DELETE /v1/collections/{id}
- Clearer lon/lat validation errors (-180..180, -90..90)

Client:
- updateCollection, deleteCollection, derivePublicKey

Docs:
- docs/frontend-development.md (demo app, local dev)
- README links to all docs

Made-with: Cursor
This commit is contained in:
2026-03-01 13:41:54 +00:00
parent ceeac1a1ee
commit ef3957b618
15 changed files with 512 additions and 26 deletions
+72 -12
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Momswap Geo Console</title>
<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 {
@@ -23,7 +23,7 @@
<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 Console</v-card-title>
<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>
@@ -37,28 +37,88 @@
<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" rows="2"></v-textarea>
<v-textarea v-model="state.privateKey" label="Private Key (local only)" rows="2"></v-textarea>
<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-divider class="my-3"></v-divider>
<v-row v-if="state.qrPk || state.qrPb" dense>
<v-col v-if="state.qrPb" cols="auto">
<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" />
</v-col>
<v-col v-if="state.qrPk" cols="auto">
<div v-if="state.showPrivateQR">
<div class="text-caption text-error mb-1">QR: Private Key (pk, secret backup)</div>
<img :src="state.qrPk" alt="Private key QR" width="140" height="140" class="rounded" />
</div>
<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-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 cols="12" md="6">
<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"></v-text-field>
<v-btn color="primary" @click="createCollection">Create Collection</v-btn>
<v-btn class="ml-2" @click="listCollections">Refresh</v-btn>
<v-list class="mt-4">
<v-list-item v-for="item in state.collections" :key="item.id">
<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>{{ item.id }}</v-list-item-subtitle>
<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-row>
<v-col cols="12">