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:
+16
-5
@@ -97,20 +97,30 @@ var serverStartCmd = &cobra.Command{
|
|||||||
Examples:
|
Examples:
|
||||||
opal server start
|
opal server start
|
||||||
opal server start --addr :8080
|
opal server start --addr :8080
|
||||||
|
opal server start --dev
|
||||||
opal server start --db /var/lib/opal/opal.db`,
|
opal server start --db /var/lib/opal/opal.db`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
addr, _ := cmd.Flags().GetString("addr")
|
addr, _ := cmd.Flags().GetString("addr")
|
||||||
dbPath, _ := cmd.Flags().GetString("db")
|
dbPath, _ := cmd.Flags().GetString("db")
|
||||||
|
devMode, _ := cmd.Flags().GetBool("dev")
|
||||||
|
|
||||||
// Override DB path if specified
|
// Override DB path if specified
|
||||||
if dbPath != "" {
|
if dbPath != "" {
|
||||||
os.Setenv("OPAL_DB_PATH", dbPath)
|
os.Setenv("OPAL_DB_PATH", dbPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate server configuration
|
// In dev mode, skip OAuth config validation
|
||||||
if err := validateServerConfig(); err != nil {
|
if devMode {
|
||||||
fmt.Fprintf(os.Stderr, "Server configuration validation failed:\n%v\n", err)
|
fmt.Println("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")
|
||||||
os.Exit(1)
|
fmt.Println("┃ ⚠ DEV MODE ENABLED ⚠ ┃")
|
||||||
|
fmt.Println("┃ Auth disabled — all requests use uid 1 ┃")
|
||||||
|
fmt.Println("┃ Do NOT use in production! ┃")
|
||||||
|
fmt.Println("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")
|
||||||
|
} else {
|
||||||
|
if err := validateServerConfig(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Server configuration validation failed:\n%v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load config (read-only — uses defaults if no opal.yml exists)
|
// Load config (read-only — uses defaults if no opal.yml exists)
|
||||||
@@ -127,7 +137,7 @@ Examples:
|
|||||||
defer engine.CloseDB()
|
defer engine.CloseDB()
|
||||||
|
|
||||||
// Create and start server
|
// Create and start server
|
||||||
server := api.NewServer(addr)
|
server := api.NewServer(addr, devMode)
|
||||||
if err := server.Start(); err != nil {
|
if err := server.Start(); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error starting server: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error starting server: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -194,6 +204,7 @@ func init() {
|
|||||||
|
|
||||||
serverStartCmd.Flags().StringP("addr", "a", ":8080", "Server address")
|
serverStartCmd.Flags().StringP("addr", "a", ":8080", "Server address")
|
||||||
serverStartCmd.Flags().StringP("db", "d", "", "Database path (default: config directory)")
|
serverStartCmd.Flags().StringP("db", "d", "", "Database path (default: config directory)")
|
||||||
|
serverStartCmd.Flags().Bool("dev", false, "Enable dev mode (no auth, no OAuth env vars required)")
|
||||||
|
|
||||||
keygenCmd.Flags().StringP("name", "n", "", "Name for this API key (e.g., device name)")
|
keygenCmd.Flags().StringP("name", "n", "", "Name for this API key (e.g., device name)")
|
||||||
keygenCmd.Flags().StringP("db", "d", "", "Database path (default: config directory)")
|
keygenCmd.Flags().StringP("db", "d", "", "Database path (default: config directory)")
|
||||||
|
|||||||
@@ -73,6 +73,16 @@ func GetUserID(r *http.Request) int {
|
|||||||
return userID
|
return userID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DevAuthMiddleware always injects userID=1 into context — for local dev only
|
||||||
|
func DevAuthMiddleware() func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := context.WithValue(r.Context(), userIDKey, 1)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CORSMiddleware adds CORS headers for future web frontend
|
// CORSMiddleware adds CORS headers for future web frontend
|
||||||
func CORSMiddleware() func(http.Handler) http.Handler {
|
func CORSMiddleware() func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
|
|||||||
@@ -11,15 +11,17 @@ import (
|
|||||||
|
|
||||||
// Server represents the API server
|
// Server represents the API server
|
||||||
type Server struct {
|
type Server struct {
|
||||||
router chi.Router
|
router chi.Router
|
||||||
addr string
|
addr string
|
||||||
|
devMode bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer creates a new API server
|
// NewServer creates a new API server
|
||||||
func NewServer(addr string) *Server {
|
func NewServer(addr string, devMode bool) *Server {
|
||||||
s := &Server{
|
s := &Server{
|
||||||
router: chi.NewRouter(),
|
router: chi.NewRouter(),
|
||||||
addr: addr,
|
addr: addr,
|
||||||
|
devMode: devMode,
|
||||||
}
|
}
|
||||||
s.setupRoutes()
|
s.setupRoutes()
|
||||||
return s
|
return s
|
||||||
@@ -39,16 +41,37 @@ func (s *Server) setupRoutes() {
|
|||||||
JSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
JSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||||
})
|
})
|
||||||
|
|
||||||
// OAuth endpoints (no auth required)
|
if s.devMode {
|
||||||
r.Get("/auth/login", handlers.GetLoginURL)
|
// Dev mode: fake session endpoint so the frontend can "log in"
|
||||||
r.Get("/auth/callback", handlers.OAuthCallback)
|
r.Get("/auth/dev-session", func(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Post("/auth/callback", handlers.OAuthCallback)
|
JSON(w, http.StatusOK, map[string]interface{}{
|
||||||
r.Post("/auth/refresh", handlers.RefreshToken)
|
"access_token": "dev-token",
|
||||||
r.Post("/auth/logout", handlers.Logout)
|
"refresh_token": "",
|
||||||
|
"expires_at": 9999999999,
|
||||||
|
"token_type": "bearer",
|
||||||
|
"user": map[string]interface{}{
|
||||||
|
"id": 1,
|
||||||
|
"username": "dev",
|
||||||
|
"email": "dev@localhost",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// OAuth endpoints (no auth required)
|
||||||
|
r.Get("/auth/login", handlers.GetLoginURL)
|
||||||
|
r.Get("/auth/callback", handlers.OAuthCallback)
|
||||||
|
r.Post("/auth/callback", handlers.OAuthCallback)
|
||||||
|
r.Post("/auth/refresh", handlers.RefreshToken)
|
||||||
|
r.Post("/auth/logout", handlers.Logout)
|
||||||
|
}
|
||||||
|
|
||||||
// Protected routes
|
// Protected routes
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(AuthMiddleware())
|
if s.devMode {
|
||||||
|
r.Use(DevAuthMiddleware())
|
||||||
|
} else {
|
||||||
|
r.Use(AuthMiddleware())
|
||||||
|
}
|
||||||
|
|
||||||
// Tasks
|
// Tasks
|
||||||
r.Route("/tasks", func(r chi.Router) {
|
r.Route("/tasks", func(r chi.Router) {
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
VITE_API_URL=https://opal.example.com/api
|
VITE_API_URL=https://opal.example.com/api
|
||||||
VITE_AUTH_URL=https://auth.example.com
|
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
|
VITE_OAUTH_ENABLED=true
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -17,17 +17,18 @@ import { getItem, setItem, removeItem } from '$lib/utils/storage.js';
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const STORAGE_KEY = 'opal_auth';
|
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
|
* Load auth state from localStorage
|
||||||
* @returns {AuthState}
|
* @returns {AuthState}
|
||||||
*/
|
*/
|
||||||
function loadAuth() {
|
function loadAuth() {
|
||||||
// In mock mode, always return authenticated
|
// In dev mode, auto-authenticate with a dev user.
|
||||||
if (MOCK_MODE) {
|
// API requests still go to the real backend (which runs with auth disabled).
|
||||||
|
if (DEV_MODE) {
|
||||||
return {
|
return {
|
||||||
accessToken: 'mock-token',
|
accessToken: 'dev-token',
|
||||||
refreshToken: '',
|
refreshToken: '',
|
||||||
expiresAt: 9999999999,
|
expiresAt: 9999999999,
|
||||||
user: { id: 1, username: 'dev', email: 'dev@localhost' },
|
user: { id: 1, username: 'dev', email: 'dev@localhost' },
|
||||||
|
|||||||
@@ -1,35 +1,18 @@
|
|||||||
import { writable, derived } from 'svelte/store';
|
import { writable, derived } from 'svelte/store';
|
||||||
import { tasks as tasksAPI } from '$lib/api/endpoints.js';
|
import { tasks as tasksAPI } from '$lib/api/endpoints.js';
|
||||||
import { queueChange } from '$lib/utils/sync-queue.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').Task} Task
|
||||||
* @typedef {import('$lib/api/types.js').TaskFilters} TaskFilters
|
* @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
|
* Create tasks store
|
||||||
*/
|
*/
|
||||||
function createTasksStore() {
|
function createTasksStore() {
|
||||||
const { subscribe, set, update } = writable(/** @type {Task[]} */ ([]));
|
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 {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
|
|
||||||
@@ -38,18 +21,6 @@ function createTasksStore() {
|
|||||||
* @param {string} reportName - Backend report name (e.g. 'list', 'next', 'completed')
|
* @param {string} reportName - Backend report name (e.g. 'list', 'next', 'completed')
|
||||||
*/
|
*/
|
||||||
async loadReport(reportName) {
|
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 {
|
try {
|
||||||
const tasks = await tasksAPI.listByReport(reportName);
|
const tasks = await tasksAPI.listByReport(reportName);
|
||||||
set(tasks);
|
set(tasks);
|
||||||
@@ -65,55 +36,9 @@ function createTasksStore() {
|
|||||||
* @returns {Promise<Task>}
|
* @returns {Promise<Task>}
|
||||||
*/
|
*/
|
||||||
async parseAndCreate(input) {
|
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 {
|
try {
|
||||||
const created = await tasksAPI.parse(input);
|
const result = await tasksAPI.parse(input);
|
||||||
|
const created = result.task ?? result;
|
||||||
update(tasks => [created, ...tasks]);
|
update(tasks => [created, ...tasks]);
|
||||||
return created;
|
return created;
|
||||||
} catch (error) {
|
} 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]
|
* @param {TaskFilters} [filters]
|
||||||
*/
|
*/
|
||||||
async load(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 {
|
try {
|
||||||
const tasks = await tasksAPI.list(filters);
|
const tasks = await tasksAPI.list(filters);
|
||||||
set(tasks);
|
set(tasks);
|
||||||
@@ -151,13 +66,6 @@ function createTasksStore() {
|
|||||||
* @param {Partial<Task>} task
|
* @param {Partial<Task>} task
|
||||||
*/
|
*/
|
||||||
async add(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 {
|
try {
|
||||||
const created = await tasksAPI.create(task);
|
const created = await tasksAPI.create(task);
|
||||||
update(tasks => [...tasks, created]);
|
update(tasks => [...tasks, created]);
|
||||||
@@ -191,14 +99,6 @@ function createTasksStore() {
|
|||||||
return tasks;
|
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 {
|
try {
|
||||||
const updated = await tasksAPI.update(uuid, updates);
|
const updated = await tasksAPI.update(uuid, updates);
|
||||||
// Sync with server response
|
// Sync with server response
|
||||||
@@ -228,11 +128,6 @@ function createTasksStore() {
|
|||||||
// Optimistic removal
|
// Optimistic removal
|
||||||
update(tasks => tasks.filter(t => t.uuid !== uuid));
|
update(tasks => tasks.filter(t => t.uuid !== uuid));
|
||||||
|
|
||||||
if (MOCK_MODE) {
|
|
||||||
mockData = mockData.filter(t => t.uuid !== uuid);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await tasksAPI.delete(uuid);
|
await tasksAPI.delete(uuid);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -250,20 +145,6 @@ function createTasksStore() {
|
|||||||
* @param {string} uuid
|
* @param {string} uuid
|
||||||
*/
|
*/
|
||||||
async complete(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 {
|
try {
|
||||||
await tasksAPI.complete(uuid);
|
await tasksAPI.complete(uuid);
|
||||||
update(tasks => tasks.filter(t => t.uuid !== uuid));
|
update(tasks => tasks.filter(t => t.uuid !== uuid));
|
||||||
|
|||||||
Executable
+32
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Start backend and frontend for local development.
|
||||||
|
# Backend runs with --dev (auth disabled), frontend points at localhost:8080.
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
echo ""
|
||||||
|
echo "Shutting down..."
|
||||||
|
kill $BACKEND_PID $FRONTEND_PID 2>/dev/null || true
|
||||||
|
wait $BACKEND_PID $FRONTEND_PID 2>/dev/null || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
# Start backend
|
||||||
|
echo "Starting backend (dev mode) on :8080..."
|
||||||
|
cd "$ROOT/opal-task"
|
||||||
|
go run . server start --dev --addr :8080 &
|
||||||
|
BACKEND_PID=$!
|
||||||
|
|
||||||
|
# Give the backend a moment to start before launching the frontend
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Start frontend
|
||||||
|
echo "Starting frontend..."
|
||||||
|
cd "$ROOT/opal-web"
|
||||||
|
bun run dev &
|
||||||
|
FRONTEND_PID=$!
|
||||||
|
|
||||||
|
wait
|
||||||
Reference in New Issue
Block a user