feat: replace mock mode with real backend dev mode

Add --dev flag to `opal server start` that disables auth (injects
userID=1 for all requests) and exposes a /auth/dev-session endpoint,
so the frontend can develop against a real backend without OAuth
config. Remove VITE_MOCK_MODE and all mock data/branches from the
frontend stores. Add scripts/dev.sh to start both services locally.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 17:07:34 +01:00
parent 80ea17227d
commit d51c6da18d
8 changed files with 111 additions and 468 deletions
+1 -1
View File
@@ -2,5 +2,5 @@
VITE_API_URL=https://opal.example.com/api
VITE_AUTH_URL=https://auth.example.com
# OAuth
# OAuth — set to "false" for local dev (used with `opal server start --dev`)
VITE_OAUTH_ENABLED=true
-315
View File
@@ -1,315 +0,0 @@
/**
* Mock task data for local development / design review.
* Covers a realistic spread of projects, priorities, tags, due dates, and statuses.
*/
const now = Math.floor(Date.now() / 1000);
const HOUR = 3600;
const DAY = 86400;
/** @type {import('$lib/api/types.js').Task[]} */
export const mockTasks = [
// ── Pending tasks ────────────────────────────────────────────
{
uuid: '11111111-1111-4111-a111-111111111101',
id: 1,
status: 'P',
description: 'Set up Caddy reverse proxy for opal-web',
project: 'Infrastructure',
priority: 3,
created: now - 7 * DAY,
modified: now - 1 * DAY,
start: now - 2 * HOUR,
end: null,
due: now + 2 * DAY,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['devops', 'selfhosted'],
urgency: 14.2
},
{
uuid: '11111111-1111-4111-a111-111111111102',
id: 2,
status: 'P',
description: 'Write unit tests for task filter parsing',
project: 'Opal',
priority: 2,
created: now - 5 * DAY,
modified: now - 3 * DAY,
start: null,
end: null,
due: now + 5 * DAY,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['testing', 'backend'],
urgency: 7.3
},
{
uuid: '11111111-1111-4111-a111-111111111103',
id: 3,
status: 'P',
description: 'Fix tag extraction for nested wiki-links',
project: 'Jade',
priority: 2,
created: now - 3 * DAY,
modified: now - 3 * DAY,
start: null,
end: null,
due: null,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['bug'],
urgency: 4.1
},
{
uuid: '11111111-1111-4111-a111-111111111104',
id: 4,
status: 'P',
description: 'Grocery run - farmers market',
project: null,
priority: 1,
created: now - 1 * DAY,
modified: now - 1 * DAY,
start: null,
end: null,
due: now + 1 * DAY,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['errand'],
urgency: 3.5
},
{
uuid: '11111111-1111-4111-a111-111111111105',
id: 5,
status: 'P',
description: 'Design task detail page for opal-web',
project: 'Opal',
priority: 3,
created: now - 2 * DAY,
modified: now - 2 * DAY,
start: null,
end: null,
due: now - 1 * DAY, // overdue
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['frontend', 'design'],
urgency: 15.8
},
{
uuid: '11111111-1111-4111-a111-111111111106',
id: 6,
status: 'P',
description: 'Renew domain registration for jnss.me',
project: 'Infrastructure',
priority: 1,
created: now - 14 * DAY,
modified: now - 14 * DAY,
start: null,
end: null,
due: now + 30 * DAY,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['admin'],
urgency: 2.4
},
{
uuid: '11111111-1111-4111-a111-111111111107',
id: 7,
status: 'P',
description: 'Add recurrence UI to task creation form',
project: 'Opal',
priority: 1,
created: now - 4 * DAY,
modified: now - 4 * DAY,
start: null,
end: null,
due: null,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['frontend'],
urgency: 2.9
},
{
uuid: '11111111-1111-4111-a111-111111111108',
id: 8,
status: 'P',
description: 'Migrate Nextcloud to latest stable',
project: 'Infrastructure',
priority: 0,
created: now - 10 * DAY,
modified: now - 10 * DAY,
start: null,
end: null,
due: null,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['selfhosted', 'maintenance'],
urgency: 1.6
},
{
uuid: '11111111-1111-4111-a111-111111111109',
id: 9,
status: 'P',
description: 'Read "Designing Data-Intensive Applications" ch. 7',
project: null,
priority: 0,
created: now - 6 * DAY,
modified: now - 6 * DAY,
start: null,
end: null,
due: null,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['reading', 'learning'],
urgency: 1.2
},
{
uuid: '11111111-1111-4111-a111-111111111110',
id: 10,
status: 'P',
description: 'Review PR: sync conflict resolution strategy',
project: 'Opal',
priority: 2,
created: now - 1 * DAY,
modified: now - 1 * DAY,
start: null,
end: null,
due: now, // due today
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['review', 'backend'],
urgency: 10.5
},
// ── Completed tasks ──────────────────────────────────────────
{
uuid: '22222222-2222-4222-a222-222222222201',
id: 11,
status: 'C',
description: 'Implement XDG directory support',
project: 'Opal',
priority: 2,
created: now - 14 * DAY,
modified: now - 7 * DAY,
start: null,
end: now - 7 * DAY,
due: null,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['backend', 'refactor'],
urgency: 0
},
{
uuid: '22222222-2222-4222-a222-222222222202',
id: 12,
status: 'C',
description: 'Set up Authentik OAuth provider',
project: 'Infrastructure',
priority: 3,
created: now - 21 * DAY,
modified: now - 10 * DAY,
start: null,
end: now - 10 * DAY,
due: null,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['auth', 'selfhosted'],
urgency: 0
},
{
uuid: '22222222-2222-4222-a222-222222222203',
id: 13,
status: 'C',
description: 'Build setup wizard for first-run',
project: 'Opal',
priority: 2,
created: now - 10 * DAY,
modified: now - 5 * DAY,
start: null,
end: now - 5 * DAY,
due: null,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['ux', 'backend'],
urgency: 0
},
{
uuid: '22222222-2222-4222-a222-222222222204',
id: 14,
status: 'C',
description: 'Fix PersistentPreRun initialization order',
project: 'Opal',
priority: 3,
created: now - 8 * DAY,
modified: now - 6 * DAY,
start: null,
end: now - 6 * DAY,
due: null,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['bug', 'backend'],
urgency: 0
},
{
uuid: '22222222-2222-4222-a222-222222222205',
id: 15,
status: 'C',
description: 'Write deployment guide with Caddy config',
project: 'Opal',
priority: 1,
created: now - 9 * DAY,
modified: now - 6 * DAY,
start: null,
end: now - 6 * DAY,
due: null,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: ['docs'],
urgency: 0
}
];
+14 -13
View File
@@ -17,17 +17,18 @@ import { getItem, setItem, removeItem } from '$lib/utils/storage.js';
*/
const STORAGE_KEY = 'opal_auth';
const MOCK_MODE = import.meta.env.VITE_MOCK_MODE === 'true';
const DEV_MODE = import.meta.env.VITE_OAUTH_ENABLED === 'false';
/**
* Load auth state from localStorage
* @returns {AuthState}
*/
function loadAuth() {
// In mock mode, always return authenticated
if (MOCK_MODE) {
// In dev mode, auto-authenticate with a dev user.
// API requests still go to the real backend (which runs with auth disabled).
if (DEV_MODE) {
return {
accessToken: 'mock-token',
accessToken: 'dev-token',
refreshToken: '',
expiresAt: 9999999999,
user: { id: 1, username: 'dev', email: 'dev@localhost' },
@@ -73,10 +74,10 @@ function loadAuth() {
*/
function createAuthStore() {
const { subscribe, set, update } = writable(loadAuth());
return {
subscribe,
/**
* Set authentication tokens
* @param {AuthTokens} tokens
@@ -90,15 +91,15 @@ function createAuthStore() {
expiresAt: tokens.expires_at,
isAuthenticated: true
};
if (browser) {
setItem(STORAGE_KEY, newState);
}
return newState;
});
},
/**
* Set user info
* @param {User} user
@@ -112,7 +113,7 @@ function createAuthStore() {
return newState;
});
},
/**
* Set full auth data (tokens + user)
* @param {AuthTokens & {user: User}} data
@@ -125,14 +126,14 @@ function createAuthStore() {
user: data.user,
isAuthenticated: true
};
if (browser) {
setItem(STORAGE_KEY, newState);
}
set(newState);
},
/**
* Clear auth (logout)
*/
+3 -122
View File
@@ -1,35 +1,18 @@
import { writable, derived } from 'svelte/store';
import { tasks as tasksAPI } from '$lib/api/endpoints.js';
import { queueChange } from '$lib/utils/sync-queue.js';
import { generateUUID } from '$lib/utils/uuid.js';
/**
* @typedef {import('$lib/api/types.js').Task} Task
* @typedef {import('$lib/api/types.js').TaskFilters} TaskFilters
*/
const MOCK_MODE = import.meta.env.VITE_MOCK_MODE === 'true';
/** Report names that map to pending tasks in mock mode */
const PENDING_REPORTS = new Set(['list', 'next', 'active', 'ready', 'overdue', 'waiting', 'newest', 'oldest']);
/**
* Create tasks store
*/
function createTasksStore() {
const { subscribe, set, update } = writable(/** @type {Task[]} */ ([]));
/** @type {Task[]} */
let mockData = [];
/** Ensure mock data is loaded */
async function ensureMockData() {
if (mockData.length === 0) {
const { mockTasks } = await import('$lib/mock/tasks.js');
mockData = [...mockTasks];
}
}
return {
subscribe,
@@ -38,18 +21,6 @@ function createTasksStore() {
* @param {string} reportName - Backend report name (e.g. 'list', 'next', 'completed')
*/
async loadReport(reportName) {
if (MOCK_MODE) {
await ensureMockData();
if (reportName === 'completed') {
set(mockData.filter(t => t.status === 'C'));
} else if (PENDING_REPORTS.has(reportName)) {
set(mockData.filter(t => t.status === 'P'));
} else {
set(mockData.filter(t => t.status === 'P'));
}
return;
}
try {
const tasks = await tasksAPI.listByReport(reportName);
set(tasks);
@@ -65,55 +36,9 @@ function createTasksStore() {
* @returns {Promise<Task>}
*/
async parseAndCreate(input) {
if (MOCK_MODE) {
await ensureMockData();
// Naive parse: non-modifier words become description
const words = input.split(/\s+/);
const descWords = [];
const task = /** @type {Task} */ ({
uuid: generateUUID(),
id: mockData.length + 1,
status: 'P',
description: '',
project: null,
priority: 1,
created: Date.now() / 1000,
modified: Date.now() / 1000,
start: null,
end: null,
due: null,
scheduled: null,
wait: null,
until: null,
recurrence_duration: null,
parent_uuid: null,
tags: []
});
for (let i = 0; i < words.length; i++) {
const w = words[i];
if (w.startsWith('project:')) {
task.project = w.slice(8);
} else if (w.startsWith('priority:') || w.startsWith('pri:')) {
const val = w.includes(':') ? w.split(':')[1] : '1';
/** @type {Record<string, number>} */
const map = { H: 3, M: 2, L: 0, h: 3, m: 2, l: 0 };
task.priority = /** @type {import('$lib/api/types.js').TaskPriority} */ ((map[val] ?? parseInt(val, 10)) || 1);
} else if (w.startsWith('+')) {
task.tags = [...(task.tags || []), w.slice(1)];
} else {
descWords.push(w);
}
}
task.description = descWords.join(' ') || 'New task';
mockData = [task, ...mockData];
update(tasks => [task, ...tasks]);
return task;
}
try {
const created = await tasksAPI.parse(input);
const result = await tasksAPI.parse(input);
const created = result.task ?? result;
update(tasks => [created, ...tasks]);
return created;
} catch (error) {
@@ -123,20 +48,10 @@ function createTasksStore() {
},
/**
* Load all tasks from API (or mock data in dev)
* Load all tasks from API
* @param {TaskFilters} [filters]
*/
async load(filters = {}) {
if (MOCK_MODE) {
await ensureMockData();
let filtered = mockData;
if (filters.status) {
filtered = filtered.filter(t => t.status === filters.status);
}
set(filtered);
return;
}
try {
const tasks = await tasksAPI.list(filters);
set(tasks);
@@ -151,13 +66,6 @@ function createTasksStore() {
* @param {Partial<Task>} task
*/
async add(task) {
if (MOCK_MODE) {
const fullTask = /** @type {Task} */ ({ ...task, id: mockData.length + 1 });
mockData = [...mockData, fullTask];
update(tasks => [...tasks, fullTask]);
return fullTask;
}
try {
const created = await tasksAPI.create(task);
update(tasks => [...tasks, created]);
@@ -191,14 +99,6 @@ function createTasksStore() {
return tasks;
});
if (MOCK_MODE) {
const index = mockData.findIndex(t => t.uuid === uuid);
if (index >= 0) {
mockData[index] = { ...mockData[index], ...updates, modified: Date.now() / 1000 };
}
return;
}
try {
const updated = await tasksAPI.update(uuid, updates);
// Sync with server response
@@ -228,11 +128,6 @@ function createTasksStore() {
// Optimistic removal
update(tasks => tasks.filter(t => t.uuid !== uuid));
if (MOCK_MODE) {
mockData = mockData.filter(t => t.uuid !== uuid);
return;
}
try {
await tasksAPI.delete(uuid);
} catch (error) {
@@ -250,20 +145,6 @@ function createTasksStore() {
* @param {string} uuid
*/
async complete(uuid) {
if (MOCK_MODE) {
const mi = mockData.findIndex(t => t.uuid === uuid);
if (mi >= 0) {
mockData[mi] = {
...mockData[mi],
status: /** @type {'P'|'C'} */ ('C'),
end: Date.now() / 1000,
modified: Date.now() / 1000
};
}
update(tasks => tasks.filter(t => t.uuid !== uuid));
return;
}
try {
await tasksAPI.complete(uuid);
update(tasks => tasks.filter(t => t.uuid !== uuid));