initial commit

This commit is contained in:
ray
2026-07-12 10:17:17 -04:00
commit dab5a4ebc6
1424 changed files with 330463 additions and 0 deletions
@@ -0,0 +1,297 @@
---
name: pocketbase-setup
description: Deploy PocketBase (Firebase alternative) on a self-hosted Linux server — Docker, nginx reverse proxy with SSL, auth providers, and port conflict resolution.
triggers:
- "PocketBase"
- "Firebase alternative"
- "self-hosted auth"
- "backend as a service"
- "Google OAuth self-hosted"
related_skills:
- firebase-to-pocketbase-migration
---
# PocketBase Self-Hosted Deployment
PocketBase is a single-binary Firebase alternative — auth (email/password + OAuth2), database, file storage, and realtime subscriptions. Deploy it in Docker behind nginx with existing SSL certs.
## Docker Setup
```yaml
# docker-compose.yml
services:
pocketbase:
image: ghcr.io/muchobien/pocketbase:latest
container_name: pocketbase
restart: unless-stopped
ports:
- "127.0.0.1:8091:8090"
volumes:
- ./pb_data:/pb_data # ⚠️ /pb_data, NOT /pb/pb_data — entrypoint uses --dir=/pb_data
- ./pb_public:/pb_public # ⚠️ /pb_public, NOT /usr/local/bin/pb_public — entrypoint uses --publicDir=/pb_public
healthcheck:
test: wget --no-verbose --tries=1 --spider http://localhost:8090/api/health || exit 1
interval: 30s
timeout: 10s
retries: 3
```
**Mount path pitfall:** The entrypoint command is `pocketbase serve --dir=/pb_data --publicDir=/pb_public`. Mounting to `/pb/pb_data` or `/usr/local/bin/pb_public` (the binary's default) silently fails — PocketBase writes to an empty volume that survives `down`, and superusers/auth look like they vanished.
**Restart doesn't pick up compose changes:** After editing `docker-compose.yml`, `docker compose restart` reuses the old container config. Always use `docker compose down && docker compose up -d` to pick up new volumes, ports, or env vars.
**Port conflict pitfall:** Choose an INTERNAL host port carefully. On our server, port 8090 was taken by Pi-hole (`0.0.0.0:8090->80/tcp`). Mapping `127.0.0.1:8091:8090` avoids the conflict — nginx proxies to `:8091`, PocketBase listens on `:8090` internally. Always check with `ss -tlnp | grep :8090` before deploying.
## nginx Reverse Proxy (with WebSocket)
PocketBase needs WebSocket support for realtime subscriptions. Use an unused SSL port:
```nginx
server {
listen 3447 ssl;
server_name grajmedia.duckdns.org;
ssl_certificate /etc/letsencrypt/live/grajmedia.duckdns.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/grajmedia.duckdns.org/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://127.0.0.1:8091;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
client_max_body_size 50m;
}
```
Symlink into `sites-enabled/`, run `nginx -t && systemctl reload nginx`.
**Port conflict pitfall:** nginx binds to the SSL port on 0.0.0.0. Docker must NOT also expose that same port (even on 127.0.0.1) — nginx will fail to bind with "Address already in use". Docker gets an internal port (e.g., 8091), nginx owns the SSL port (e.g., 3447).
## Superuser Password Recovery
If the superuser password is lost and admin API authentication fails with `"Failed to authenticate."` (HTTP 400), the password can be reset directly via the SQLite database:
```bash
cd /path/to/pocketbase
docker compose stop pocketbase
# Remove stale WAL/SHM files and make DB writable
rm -f pb_data/data.db-shm pb_data/data.db-wal
chmod 666 pb_data/data.db
# Generate a bcrypt hash for the new password
python3 -c "
import bcrypt, sqlite3
hashed = bcrypt.hashpw(b'NewPassword123!', bcrypt.gensalt(rounds=10)).decode()
conn = sqlite3.connect('pb_data/data.db')
conn.execute('UPDATE _superusers SET password=? WHERE email=?', (hashed, 'admin@example.com'))
conn.commit()
conn.close()
print('Password updated')
"
# Restore permissions
chmod 644 pb_data/data.db
# Start PocketBase
docker compose start pocketbase
```
**Note:** Python's `bcrypt` library uses the `$2b$` prefix while PocketBase generates `$2a$` — both are accepted by PocketBase v0.23+.
After recovery, verify:
```bash
curl -s -X POST http://127.0.0.1:8091/api/collections/_superusers/auth-with-password \
-H 'Content-Type: application/json' \
-d '{"identity":"admin@example.com","password":"NewPassword123!"}' \
| python3 -c "import sys,json; d=json.load(sys.stdin); print('Token OK' if 'token' in d else 'FAILED')"
```
Create the admin account via CLI (faster than the UI):
```bash
docker exec pocketbase /usr/local/bin/pocketbase superuser upsert admin@example.com 'Str0ngPass!' --dir=/pb_data
```
The `--dir=/pb_data` flag is **critical** — without it, the `upsert` command writes to a different default directory and auth against the running server fails silently.
After admin is created, authenticate programmatically via:
```bash
# v0.22+ endpoint — NOT /api/admins/auth-with-password
curl -s -X POST http://127.0.0.1:8091/api/collections/_superusers/auth-with-password \
-H 'Content-Type: application/json' \
-d '{"identity":"admin@example.com","password":"..."}'
```
The old `/api/admins/auth-with-password` endpoint was removed in PocketBase v0.22+.
## Auth Provider Configuration
The `users` auth collection uses the internal system ID `_pb_users_auth_` — NOT the name `users`. All updates use `PATCH`, not `POST`.
### Email/Password (built-in)
Enable via API:
```bash
TOKEN=*** -s -X POST http://127.0.0.1:8091/api/collections/_superusers/auth-with-password \
-H 'Content-Type: application/json' \
-d '{"identity":"admin@example.com","password":"..."}' | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")
curl -s -X PATCH http://127.0.0.1:8091/api/collections/_pb_users_auth_ \
-H "Authorization: $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"passwordAuth":{"enabled":true,"minLength":8,"identityFields":["email"]}}'
```
### Google OAuth
1. Create a project in [Google Cloud Console](https://console.cloud.google.com/)
2. APIs & Services → Credentials → Create OAuth 2.0 Client ID → Web application
3. Add redirect URI: `https://<domain>:<port>/api/oauth2-redirect`
4. Enable via API:
```bash
curl -s -X PATCH http://127.0.0.1:8091/api/collections/_pb_users_auth_ \
-H "Authorization: $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"oauth2":{"enabled":true,"providers":{"google":{"enabled":true,"clientId":"YOUR_ID","clientSecret":"YOUR_SECRET"}}}}'
```
PocketBase handles the OAuth flow itself. No custom callback page needed — the SDK's `authWithOAuth2()` works after the provider is configured.
### Collection rules (API access control)
```bash
curl -s -X PATCH http://127.0.0.1:8091/api/collections/_pb_users_auth_ \
-H "Authorization: $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"listRule":"","viewRule":"","createRule":"",
"updateRule":"id = @request.auth.id",
"deleteRule":"id = @request.auth.id"
}'
```
## Verification
### Firebase Migration
If you're migrating an existing Firebase app, see the `firebase-to-pocketbase-migration` skill — it provides a Firebase-compatible adapter (`pocketbase.js`) that lets you swap one import per file without rewriting your app, plus a same-origin nginx config to eliminate CORS.
### Same-Origin App Hosting
When deploying a JavaScript app (Flutter Web, React, vanilla JS) that uses PocketBase, the cleanest approach is same-origin nginx: serve the static app AND proxy `/api/` to PocketBase from the same server block. The app calls `new PocketBase(window.location.origin)` and all API calls go to `/api/collections/...` on the same port — zero CORS configuration needed.
Use `templates/same-origin-nginx.conf` from `firebase-to-pocketbase-migration` as the template. Replace the generic PocketBase proxy with:
```nginx
root /path/to/your/app;
index index.html;
location / { try_files $uri $uri/ /index.html; }
location /api/ { proxy_pass http://127.0.0.1:8091; }
location /_/ { proxy_pass http://127.0.0.1:8091; }
```
### Static login page
A ready-to-use login page with Google OAuth + email/password is at `templates/login.html`. Copy it to `pb_public/index.html` — PocketBase serves it at the root `/`. It uses the PocketBase JS SDK from CDN and initializes with `new PocketBase(window.location.origin)`. No build step needed.
```bash
# Health check
curl -s http://127.0.0.1:8091/api/health
# Via nginx (external URL)
curl -sk --resolve <domain>:<port>:127.0.0.1 https://<domain>:<port>/api/health
```
## Production Hardening
### Log Rotation (Docker json-file driver)
By default the PB container uses `json-file` logs with no size cap. Add a `logging` block to `docker-compose.yml`:
```yaml
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
```
Then recreate: `docker compose up -d --force-recreate`. Verify: `docker inspect pocketbase --format '{{.HostConfig.LogConfig.Type}} {{json .HostConfig.LogConfig.Config}}'`.
**Pitfall:** `docker update --log-driver` does NOT support changing the log driver on a running container. You must recreate via `docker compose up -d --force-recreate`.
### Daily DB Backup Cron
```bash
#!/bin/bash
# /home/ray/docker/pocketbase/backup.sh
PB_CID=$(docker ps --filter "name=pocketbase" --format "{{.ID}}")
STAMP=$(date +%Y%m%d)
docker exec "$PB_CID" cp /pb_data/data.db "/pb_data/data.db.backup-$STAMP"
docker cp "$PB_CID:/pb_data/data.db.backup-$STAMP" "/backups/spq/"
docker exec "$PB_CID" rm "/pb_data/data.db.backup-$STAMP"
find /backups/spq/ -name "data.db.backup-*" -mtime +14 -delete
```
Crontab (root): `0 3 * * * /home/ray/docker/pocketbase/backup.sh >> /var/log/spq/backup.log 2>&1`
The script dynamically finds the container ID (it changes on recreate), copies the DB to `/backups/spq/`, and purges backups older than 14 days.
### Structured nginx Access Logs (JSON)
Add a `log_format` to the `http {}` block in `/etc/nginx/nginx.conf`:
```nginx
log_format json_combined escape=json '{ "time":"$time_iso8601", "remote":"$remote_addr", "method":"$request_method", "uri":"$uri", "status":$status, "bytes":$body_bytes_sent, "rt":$request_time }';
```
Then in the site config: `access_log /var/log/spq/spq.access.log json_combined;`
## Pitfalls
- **Mount paths must match entrypoint flags.** The image runs `serve --dir=/pb_data --publicDir=/pb_public`. Mount to `/pb_data` (not `/pb/pb_data`) and `/pb_public` (not `/usr/local/bin/pb_public`). Wrong paths cause silent data loss — database writes go to an empty volume.
- **`docker compose restart` ignores compose file changes.** After editing volumes, ports, or env vars, use `docker compose down && docker compose up -d`. `restart` reuses the old container config.
- **`pocketbase superuser upsert` needs `--dir` flag.** Default CWD is not `/pb_data`. Always pass `--dir=/pb_data` when running inside the container.
- **Auth API is `_superusers`, not `admins`.** PocketBase v0.22+ uses `POST /api/collections/_superusers/auth-with-password`. The old `/api/admins/auth-with-password` returns 404.
- **Users collection is `_pb_users_auth_` internally.** Use `PATCH /api/collections/_pb_users_auth_` (NOT `POST /api/collections/users`). POST to `users` works for initial creation; subsequent config changes need PATCH to the system ID.
- **Port conflicts are easy to miss.** Pi-hole often uses 8090 (host:container 80 mapping). Always `ss -tlnp | grep :8090` before deploying. Map to a different host port (e.g., 8091) and proxy via nginx.
- **nginx needs WebSocket headers.** Without `Upgrade` and `Connection` headers, PocketBase realtime silently fails.
- **Docker volume persists across rebuilds.** `docker compose down` doesn't remove `./pb_data/`. Safe to rebuild; admin account survives.
- **Admin auth leaks into same-origin apps.** Logging into the PocketBase Admin UI (`/_/`) stores the superuser token in localStorage for the origin. Any JavaScript app on the same origin that uses `pb.authStore.isValid` will see a valid session and auto-authenticate as admin. Fix: in the PocketBase SDK adapter, check `model.collectionName === 'users'` before returning a user. Reject `_superusers` model types. This prevents admin sessions from bypassing the app's login page.
- **Cached user tokens also bypass login.** After fixing the admin auth leak, the login page may still be skipped. The PocketBase SDK auto-restores ANY valid token from localStorage at initialization — including tokens from a previous `test@` or real user login. The adapter's admin filtering only rejects `_superusers`; a valid `users` collection token passes right through. When the user reports the login page not appearing after the admin fix, they likely have a stale user token. Fix: instruct them to clear site data (DevTools → Application → Clear storage), or add a manual sign-out that calls `pb.authStore.clear()`. Incognito mode is a quick test.
- **Shell token redaction.** When using `TOKEN=*** ... | python3 ...)` in bash, the token value may be redacted as `***` by secret filtering. Use `execute_code` with Python's `urllib` to pass tokens between API calls safely.
- **Collection schema API key changed in v0.23+.** PocketBase v0.22 and earlier used `"schema"` as the key for field definitions in API calls. v0.23+ changed this to `"fields"`. When scripting collection creation, check your version first: `docker exec pocketbase /usr/local/bin/pocketbase --version`. If ≥0.23, use `"fields"` not `"schema"`. The `firebase-to-pocketbase-migration` skill covers both formats.
- **PocketBase silently drops undefined fields.** Creating/updating records with fields not defined in the collection schema returns HTTP 200 but stores nothing for those fields — no error, no warning. Define all fields before any CRUD operations. See `references/add-fields-to-collection.md` for the PATCH pattern to add fields to existing collections.
- **`created`/`updated` system fields not present on collections created via API.** When collections are created programmatically (e.g., via `createCollection` or migration scripts), PocketBase may omit the `created` and `updated` autodate system fields. Any query using `sort: '-created'` or `sort: '-updated'` returns HTTP 400 `"Something went wrong while processing your request."` — a misleading error with no field name. **Diagnosis:** test with `sort: '-id'` first. If that works, the collection is missing system fields. **Fix:** either add the fields via admin or use `sort: '-id'` which is always available. Check ALL pages that query the collection — Dashboard, FinancialDashboard, RepairOrders, Invoices were all affected in one project by this single missing field.
- **SPA same-origin nginx with `/pb` SDK prefix.** When a React/Vite SPA uses PocketBase SDK with `PB_URL = '/pb'` (common default), the nginx config must handle BOTH SPA fallback AND the `/pb/` proxy. Standard pattern:
```nginx
root /path/to/spa/dist;
index index.html;
# SPA — all client-side routes serve index.html
location / { try_files $uri $uri/ /index.html; }
# PocketBase SDK proxy — SDK calls /pb/api/collections/...
location /pb/ { proxy_pass http://127.0.0.1:8091/; }
# Also keep direct /api/ proxy for non-SDK clients
location /api/ { proxy_pass http://127.0.0.1:8091/api/; }
```
Without the `/pb/` location, PocketBase SDK calls fail because the SPA fallback returns `index.html` instead of proxying to PocketBase.
- **User sign-up via PocketBase SDK.** The `pb.collection('users').create()` method accepts `{email, password, passwordConfirm, name, emailVisibility: true}`. PocketBase queues a verification email automatically when SMTP is configured in admin. Without SMTP, accounts are created but `verified: false` — users can still log in if the collection's auth rules allow it. Test sign-up through the nginx proxy, not just direct API calls — the `/pb/` location must be configured for the SDK to reach PocketBase.
@@ -0,0 +1,78 @@
# Adding Fields to an Existing PocketBase Collection
PocketBase silently drops fields not in the collection schema — no HTTP error, no warning, just data loss. If your app writes fields that PocketBase ignores, you need to add them to the schema.
## Detection
Signs that PocketBase is dropping fields:
- Code writes a field (e.g., `recommendation`, `explanation`) via `addDoc()` or `updateDoc()`
- The record is created/updated with HTTP 200
- But re-reading the record shows the field as `undefined`/missing
- The field works fine in Firestore but not PocketBase
## Fix: PATCH the Collection Schema
Adding fields requires sending the COMPLETE fields array (existing + new) via PATCH:
```python
#!/usr/bin/env python3
"""Add fields to existing PocketBase collection"""
import json, urllib.request, ssl
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
BASE = "http://localhost:8091"
COLLECTION_ID = "pbc_863811952" # from GET /api/collections
# Step 1: Auth as superuser
auth_resp = json.loads(urllib.request.urlopen(
urllib.request.Request(f"{BASE}/api/collections/_superusers/auth-with-password",
data=json.dumps({"identity": "admin@example.com", "password": "..."}).encode(),
headers={"Content-Type": "application/json"},
method="POST"), context=ctx).read())
token = auth_resp["token"]
# Step 2: Get current fields
collections = json.loads(urllib.request.urlopen(
urllib.request.Request(f"{BASE}/api/collections",
headers={"Authorization": f"Bearer {token}"}), context=ctx).read())
svc = next(c for c in collections["items"] if c["id"] == COLLECTION_ID)
existing_fields = svc["fields"]
# Step 3: Build new fields to add
new_fields = [
{"autogeneratePattern":"","hidden":False,"id":"text_new_1","max":0,"min":0,
"name":"explanation","pattern":"","presentable":False,"primaryKey":False,
"required":False,"system":False,"type":"text"},
{"autogeneratePattern":"","hidden":False,"id":"text_new_2","max":0,"min":0,
"name":"recommendation","pattern":"","presentable":False,"primaryKey":False,
"required":False,"system":False,"type":"text"},
]
# Step 4: PATCH with complete fields array (existing + new)
all_fields = existing_fields + new_fields
result = json.loads(urllib.request.urlopen(
urllib.request.Request(f"{BASE}/api/collections/{COLLECTION_ID}",
data=json.dumps({"fields": all_fields}).encode(),
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
method="PATCH"), context=ctx).read())
print(f"Updated fields: {[f['name'] for f in result['fields']]}")
```
## Critical Requirements
1. **Send ALL fields, not just new ones.** PATCH replaces the entire `fields` array. Sending only the new fields will DELETE all existing fields and break the collection.
2. **Each field needs a unique `id` string.** Use descriptive strings like `text_exp_001` — they just need to be unique within the collection.
3. **Field types matter.** `text` for strings, `number` for numbers, `bool` for booleans. Use `text` for most fields to be safe.
4. **No `system:true` for custom fields.** Only the built-in `id` field should have `system:true`.
## Never Do This
- **Never edit the SQLite database directly.** Use the REST API. Direct SQLite edits can corrupt field ID tracking and break the collection irreversibly.
- **Never use POST to update an existing collection.** POST creates a new collection; PATCH updates an existing one.
- **Don't forget to include the `id` field.** The system `id` field must remain in the array.
@@ -0,0 +1,48 @@
# Adding Fields to PocketBase Collection Schema
PocketBase silently drops fields not defined in the collection schema — HTTP 200, no error, data just disappears. When you add new fields to the data model, you MUST also add them to the PocketBase schema via API.
## The problem
Services saved with `recommendation` and `explanation` fields appeared to save successfully, but the fields were missing when read back. Root cause: the `services` collection schema only had `name`, `price`, `description`, `category`, `duration` — unknown fields were silently stripped on create/update.
## Detection
Query the collection schema to see current fields:
```python
# Auth + list collections
auth = api("POST", "/api/collections/_superusers/auth-with-password", {
"identity": "admin@example.com",
"password": "password"
})
token = auth["token"]
cols = api("GET", "/api/collections", token=token)
svc = next(c for c in cols["items"] if c["name"] == "services")
print([f["name"] for f in svc["fields"]])
# ['id', 'userId', 'name', 'description', 'price', 'category', ...]
```
## Fix: PATCH the collection
Add missing fields by PATCHing the complete fields array (including existing fields):
```python
existing_fields = svc["fields"]
new_fields = [
{"autogeneratePattern":"","hidden":False,"id":"text_rec_new","max":0,"min":0,
"name":"recommendation","pattern":"","presentable":False,"primaryKey":False,
"required":False,"system":False,"type":"text"},
{"autogeneratePattern":"","hidden":False,"id":"text_exp_new","max":0,"min":0,
"name":"explanation","pattern":"","presentable":False,"primaryKey":False,
"required":False,"system":False,"type":"text"},
]
result = api("PATCH", f"/api/collections/{svc['id']}",
{"fields": existing_fields + new_fields}, token=token)
```
**Important**: You must send the COMPLETE fields array, not just the new fields. PocketBase replaces the entire schema. Each field needs a unique `id` string — use descriptive names like `text_rec_001` or UUIDs.
## Full script
See `scripts/pb_add_fields.py` for a reusable script that auths, lists collections, and adds specified fields.
@@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""Add fields to PocketBase collection schema. PocketBase silently drops fields
not in schema — you MUST add them via API when extending the data model.
Usage: Edit FIELDS_TO_ADD and COLLECTION_NAME below, then run.
"""
import json, urllib.request, ssl
# === CONFIG ===
PB_URL = "http://localhost:8091"
SUPERUSER_EMAIL = "admin@example.com"
SUPERUSER_PASSWORD = "password"
COLLECTION_NAME = "services" # Change this
FIELDS_TO_ADD = [
# Example: add a text field
{"name": "recommendation", "type": "text"},
{"name": "explanation", "type": "text"},
# For other field types, see PocketBase API docs
# {"name": "duration", "type": "number", "onlyInt": False},
]
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
def api(method, path, data=None, token=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
body = json.dumps(data).encode() if data else None
req = urllib.request.Request(f"{PB_URL}{path}", data=body, headers=headers, method=method)
resp = urllib.request.urlopen(req, context=ctx)
return json.loads(resp.read())
# Step 1: Auth
auth_resp = api("POST", "/api/collections/_superusers/auth-with-password", {
"identity": SUPERUSER_EMAIL,
"password": SUPERUSER_PASSWORD
})
token = auth_resp["token"]
# Step 2: Get collection to see current fields
collections = api("GET", "/api/collections", token=token)
try:
col = next(c for c in collections["items"] if c["name"] == COLLECTION_NAME)
except StopIteration:
print(f"Collection '{COLLECTION_NAME}' not found. Available:",
[c["name"] for c in collections["items"]])
exit(1)
print(f"Collection: {col['name']} (id: {col['id']})")
print(f"Current fields: {[f['name'] for f in col['fields']]}")
# Step 3: Build new fields with unique IDs
existing_fields = col["fields"]
existing_names = {f["name"] for f in existing_fields}
new_field_defs = []
for f in FIELDS_TO_ADD:
if f["name"] in existing_names:
print(f"Field '{f['name']}' already exists, skipping")
continue
field_type = f.get("type", "text")
field_def = {
"autogeneratePattern": "",
"hidden": False,
"id": f"field_{f['name']}_{len(existing_fields) + len(new_field_defs)}",
"max": f.get("max", 0),
"min": f.get("min", 0),
"name": f["name"],
"pattern": f.get("pattern", ""),
"presentable": False,
"primaryKey": False,
"required": f.get("required", False),
"system": False,
"type": field_type,
}
if field_type == "number":
field_def["onlyInt"] = f.get("onlyInt", False)
new_field_defs.append(field_def)
if not new_field_defs:
print("No new fields to add.")
exit(0)
# Step 4: PATCH with complete fields array
all_fields = existing_fields + new_field_defs
result = api("PATCH", f"/api/collections/{col['id']}", {"fields": all_fields}, token=token)
print(f"Updated fields: {[f['name'] for f in result['fields']]}")
print(f"✅ Added {len(new_field_defs)} field(s) to {COLLECTION_NAME}")
@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login — grajmedia</title>
<script src="https://unpkg.com/pocketbase@0.21.5/dist/pocketbase.umd.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0d1117; color: #e6edf3;
display: flex; align-items: center; justify-content: center; min-height: 100vh;
}
.card { background: #161b22; border: 1px solid #30363d; border-radius: 12px; padding: 40px; width: 100%; max-width: 400px; }
.card h1 { font-size: 24px; margin-bottom: 8px; text-align: center; }
.card p.subtitle { color: #8b949e; text-align: center; margin-bottom: 28px; font-size: 14px; }
.divider { display: flex; align-items: center; margin: 24px 0; color: #484f58; font-size: 13px; }
.divider::before, .divider::after { content: ''; flex: 1; border-bottom: 1px solid #30363d; }
.divider span { padding: 0 16px; }
.btn { display: block; width: 100%; padding: 12px; border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; transition: all 0.2s; text-align: center; }
.btn-google { background: #fff; color: #24292f; border-color: #d0d7de; }
.btn-google:hover { background: #f3f4f6; }
.btn-primary { background: #238636; color: #fff; border-color: #2ea043; margin-top: 16px; }
.btn-primary:hover { background: #2ea043; }
.field { margin-bottom: 16px; }
.field label { display: block; font-size: 13px; color: #8b949e; margin-bottom: 6px; }
.field input { width: 100%; padding: 10px 12px; border-radius: 6px; border: 1px solid #30363d; background: #0d1117; color: #e6edf3; font-size: 14px; }
.field input:focus { outline: none; border-color: #58a6ff; box-shadow: 0 0 0 2px rgba(88,166,255,0.3); }
.error { background: #490202; border: 1px solid #f85149; color: #f85149; padding: 10px; border-radius: 6px; margin-bottom: 16px; font-size: 13px; display: none; }
.error.show { display: block; }
</style>
</head>
<body>
<div class="card">
<div id="login-form">
<h1>🔐 grajmedia</h1>
<p class="subtitle">Sign in to continue</p>
<div class="error" id="error"></div>
<!-- Google OAuth -->
<button class="btn btn-google" onclick="loginGoogle()">Sign in with Google</button>
<div class="divider"><span>or</span></div>
<!-- Email / Password -->
<div class="field"><label>Email</label><input type="email" id="email" placeholder="you@example.com"></div>
<div class="field"><label>Password</label><input type="password" id="password" placeholder="••••••••"></div>
<button class="btn btn-primary" onclick="loginPassword()">Sign in with Email</button>
<div style="text-align:center;margin-top:20px;">
<a href="#" style="color:#58a6ff;font-size:13px;" onclick="showRegister()">Create account</a>
</div>
</div>
</div>
<script>
const pb = new PocketBase(window.location.origin);
function showError(msg) { const e = document.getElementById('error'); e.textContent = msg; e.classList.add('show'); setTimeout(() => e.classList.remove('show'), 5000); }
async function loginGoogle() {
try {
await pb.collection('users').authWithOAuth2({ provider: 'google' });
window.location.href = '/index.html';
} catch (err) { showError('Google login failed: ' + err.message); }
}
async function loginPassword() {
try {
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
await pb.collection('users').authWithPassword(email, password);
window.location.href = '/index.html';
} catch (err) { showError(err.message); }
}
</script>
</body>
</html>