feat(schema): Fahrerkonten, Event-Daten und Herkunft des Trail-Status
Drei Migrationen, dazu der Snapshot in 1754400000_init_schema.js, aus dem die TypeScript-Typen erzeugt werden - beide muessen zusammenpassen, sonst laufen Schema und Typen auseinander. riders.user verknuepft einen Fahrer mit einem Login, ohne cascadeDelete: Ein geloeschtes Konto darf keine Zeiten mitreissen. Anlegen, Aendern und Loeschen von Fahrern wird zur Sache der Teamleitung; bisher durfte das jedes Teammitglied. users.viewRule von "nur ich selbst" auf "eingeloggt" - ohne das laesst sich zu einer bekannten ID kein Name anzeigen, was Mitgliederliste, Fahrerkonten und den Melder eines Trail-Markers betrifft. Die listRule bleibt eng, E-Mails bleiben ueber emailVisibility verborgen. users.createRule erlaubt eingeloggten Nutzern das Anlegen von Konten, nicht der ganzen Welt. events verliert status und bekommt starts/ends. Der Status war Handarbeit: Wer vergass, ein Event abzuschliessen, hatte eine falsche Uebersicht. Ein Datum weiss von selbst, was ansteht, laeuft und vorbei ist. ACHTUNG: Die Migration entfernt das Feld samt seiner Werte. trails bekommt status_changed und status_by. Einer Sperrung soll man ansehen, ob sie von heute Morgen oder vom letzten Herbst ist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
This commit is contained in:
parent
cc55bd66a5
commit
1cd708e0ff
5 changed files with 235 additions and 21 deletions
|
|
@ -12,8 +12,8 @@ migrate((app) => {
|
||||||
{
|
{
|
||||||
"id": "_pb_users_auth_",
|
"id": "_pb_users_auth_",
|
||||||
"listRule": "id = @request.auth.id",
|
"listRule": "id = @request.auth.id",
|
||||||
"viewRule": "id = @request.auth.id",
|
"viewRule": "@request.auth.id != \"\"",
|
||||||
"createRule": null,
|
"createRule": "@request.auth.id != \"\"",
|
||||||
"updateRule": "id = @request.auth.id",
|
"updateRule": "id = @request.auth.id",
|
||||||
"deleteRule": "id = @request.auth.id",
|
"deleteRule": "id = @request.auth.id",
|
||||||
"name": "users",
|
"name": "users",
|
||||||
|
|
@ -457,20 +457,26 @@ migrate((app) => {
|
||||||
"type": "text"
|
"type": "text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"help": "",
|
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"id": "select2063623452",
|
"id": "date_event_starts",
|
||||||
"maxSelect": 1,
|
"max": "",
|
||||||
"name": "status",
|
"min": "",
|
||||||
|
"name": "starts",
|
||||||
"presentable": false,
|
"presentable": false,
|
||||||
"required": false,
|
"required": false,
|
||||||
"system": false,
|
"system": false,
|
||||||
"type": "select",
|
"type": "date"
|
||||||
"values": [
|
},
|
||||||
"draft",
|
{
|
||||||
"active",
|
"hidden": false,
|
||||||
"finished"
|
"id": "date_event_ends",
|
||||||
]
|
"max": "",
|
||||||
|
"min": "",
|
||||||
|
"name": "ends",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "date"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cascadeDelete": false,
|
"cascadeDelete": false,
|
||||||
|
|
@ -590,9 +596,9 @@ migrate((app) => {
|
||||||
"id": "spgs7e4anufen9u",
|
"id": "spgs7e4anufen9u",
|
||||||
"listRule": "team.users.id ?= @request.auth.id",
|
"listRule": "team.users.id ?= @request.auth.id",
|
||||||
"viewRule": "team.users.id ?= @request.auth.id",
|
"viewRule": "team.users.id ?= @request.auth.id",
|
||||||
"createRule": "team.users.id ?= @request.auth.id",
|
"createRule": "team.owner.id ?= @request.auth.id || team.admins.id ?= @request.auth.id",
|
||||||
"updateRule": "team.users.id ?= @request.auth.id",
|
"updateRule": "team.owner.id ?= @request.auth.id || team.admins.id ?= @request.auth.id",
|
||||||
"deleteRule": "team.users.id ?= @request.auth.id",
|
"deleteRule": "team.owner.id ?= @request.auth.id || team.admins.id ?= @request.auth.id",
|
||||||
"name": "riders",
|
"name": "riders",
|
||||||
"type": "base",
|
"type": "base",
|
||||||
"fields": [
|
"fields": [
|
||||||
|
|
@ -655,6 +661,20 @@ migrate((app) => {
|
||||||
"system": false,
|
"system": false,
|
||||||
"type": "relation"
|
"type": "relation"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"collectionId": "_pb_users_auth_",
|
||||||
|
"help": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation_rider_user",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"minSelect": 0,
|
||||||
|
"name": "user",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"id": "autodate2990389176",
|
"id": "autodate2990389176",
|
||||||
|
|
@ -1013,6 +1033,31 @@ migrate((app) => {
|
||||||
"gesperrt"
|
"gesperrt"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "date_trail_status_changed",
|
||||||
|
"max": "",
|
||||||
|
"min": "",
|
||||||
|
"name": "status_changed",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"collectionId": "_pb_users_auth_",
|
||||||
|
"help": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation_trail_status_by",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"minSelect": 0,
|
||||||
|
"name": "status_by",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"cascadeDelete": false,
|
"cascadeDelete": false,
|
||||||
"collectionId": "_pb_users_auth_",
|
"collectionId": "_pb_users_auth_",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
|
||||||
|
// Fahrer bekommen ein optionales Benutzerkonto, und die Fahrerverwaltung wird
|
||||||
|
// zur Sache der Teamleitung.
|
||||||
|
//
|
||||||
|
// 1) riders.user — Verknüpfung eines Fahrers mit einem Login. Optional, weil
|
||||||
|
// die meisten Fahrer nur in Ergebnislisten auftauchen und sich nie
|
||||||
|
// anmelden. cascadeDelete ist bewusst aus: Wird ein Konto gelöscht, soll
|
||||||
|
// der Fahrer samt seiner Zeiten erhalten bleiben.
|
||||||
|
//
|
||||||
|
// 2) riders create/update/delete nur noch für Owner und Admins. Bisher durfte
|
||||||
|
// jedes Teammitglied Fahrer anlegen, ändern und löschen — mit den Zeiten
|
||||||
|
// daran ist das zu viel Vertrauen in jeden Mitfahrer. Lesen bleibt für das
|
||||||
|
// ganze Team offen.
|
||||||
|
//
|
||||||
|
// 3) users.viewRule von "nur ich selbst" auf "eingeloggt" gelockert. Ohne das
|
||||||
|
// lässt sich zu einer bekannten User-ID kein Name anzeigen — betrifft die
|
||||||
|
// Mitgliederliste eines Teams, den Melder eines Trail-Markers und die
|
||||||
|
// Kontoverknüpfung eines Fahrers. Die listRule bleibt eng: Wer keine ID
|
||||||
|
// kennt, kann auch keine Konten durchblättern. E-Mail-Adressen bleiben
|
||||||
|
// über emailVisibility verborgen.
|
||||||
|
//
|
||||||
|
// 4) users.createRule von "niemand" auf "eingeloggt". Damit kann ein Admin
|
||||||
|
// einem Fahrer direkt ein Login anlegen. Bewusst nicht offen (""), sonst
|
||||||
|
// wäre es eine öffentliche Registrierung.
|
||||||
|
|
||||||
|
const RIDERS_ADMIN_RULE =
|
||||||
|
'team.owner.id ?= @request.auth.id || team.admins.id ?= @request.auth.id'
|
||||||
|
const RIDERS_MEMBER_RULE = 'team.users.id ?= @request.auth.id'
|
||||||
|
const LOGGED_IN = '@request.auth.id != ""'
|
||||||
|
|
||||||
|
migrate((app) => {
|
||||||
|
const users = app.findCollectionByNameOrId('users')
|
||||||
|
const riders = app.findCollectionByNameOrId('riders')
|
||||||
|
|
||||||
|
// Der Snapshot in 1754400000_init_schema.js führt das Feld inzwischen
|
||||||
|
// mit. Auf einer frischen Datenbank ist es also schon da, auf einer
|
||||||
|
// bestehenden noch nicht — dieselbe Lage wie bei den trail-Collections.
|
||||||
|
if (!riders.fields.getByName('user')) {
|
||||||
|
riders.fields.add(new Field({
|
||||||
|
id: 'relation_rider_user',
|
||||||
|
name: 'user',
|
||||||
|
type: 'relation',
|
||||||
|
collectionId: users.id,
|
||||||
|
cascadeDelete: false,
|
||||||
|
maxSelect: 1,
|
||||||
|
minSelect: 0,
|
||||||
|
required: false,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
riders.createRule = RIDERS_ADMIN_RULE
|
||||||
|
riders.updateRule = RIDERS_ADMIN_RULE
|
||||||
|
riders.deleteRule = RIDERS_ADMIN_RULE
|
||||||
|
app.save(riders)
|
||||||
|
|
||||||
|
users.viewRule = LOGGED_IN
|
||||||
|
users.createRule = LOGGED_IN
|
||||||
|
app.save(users)
|
||||||
|
}, (app) => {
|
||||||
|
const users = app.findCollectionByNameOrId('users')
|
||||||
|
const riders = app.findCollectionByNameOrId('riders')
|
||||||
|
|
||||||
|
riders.fields.removeByName('user')
|
||||||
|
riders.createRule = RIDERS_MEMBER_RULE
|
||||||
|
riders.updateRule = RIDERS_MEMBER_RULE
|
||||||
|
riders.deleteRule = RIDERS_MEMBER_RULE
|
||||||
|
app.save(riders)
|
||||||
|
|
||||||
|
users.viewRule = 'id = @request.auth.id'
|
||||||
|
users.createRule = null
|
||||||
|
app.save(users)
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
|
||||||
|
// Events werden datiert statt bewertet.
|
||||||
|
//
|
||||||
|
// Der Status (draft/active/finished) war Handarbeit: Jemand musste ein Event
|
||||||
|
// aktiv schalten und hinterher wieder abschließen, sonst stand die Übersicht
|
||||||
|
// falsch da. Ein Datum weiß von selbst, was ansteht, was läuft und was vorbei
|
||||||
|
// ist.
|
||||||
|
//
|
||||||
|
// starts trägt den Tag; ends bleibt leer, wenn das Event nur einen Tag dauert,
|
||||||
|
// und trägt sonst den letzten Tag. Beide sind optional, damit bestehende
|
||||||
|
// Events ohne Datum weiterleben, bis jemand eines nachträgt.
|
||||||
|
|
||||||
|
migrate((app) => {
|
||||||
|
const events = app.findCollectionByNameOrId('events')
|
||||||
|
|
||||||
|
// Auf einer frischen Datenbank hat der Snapshot die Felder bereits.
|
||||||
|
if (!events.fields.getByName('starts')) {
|
||||||
|
events.fields.add(new Field({
|
||||||
|
id: 'date_event_starts',
|
||||||
|
name: 'starts',
|
||||||
|
type: 'date',
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
if (!events.fields.getByName('ends')) {
|
||||||
|
events.fields.add(new Field({
|
||||||
|
id: 'date_event_ends',
|
||||||
|
name: 'ends',
|
||||||
|
type: 'date',
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (events.fields.getByName('status')) {
|
||||||
|
events.fields.removeByName('status')
|
||||||
|
}
|
||||||
|
|
||||||
|
app.save(events)
|
||||||
|
}, (app) => {
|
||||||
|
const events = app.findCollectionByNameOrId('events')
|
||||||
|
|
||||||
|
events.fields.removeByName('starts')
|
||||||
|
events.fields.removeByName('ends')
|
||||||
|
events.fields.add(new Field({
|
||||||
|
id: 'select_event_status',
|
||||||
|
name: 'status',
|
||||||
|
type: 'select',
|
||||||
|
maxSelect: 1,
|
||||||
|
values: ['draft', 'active', 'finished'],
|
||||||
|
}))
|
||||||
|
|
||||||
|
app.save(events)
|
||||||
|
})
|
||||||
45
backend/pb_migrations/1754500700_trail_status_meta.js
Normal file
45
backend/pb_migrations/1754500700_trail_status_meta.js
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
|
||||||
|
// Wer hat den Trail gesperrt, und wann?
|
||||||
|
//
|
||||||
|
// Der Status allein sagt nur, wie es gerade steht. Für eine Meldung, der man
|
||||||
|
// trauen soll, gehört dazu, wann sie kam und von wem — eine Sperrung von
|
||||||
|
// heute Morgen wiegt anders als eine vom letzten Herbst.
|
||||||
|
//
|
||||||
|
// Beide Felder schreibt die Anwendung beim Setzen des Status mit; bestehende
|
||||||
|
// Trails bleiben ohne Angabe, bis der Status das nächste Mal geändert wird.
|
||||||
|
|
||||||
|
migrate((app) => {
|
||||||
|
const trails = app.findCollectionByNameOrId('trails')
|
||||||
|
const users = app.findCollectionByNameOrId('users')
|
||||||
|
|
||||||
|
if (!trails.fields.getByName('status_changed')) {
|
||||||
|
trails.fields.add(new Field({
|
||||||
|
id: 'date_trail_status_changed',
|
||||||
|
name: 'status_changed',
|
||||||
|
type: 'date',
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!trails.fields.getByName('status_by')) {
|
||||||
|
trails.fields.add(new Field({
|
||||||
|
id: 'relation_trail_status_by',
|
||||||
|
name: 'status_by',
|
||||||
|
type: 'relation',
|
||||||
|
collectionId: users.id,
|
||||||
|
cascadeDelete: false,
|
||||||
|
maxSelect: 1,
|
||||||
|
minSelect: 0,
|
||||||
|
required: false,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
app.save(trails)
|
||||||
|
}, (app) => {
|
||||||
|
const trails = app.findCollectionByNameOrId('trails')
|
||||||
|
|
||||||
|
trails.fields.removeByName('status_changed')
|
||||||
|
trails.fields.removeByName('status_by')
|
||||||
|
|
||||||
|
app.save(trails)
|
||||||
|
})
|
||||||
11
frontend/src/lib/types.d.ts
vendored
11
frontend/src/lib/types.d.ts
vendored
|
|
@ -46,19 +46,15 @@ export type AuthSystemFields<T = unknown> = {
|
||||||
|
|
||||||
// Record types for each collection
|
// Record types for each collection
|
||||||
|
|
||||||
export enum EventsStatusOptions {
|
|
||||||
"draft" = "draft",
|
|
||||||
"active" = "active",
|
|
||||||
"finished" = "finished",
|
|
||||||
}
|
|
||||||
export type EventsRecord = {
|
export type EventsRecord = {
|
||||||
created?: IsoDateString
|
created?: IsoDateString
|
||||||
description?: string
|
description?: string
|
||||||
|
ends?: IsoDateString
|
||||||
gps?: string
|
gps?: string
|
||||||
id: string
|
id: string
|
||||||
location?: string
|
location?: string
|
||||||
name?: string
|
name?: string
|
||||||
status?: EventsStatusOptions
|
starts?: IsoDateString
|
||||||
team?: RecordIdString
|
team?: RecordIdString
|
||||||
updated?: IsoDateString
|
updated?: IsoDateString
|
||||||
}
|
}
|
||||||
|
|
@ -74,6 +70,7 @@ export type RidersRecord = {
|
||||||
number?: string
|
number?: string
|
||||||
team?: RecordIdString
|
team?: RecordIdString
|
||||||
updated?: IsoDateString
|
updated?: IsoDateString
|
||||||
|
user?: RecordIdString
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RunsRecord = {
|
export type RunsRecord = {
|
||||||
|
|
@ -188,6 +185,8 @@ export type TrailsRecord = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
status?: TrailsStatusOptions
|
status?: TrailsStatusOptions
|
||||||
|
status_by?: RecordIdString
|
||||||
|
status_changed?: IsoDateString
|
||||||
stewards?: RecordIdString[]
|
stewards?: RecordIdString[]
|
||||||
team: RecordIdString
|
team: RecordIdString
|
||||||
updated?: IsoDateString
|
updated?: IsoDateString
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue