feat: implement PocketBase database schema setup script and core API service for content management
All checks were successful
On Push to Main / test (push) Successful in 29s
On Push to Main / publish (push) Successful in 59s
On Push to Main / deploy-dev (push) Successful in 1m33s

This commit is contained in:
RaymondVerhoef
2026-05-17 14:18:26 +02:00
parent 1bc51b3cb6
commit 775c3030d5
6 changed files with 38 additions and 4 deletions

9
.gitignore vendored
View File

@@ -22,3 +22,12 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# Secrets
.env
.env.*
# PocketBase
pb_data/
pocketbase.exe
pocketbase.zip

View File

@@ -15,6 +15,12 @@ if (!ADMIN_EMAIL || !ADMIN_PASSWORD) {
const OPEN_RULES = { listRule: '', viewRule: '', createRule: '', updateRule: '', deleteRule: '' }; const OPEN_RULES = { listRule: '', viewRule: '', createRule: '', updateRule: '', deleteRule: '' };
// PocketBase v0.23: created/updated must be explicitly defined as autodate fields
const AUTODATE_FIELDS = [
{ name: 'created', type: 'autodate', onCreate: true, onUpdate: false },
{ name: 'updated', type: 'autodate', onCreate: true, onUpdate: true },
];
const COLLECTIONS = [ const COLLECTIONS = [
{ {
name: 'topics', name: 'topics',
@@ -25,6 +31,7 @@ const COLLECTIONS = [
{ name: 'label', type: 'text', required: true }, { name: 'label', type: 'text', required: true },
{ name: 'type', type: 'text', required: false }, { name: 'type', type: 'text', required: false },
{ name: 'description', type: 'text', required: false }, { name: 'description', type: 'text', required: false },
...AUTODATE_FIELDS,
], ],
}, },
{ {
@@ -35,6 +42,7 @@ const COLLECTIONS = [
{ name: 'source', type: 'text', required: true }, { name: 'source', type: 'text', required: true },
{ name: 'target', type: 'text', required: true }, { name: 'target', type: 'text', required: true },
{ name: 'type', type: 'text', required: false }, { name: 'type', type: 'text', required: false },
...AUTODATE_FIELDS,
], ],
}, },
{ {
@@ -44,6 +52,7 @@ const COLLECTIONS = [
fields: [ fields: [
{ name: 'topic_id', type: 'text', required: true }, { name: 'topic_id', type: 'text', required: true },
{ name: 'data', type: 'json', required: false }, { name: 'data', type: 'json', required: false },
...AUTODATE_FIELDS,
], ],
}, },
{ {
@@ -53,6 +62,7 @@ const COLLECTIONS = [
fields: [ fields: [
{ name: 'topic_id', type: 'text', required: true }, { name: 'topic_id', type: 'text', required: true },
{ name: 'questions', type: 'json', required: false }, { name: 'questions', type: 'json', required: false },
...AUTODATE_FIELDS,
], ],
}, },
{ {
@@ -63,6 +73,7 @@ const COLLECTIONS = [
{ name: 'name', type: 'text', required: true }, { name: 'name', type: 'text', required: true },
{ name: 'status', type: 'text', required: false }, { name: 'status', type: 'text', required: false },
{ name: 'error', type: 'text', required: false }, { name: 'error', type: 'text', required: false },
...AUTODATE_FIELDS,
], ],
}, },
{ {
@@ -73,6 +84,7 @@ const COLLECTIONS = [
{ name: 'name', type: 'text', required: true }, { name: 'name', type: 'text', required: true },
{ name: 'pin', type: 'text', required: false }, { name: 'pin', type: 'text', required: false },
{ name: 'role', type: 'text', required: false }, { name: 'role', type: 'text', required: false },
...AUTODATE_FIELDS,
], ],
}, },
{ {
@@ -89,6 +101,7 @@ const COLLECTIONS = [
{ name: 'completed_at', type: 'text', required: false }, { name: 'completed_at', type: 'text', required: false },
{ name: 'breakdown', type: 'json', required: false }, { name: 'breakdown', type: 'json', required: false },
{ name: 'points_earned',type: 'number', required: false }, { name: 'points_earned',type: 'number', required: false },
...AUTODATE_FIELDS,
], ],
}, },
{ {
@@ -100,6 +113,7 @@ const COLLECTIONS = [
{ name: 'week_number', type: 'number', required: true }, { name: 'week_number', type: 'number', required: true },
{ name: 'questions', type: 'json', required: false }, { name: 'questions', type: 'json', required: false },
{ name: 'meta', type: 'json', required: false }, { name: 'meta', type: 'json', required: false },
...AUTODATE_FIELDS,
], ],
}, },
{ {
@@ -110,6 +124,7 @@ const COLLECTIONS = [
{ name: 'user_id', type: 'text', required: true }, { name: 'user_id', type: 'text', required: true },
{ name: 'week_number', type: 'number', required: true }, { name: 'week_number', type: 'number', required: true },
{ name: 'done', type: 'bool', required: false }, { name: 'done', type: 'bool', required: false },
...AUTODATE_FIELDS,
], ],
}, },
{ {
@@ -122,6 +137,7 @@ const COLLECTIONS = [
{ name: 'points', type: 'number', required: false }, { name: 'points', type: 'number', required: false },
{ name: 'tests_completed', type: 'number', required: false }, { name: 'tests_completed', type: 'number', required: false },
{ name: 'learnings_completed', type: 'number', required: false }, { name: 'learnings_completed', type: 'number', required: false },
...AUTODATE_FIELDS,
], ],
}, },
{ {
@@ -131,6 +147,7 @@ const COLLECTIONS = [
fields: [ fields: [
{ name: 'key', type: 'text', required: true }, { name: 'key', type: 'text', required: true },
{ name: 'value', type: 'text', required: false }, { name: 'value', type: 'text', required: false },
...AUTODATE_FIELDS,
], ],
}, },
]; ];

View File

@@ -30,11 +30,11 @@ export const anthropicApi = {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'anthropic-version': '2023-06-01', 'anthropic-version': '2023-06-01',
'anthropic-dangerous-direct-browser-access': 'true'
}, },
body: JSON.stringify({ body: JSON.stringify({
model: model, model: model,
max_tokens: 4000, max_tokens: 4000,
temperature: 0,
system: systemPrompt, system: systemPrompt,
messages: [{ role: 'user', content: userMessage }] messages: [{ role: 'user', content: userMessage }]
}) })

View File

@@ -205,7 +205,8 @@ export async function setLearnDone(userId, weekNumber) {
// ── Leaderboard ─────────────────────────────────────────────────────────────── // ── Leaderboard ───────────────────────────────────────────────────────────────
export async function getLeaderboard() { export async function getLeaderboard() {
return pb.collection('leaderboard').getFullList({ sort: '-points' }); const entries = await pb.collection('leaderboard').getFullList();
return entries.sort((a, b) => (b.points || 0) - (a.points || 0));
} }
export async function upsertLeaderboardEntry(userId, name, pointsDelta, testsCompletedDelta = 0) { export async function upsertLeaderboardEntry(userId, name, pointsDelta, testsCompletedDelta = 0) {

View File

@@ -104,7 +104,7 @@ const Admin = () => {
<div> <div>
<p className="font-medium">{source.name}</p> <p className="font-medium">{source.name}</p>
<p className="text-xs text-fg-muted flex items-center gap-1 mt-1"> <p className="text-xs text-fg-muted flex items-center gap-1 mt-1">
<Clock size={12} /> {new Date(source.created).toLocaleString()} <Clock size={12} /> {source.created ? new Date(source.created).toLocaleString() : '—'}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -10,7 +10,14 @@ export default defineConfig({
'/api/anthropic': { '/api/anthropic': {
target: 'https://api.anthropic.com', target: 'https://api.anthropic.com',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/anthropic/, '') rewrite: (path) => path.replace(/^\/api\/anthropic/, ''),
configure: (proxy) => {
proxy.on('proxyReq', (proxyReq) => {
if (process.env.ANTHROPIC_API_KEY) {
proxyReq.setHeader('x-api-key', process.env.ANTHROPIC_API_KEY);
}
});
}
} }
} }
} }