fix: resolve all 15 svelte-check type errors
- Type headers as Record<string, string> in apiRequest (client.js)
- Annotate SyncResult on result object, cast catch vars to any (sync.js)
- Widen sync.push param to Partial<Task>[] (endpoints.js)
- Fix parse() return type to reflect {task?: Task} shape (endpoints.js)
- Narrow add() param from Partial<Task> to Task (tasks.js)
- Cast parseAndCreate result to Task (tasks.js)
- Type tasksByProject grouped object as Record<string, Task[]> (tasks.js)
svelte-check now reports 0 errors and 0 warnings.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,482 +0,0 @@
|
||||
# Config System Redesign
|
||||
|
||||
## 1. Problem Statement
|
||||
|
||||
`LoadConfig()` panics on the server because the config system assumes a writable
|
||||
config directory. On the server, `OPAL_CONFIG_DIR=/etc/opal` is read-only
|
||||
(systemd `ReadOnlyPaths`), `opal.yml` doesn't exist (Ansible only deploys
|
||||
`opal.env`), and the attempt to create it fails. The nil `*Config` propagates
|
||||
unchecked through `GetConfig()` callers to `BuildUrgencyCoefficients(nil)`,
|
||||
causing a nil-pointer panic.
|
||||
|
||||
### Panic chain
|
||||
|
||||
```
|
||||
sortByUrgency() report.go:426
|
||||
cfg, _ := GetConfig() ← error discarded, cfg = nil
|
||||
coeffs := BuildUrgencyCoefficients(cfg)
|
||||
return &UrgencyCoefficients{
|
||||
Due: cfg.UrgencyDue, ← nil dereference → PANIC
|
||||
}
|
||||
```
|
||||
|
||||
### Root cause
|
||||
|
||||
The config system was designed for the CLI (user-writable `~/.config/opal/`)
|
||||
and has three interacting issues:
|
||||
|
||||
1. **Write-on-read**: `LoadConfig()` creates directories and writes a default
|
||||
`opal.yml` as a side effect of *reading* config. This fails when the
|
||||
filesystem is read-only.
|
||||
2. **Error swallowing**: All internal callers use `cfg, _ := GetConfig()`,
|
||||
turning a load failure into a nil-pointer panic instead of a graceful error.
|
||||
3. **No mode awareness**: The same code path runs for CLI users (writable home
|
||||
dir, interactive, config file is useful) and for the server (read-only
|
||||
`/etc/opal`, headless, defaults are fine).
|
||||
|
||||
---
|
||||
|
||||
## 2. Current Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "Config Sources"
|
||||
ENV["opal.env<br/>(systemd EnvironmentFile)"]
|
||||
YML["opal.yml<br/>(Viper / YAML)"]
|
||||
DEFAULTS["Hardcoded defaults<br/>(in LoadConfig)"]
|
||||
end
|
||||
|
||||
subgraph "Loading Mechanisms"
|
||||
AUTH_LOAD["auth.LoadConfig()<br/>os.Getenv() → auth.Config"]
|
||||
ENGINE_LOAD["engine.LoadConfig()<br/>Viper → engine.Config"]
|
||||
end
|
||||
|
||||
subgraph "Consumers"
|
||||
SERVER["API Server<br/>(handlers, middleware)"]
|
||||
CLI["CLI Commands<br/>(cmd/ package)"]
|
||||
end
|
||||
|
||||
ENV --> AUTH_LOAD
|
||||
YML --> ENGINE_LOAD
|
||||
DEFAULTS --> ENGINE_LOAD
|
||||
|
||||
AUTH_LOAD --> SERVER
|
||||
ENGINE_LOAD --> SERVER
|
||||
ENGINE_LOAD --> CLI
|
||||
```
|
||||
|
||||
### Two independent config subsystems
|
||||
|
||||
| Aspect | `engine.Config` | `auth.Config` |
|
||||
|--------|----------------|---------------|
|
||||
| **Source** | `opal.yml` via Viper | Environment variables |
|
||||
| **Loaded by** | `engine.LoadConfig()` | `auth.LoadConfig()` |
|
||||
| **Caching** | Singleton (`globalConfig`) | None (re-read each call) |
|
||||
| **Write side effects** | Creates dir + file on load | None |
|
||||
| **Error model** | Returns `(*Config, error)` | Returns `*Config` (no error) |
|
||||
| **Used by** | CLI + Server (lazy) | Server only |
|
||||
|
||||
### `engine.Config` field categories
|
||||
|
||||
| Category | Fields | CLI | Server | Notes |
|
||||
|----------|--------|-----|--------|-------|
|
||||
| **Display** | `DefaultFilter`, `DefaultSort`, `DefaultReport`, `ColorOutput` | Yes | No | Terminal-only |
|
||||
| **Date** | `WeekStartDay`, `DefaultDueTime` | Yes | Yes | Used by `ParseDate()` |
|
||||
| **Urgency** | 13 urgency coefficients | Yes | Yes | Core scoring logic |
|
||||
| **Limits** | `NextLimit` | Yes | Indirectly | Report limit |
|
||||
| **Sync** | 6 sync fields | Yes | No | Client-side sync config |
|
||||
|
||||
Key observation: The server only needs **urgency coefficients** and **date
|
||||
settings** from `engine.Config`. It doesn't need display preferences or sync
|
||||
settings. But all 30 fields are loaded through the same mechanism.
|
||||
|
||||
### `engine.LoadConfig()` flow
|
||||
|
||||
```
|
||||
1. Check globalConfig singleton → return if cached
|
||||
2. GetConfigDir() → resolve directory path
|
||||
3. os.MkdirAll(configDir) ← FAILS on read-only FS
|
||||
4. Viper.ReadInConfig()
|
||||
5. If read fails:
|
||||
a. Viper.WriteConfigAs() ← FAILS on read-only FS
|
||||
b. Re-read written file
|
||||
6. Unmarshal → Config struct
|
||||
7. Cache in globalConfig
|
||||
```
|
||||
|
||||
Steps 3 and 5a are the failure points on the server.
|
||||
|
||||
### Caller error handling audit
|
||||
|
||||
| Caller | File:Line | Error handling |
|
||||
|--------|-----------|----------------|
|
||||
| `FormatTaskListWithFormat` | display.go:24 | `cfg, _ := GetConfig()` — **ignored** |
|
||||
| `formatMinimalLine` | display.go:119 | `cfg, _ := GetConfig()` — **ignored** |
|
||||
| `NextReport.SortFunc` | report.go:168 | `cfg, _ := GetConfig()` — **ignored** |
|
||||
| `NextReport.LimitFunc` | report.go:187 | `cfg, _ := GetConfig()` — **ignored** |
|
||||
| `sortByUrgency` | report.go:426 | `cfg, _ := GetConfig()` — **ignored** |
|
||||
| `PopulateUrgency` | task.go:769 | `cfg, _ := GetConfig()` — **ignored** |
|
||||
| `getWeekStart` | dateparse.go:484 | Checked — falls back to Monday |
|
||||
| All cmd/ callers | root.go, sync.go | Checked — exits on error |
|
||||
|
||||
Every engine-internal caller except `dateparse.go` discards the error.
|
||||
|
||||
---
|
||||
|
||||
## 3. Additional Issues Found
|
||||
|
||||
### 3a. Config file clobbering
|
||||
|
||||
`LoadConfig()` line 296-306 catches **any** `ReadInConfig` error (not just
|
||||
"file not found") and overwrites the config file with defaults. A YAML syntax
|
||||
error in the user's `opal.yml` silently destroys their customizations.
|
||||
|
||||
### 3b. `SaveConfig()` manual field sync
|
||||
|
||||
Adding a new config field requires updating three places:
|
||||
1. `Config` struct definition
|
||||
2. `LoadConfig()` — `v.SetDefault(...)` call
|
||||
3. `SaveConfig()` — `v.Set(...)` call
|
||||
|
||||
Missing any one of these causes silent data loss on save or missing defaults.
|
||||
|
||||
### 3c. `auth.LoadConfig()` silent parse failures
|
||||
|
||||
```go
|
||||
jwtExpiry, _ := strconv.Atoi(getEnv("JWT_EXPIRY", "3600"))
|
||||
```
|
||||
|
||||
If `JWT_EXPIRY=abc`, `Atoi` returns `(0, error)`, error is discarded, and
|
||||
`JWTExpiry` silently becomes 0 (tokens expire immediately).
|
||||
|
||||
### 3d. Insecure JWT default
|
||||
|
||||
`JWT_SECRET` defaults to `"change-me-in-production"`. While `validateServerConfig()`
|
||||
checks for empty values, it doesn't check for the insecure default.
|
||||
|
||||
### 3e. Sync API key in plaintext
|
||||
|
||||
`sync_api_key` is stored in `opal.yml` with no special file permissions.
|
||||
|
||||
### 3f. No env-var override of YAML config
|
||||
|
||||
Viper's `AutomaticEnv()` is never called, so there's no way to override YAML
|
||||
config values via environment variables. This is an obstacle for 12-factor
|
||||
server deployments.
|
||||
|
||||
---
|
||||
|
||||
## 4. Design Options
|
||||
|
||||
### Option A: Minimal fix — graceful fallback to defaults
|
||||
|
||||
**Change**: Make `LoadConfig()` return a `DefaultConfig()` when it can't read
|
||||
or write the config file, instead of returning nil.
|
||||
|
||||
```go
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
DefaultFilter: "status:pending",
|
||||
DefaultSort: "due,priority",
|
||||
// ... all defaults ...
|
||||
UrgencyDue: 12.0,
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
func LoadConfig() (*Config, error) {
|
||||
if globalConfig != nil {
|
||||
return globalConfig, nil
|
||||
}
|
||||
|
||||
cfg, err := loadFromFile()
|
||||
if err != nil {
|
||||
// File not available — use defaults (common in server mode)
|
||||
cfg = DefaultConfig()
|
||||
}
|
||||
|
||||
globalConfig = cfg
|
||||
return cfg, nil
|
||||
}
|
||||
```
|
||||
|
||||
**Pros**: Smallest change, fixes the panic, no behavior change for CLI users.
|
||||
**Cons**: Doesn't address the architectural confusion. Error-swallowing callers
|
||||
remain. Write-on-read side effect remains for CLI. Three-source config stays.
|
||||
|
||||
---
|
||||
|
||||
### Option B: Separate load paths for CLI and server
|
||||
|
||||
**Change**: Split `LoadConfig()` into two functions:
|
||||
- `LoadConfigFromFile()` — current behavior for CLI (read/write YAML)
|
||||
- `LoadConfigFromDefaults()` — returns `DefaultConfig()` for server
|
||||
|
||||
The server startup code calls `LoadConfigFromDefaults()` eagerly during init,
|
||||
populating the singleton before any lazy `GetConfig()` call.
|
||||
|
||||
```go
|
||||
// cmd/server.go — in serverStartCmd.Run, before InitDB()
|
||||
engine.LoadConfigFromDefaults()
|
||||
```
|
||||
|
||||
**Pros**: Fixes the panic. Server path never touches the filesystem for config.
|
||||
Explicit about which mode is running. CLI behavior unchanged.
|
||||
**Cons**: Two code paths to maintain. Server can't be customized without code
|
||||
changes (urgency tuning, etc.).
|
||||
|
||||
---
|
||||
|
||||
### Option C: Read-only load with env-var overrides (recommended)
|
||||
|
||||
**Change**: Restructure `LoadConfig()` to:
|
||||
1. Start with hardcoded defaults (always succeeds)
|
||||
2. Layer YAML file on top **if it exists** (read-only, never create)
|
||||
3. Layer environment variables on top (via Viper `AutomaticEnv`)
|
||||
4. Never write to the filesystem as a side effect of loading
|
||||
|
||||
```go
|
||||
func LoadConfig() (*Config, error) {
|
||||
if globalConfig != nil {
|
||||
return globalConfig, nil
|
||||
}
|
||||
|
||||
v := viper.New()
|
||||
v.SetConfigType("yaml")
|
||||
|
||||
// 1. Hardcoded defaults (always present)
|
||||
setDefaults(v)
|
||||
|
||||
// 2. YAML file overlay (optional, read-only)
|
||||
if configPath, err := GetConfigPath(); err == nil {
|
||||
v.SetConfigFile(configPath)
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||
// File exists but is malformed — report, don't clobber
|
||||
if _, statErr := os.Stat(configPath); statErr == nil {
|
||||
return nil, fmt.Errorf("config file %s is invalid: %w", configPath, err)
|
||||
}
|
||||
// File doesn't exist — that's fine, use defaults
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Environment variable overlay
|
||||
v.SetEnvPrefix("OPAL")
|
||||
v.AutomaticEnv()
|
||||
|
||||
cfg := &Config{}
|
||||
if err := v.Unmarshal(cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
globalConfig = cfg
|
||||
return cfg, nil
|
||||
}
|
||||
```
|
||||
|
||||
File creation moves to an explicit `InitConfig()` function, called only during
|
||||
`opal setup` and CLI first-run:
|
||||
|
||||
```go
|
||||
func InitConfig() error {
|
||||
configDir, err := GetConfigDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create config directory: %w", err)
|
||||
}
|
||||
return SaveConfig(DefaultConfig())
|
||||
}
|
||||
```
|
||||
|
||||
Environment variable mapping:
|
||||
|
||||
| YAML key | Env var |
|
||||
|----------|---------|
|
||||
| `urgency_due_coefficient` | `OPAL_URGENCY_DUE_COEFFICIENT` |
|
||||
| `default_filter` | `OPAL_DEFAULT_FILTER` |
|
||||
| `week_start_day` | `OPAL_WEEK_START_DAY` |
|
||||
| ... | `OPAL_<UPPER_SNAKE>` |
|
||||
|
||||
**Pros**:
|
||||
- Fixes the panic — loading never fails fatally (missing file = defaults)
|
||||
- Malformed YAML is reported, not silently clobbered
|
||||
- Server can tune urgency via `opal.env` without deploying `opal.yml`
|
||||
- 12-factor compatible (env vars override everything)
|
||||
- CLI behavior unchanged (reads existing `opal.yml` + env overrides)
|
||||
- Single code path for both CLI and server
|
||||
- No filesystem write side effects during load
|
||||
|
||||
**Cons**:
|
||||
- `SaveConfig()` still needs the manual field-sync (separate improvement)
|
||||
- Slightly more Viper configuration
|
||||
|
||||
---
|
||||
|
||||
### Option D: Full restructure with typed config sections
|
||||
|
||||
**Change**: Split `Config` into domain-specific sub-configs and unify the
|
||||
loading mechanism for all config (merge `auth.Config` into the same system).
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
Display DisplayConfig `mapstructure:"display"`
|
||||
Urgency UrgencyConfig `mapstructure:"urgency"`
|
||||
Sync SyncConfig `mapstructure:"sync"`
|
||||
Server ServerConfig `mapstructure:"server"` // absorbs auth.Config
|
||||
}
|
||||
```
|
||||
|
||||
**Pros**: Clean separation. Single config system. Extensible.
|
||||
**Cons**: Large refactor. Breaks existing `opal.yml` format. `auth.Config`
|
||||
works fine as-is for its purpose. Over-engineered for the current problem.
|
||||
|
||||
---
|
||||
|
||||
## 5. Recommendation
|
||||
|
||||
**Option C** — read-only load with env-var overrides.
|
||||
|
||||
It fixes the immediate panic, eliminates the write-on-read footgun, enables
|
||||
server customization via environment variables, and does it with a focused
|
||||
change to one function. It doesn't over-engineer or restructure things that
|
||||
work.
|
||||
|
||||
### Implementation plan
|
||||
|
||||
#### Step 1: Extract `DefaultConfig()` constructor
|
||||
|
||||
Create a `DefaultConfig()` function that returns a `*Config` with all defaults
|
||||
populated. This is the single source of truth for default values — used by
|
||||
`LoadConfig()`, `InitConfig()`, and `SaveConfig()`.
|
||||
|
||||
**Files**: `internal/engine/config.go`
|
||||
|
||||
#### Step 2: Rewrite `LoadConfig()` to be read-only
|
||||
|
||||
Remove directory creation and file writing. Layer: defaults → YAML (if
|
||||
exists) → env vars. Return `DefaultConfig()` on missing file instead of nil.
|
||||
Return error only on malformed YAML (file exists but can't be parsed).
|
||||
|
||||
**Files**: `internal/engine/config.go`
|
||||
|
||||
#### Step 3: Extract `InitConfig()` for file creation
|
||||
|
||||
Move the "create config directory + write defaults" logic into `InitConfig()`.
|
||||
Call it from `initializeApp()` (CLI first-run path) and `opal setup`.
|
||||
|
||||
**Files**: `internal/engine/config.go`, `cmd/root.go`, `cmd/setup.go`
|
||||
|
||||
#### Step 4: Fix error-swallowing callers
|
||||
|
||||
Two options, from least to most invasive:
|
||||
|
||||
**4a (recommended)**: Make `GetConfig()` never return nil. Since `LoadConfig()`
|
||||
now always returns a valid `*Config` (defaults on failure), `GetConfig()` also
|
||||
always returns non-nil. The `cfg, _ := GetConfig()` pattern becomes safe. The
|
||||
error return is kept for callers that want to distinguish "loaded from file" vs
|
||||
"using defaults" but nil is never returned.
|
||||
|
||||
**4b (defensive)**: Also make `BuildUrgencyCoefficients()` accept nil and return
|
||||
default coefficients. Belt-and-suspenders.
|
||||
|
||||
**Files**: `internal/engine/config.go`, `internal/engine/urgency.go`
|
||||
|
||||
#### Step 5: Enable env-var overrides via Viper
|
||||
|
||||
Add `v.SetEnvPrefix("OPAL")` and `v.AutomaticEnv()` so that any config key
|
||||
can be overridden by `OPAL_<KEY>`. Update `opal.env` in deployment docs to
|
||||
show urgency tuning examples.
|
||||
|
||||
**Files**: `internal/engine/config.go`, `docs/deployment.md`
|
||||
|
||||
#### Step 6: Eliminate `SaveConfig()` field duplication
|
||||
|
||||
Replace the manual `v.Set()` calls with Viper's `mapstructure` round-trip or
|
||||
a reflect-based helper so new fields are automatically included.
|
||||
|
||||
```go
|
||||
func SaveConfig(cfg *Config) error {
|
||||
v := viper.New()
|
||||
v.SetConfigFile(configPath)
|
||||
v.SetConfigType("yaml")
|
||||
|
||||
// Use mapstructure tags to populate viper from struct
|
||||
data, _ := mapstructure.Decode(cfg) // or manual marshal
|
||||
for k, val := range data {
|
||||
v.Set(k, val)
|
||||
}
|
||||
return v.WriteConfig()
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, marshal to YAML directly with `yaml.Marshal` and write the
|
||||
bytes, bypassing Viper for the write path entirely.
|
||||
|
||||
**Files**: `internal/engine/config.go`
|
||||
|
||||
### Deployment changes
|
||||
|
||||
After this redesign, the Ansible role needs **no changes** — `opal.env` is
|
||||
sufficient. To customize urgency coefficients on the server, add lines to
|
||||
`opal.env`:
|
||||
|
||||
```bash
|
||||
# Server-side urgency tuning (optional)
|
||||
OPAL_URGENCY_DUE_COEFFICIENT=12.0
|
||||
OPAL_URGENCY_AGE_MAX=180
|
||||
```
|
||||
|
||||
No `opal.yml` deployment needed. The server runs on defaults + env overrides.
|
||||
|
||||
---
|
||||
|
||||
## 6. Technical Decisions
|
||||
|
||||
### ADR-1: Config loading must not write to the filesystem
|
||||
|
||||
- **Context**: `LoadConfig()` creates directories and writes `opal.yml` as a
|
||||
side effect. This fails on read-only filesystems (server) and is surprising
|
||||
behavior for a "load" function.
|
||||
- **Decision**: `LoadConfig()` becomes pure read. File creation moves to
|
||||
`InitConfig()`, called explicitly during setup/first-run.
|
||||
- **Alternatives**: Deploy `opal.yml` via Ansible; make `/etc/opal` writable.
|
||||
- **Consequences**: CLI first-run code must call `InitConfig()` explicitly.
|
||||
`IsFirstRun()` check remains in `initializeApp()`.
|
||||
|
||||
### ADR-2: Missing config file returns defaults, not an error
|
||||
|
||||
- **Context**: On the server, `opal.yml` doesn't exist and doesn't need to.
|
||||
The current code treats this as a fatal error.
|
||||
- **Decision**: Missing file = use defaults silently. Malformed file = return
|
||||
error (don't clobber). `GetConfig()` never returns nil.
|
||||
- **Alternatives**: Require `opal.yml` everywhere; use separate load functions
|
||||
per mode.
|
||||
- **Consequences**: Callers that discard errors (`cfg, _ := GetConfig()`) are
|
||||
now safe. The error return still exists for callers that need to distinguish
|
||||
"file loaded" from "using defaults".
|
||||
|
||||
### ADR-3: Environment variables can override any config key
|
||||
|
||||
- **Context**: The server is configured entirely via env vars (`opal.env` +
|
||||
systemd). Currently urgency coefficients can only be set via `opal.yml`.
|
||||
- **Decision**: Enable Viper's `AutomaticEnv()` with prefix `OPAL_`. Any YAML
|
||||
key `foo_bar` can be overridden by `OPAL_FOO_BAR`.
|
||||
- **Alternatives**: Add a server-specific config file; keep config values
|
||||
hardcoded for server.
|
||||
- **Consequences**: Layered config: defaults < YAML file < env vars. Aligns
|
||||
with 12-factor app principles. Deployment can tune behavior without deploying
|
||||
additional files.
|
||||
|
||||
### ADR-4: Do not merge auth.Config into engine.Config
|
||||
|
||||
- **Context**: `auth.Config` and `engine.Config` are completely independent
|
||||
systems. Merging them would create a single unified config.
|
||||
- **Decision**: Keep them separate. `auth.Config` loads from env vars, works
|
||||
correctly, and is server-only. No reason to change it.
|
||||
- **Alternatives**: Unified config struct with sections.
|
||||
- **Consequences**: Two config types remain. This is acceptable because they
|
||||
serve different purposes, have different lifecycles, and the current
|
||||
`auth.Config` has no bugs.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,533 @@
|
||||
# Web CLI Parity — Requirements Spec
|
||||
|
||||
**Status:** Draft
|
||||
**Last updated:** 2026-02-19
|
||||
**Related:** [`cli-ux-improvements.md`](cli-ux-improvements.md) — CLI UX
|
||||
improvements being developed in parallel. Features marked with **(CLI dep)**
|
||||
depend on or benefit from CLI-side work landing first.
|
||||
|
||||
This document covers the features the CLI exposes that the web frontend does
|
||||
not. Each section maps a CLI capability to a proposed web feature, with
|
||||
acceptance criteria and priority.
|
||||
|
||||
---
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The opal CLI is the primary interface and offers rich task management: in-place
|
||||
editing, start/stop timers, detailed task info, project/tag browsing, bulk
|
||||
operations, and full sync controls. The web frontend currently only supports
|
||||
create, complete, delete, and report-based listing. Users who switch between
|
||||
CLI and web hit a wall — most tasks that go beyond "add" and "done" require
|
||||
falling back to the terminal.
|
||||
|
||||
---
|
||||
|
||||
## Tier 1 — Core Gaps (the web feels broken without these)
|
||||
|
||||
### 1.1 Task Detail View (`info` equivalent)
|
||||
|
||||
**CLI:** `opal info 2` shows every field on a task — UUID, status, description,
|
||||
urgency, priority, project, all timestamps (created, modified, started, ended,
|
||||
due, scheduled, wait, until), recurrence pattern, parent UUID, and tags.
|
||||
|
||||
**Web gap:** Tapping a task does nothing. There is no way to see a task's full
|
||||
state.
|
||||
|
||||
**User story:** As a user, I want to tap a task to see all of its fields so
|
||||
that I can understand its full context without switching to the CLI.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- Given a task in the list, when I tap it, then a detail view opens showing
|
||||
every non-null field on the task
|
||||
- Given a task with a recurrence pattern, when I view its detail, then the
|
||||
recurrence interval and parent template link are visible
|
||||
- Given a task with scheduled/wait/until dates, when I view its detail, then
|
||||
those dates are displayed with labels
|
||||
- Given a task detail view, when I tap outside or press a close/back control,
|
||||
then the detail view closes and the list is restored
|
||||
|
||||
**Interaction:** Bottom sheet — slides up from the bottom of the screen.
|
||||
Natural on mobile, avoids a route change, and leaves the task list partially
|
||||
visible behind it. Light-dismiss (tap scrim to close).
|
||||
|
||||
**Priority:** MUST
|
||||
|
||||
---
|
||||
|
||||
### 1.2 Task Editing (`modify` / `edit` equivalent)
|
||||
|
||||
**CLI:** `opal 2 modify priority:H due:friday` changes attributes inline.
|
||||
`opal 2 edit` opens `$EDITOR` with all fields in a structured format.
|
||||
|
||||
**Web gap:** There is no way to edit a task after creation. Users must delete
|
||||
and recreate to fix a typo or change a due date.
|
||||
|
||||
**User story:** As a user, I want to edit any field on an existing task so that
|
||||
I can adjust it as my plans change.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- Given a task detail view, when I tap an editable field, then I can modify its
|
||||
value
|
||||
- Given I change a field and confirm, when the update succeeds, then the task
|
||||
list reflects the change
|
||||
- Given I change a field and confirm, when the update fails, then the error is
|
||||
shown and the original value is preserved
|
||||
- Editable fields: description, project, priority, due, scheduled, wait, until,
|
||||
tags, recurrence (on templates)
|
||||
- Read-only fields (displayed but not editable): UUID, created, modified, end,
|
||||
parent UUID, status
|
||||
|
||||
**Implementation notes:**
|
||||
- Uses `PUT /tasks/:uuid` — already exists in the API
|
||||
- For tags, uses `POST /tasks/:uuid/tags` and
|
||||
`DELETE /tasks/:uuid/tags/:tag` — already exist
|
||||
- Field editing could be inline (tap field to edit) or form-based (edit mode
|
||||
that makes all fields editable). Inline feels lighter for single-field tweaks.
|
||||
|
||||
**Priority:** MUST
|
||||
|
||||
---
|
||||
|
||||
### 1.3 Start / Stop Timer (`start` / `stop` equivalent)
|
||||
|
||||
**CLI:** `opal 1 start` marks a task as actively being worked on (sets `start`
|
||||
timestamp). `opal 1 stop` clears it. The `active` report lists started tasks.
|
||||
|
||||
**Web gap:** The API endpoints (`POST /tasks/:uuid/start`,
|
||||
`POST /tasks/:uuid/stop`) are wired in `endpoints.js` but there are no UI
|
||||
controls. The `active` report works in the report picker but users can't
|
||||
actually start tasks.
|
||||
|
||||
**User story:** As a user, I want to mark a task as "in progress" so that I can
|
||||
track what I'm actively working on and see it in the Active report.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- Given a pending task, when I tap a start control, then the task's start time
|
||||
is set and it appears in the Active report
|
||||
- Given a started task, when I tap a stop control, then the start time is
|
||||
cleared
|
||||
- Given a started task, when I view it in the list, then there is a visual
|
||||
indicator that it is active (distinguishable from non-started tasks)
|
||||
- The start/stop action should be accessible from both the task row and the
|
||||
detail view
|
||||
|
||||
**Interaction:** Swipe left to toggle start/stop — mirrors swipe right for
|
||||
complete. Also accessible from the detail view (1.1). No visible button on the
|
||||
task row.
|
||||
|
||||
**Priority:** MUST
|
||||
|
||||
---
|
||||
|
||||
### 1.4 Uncomplete / Revert (`modify status:pending` equivalent)
|
||||
|
||||
**(CLI dep)** — CLI is getting a dedicated `uncomplete` command and a generic
|
||||
`undo` (see [IMP-1](cli-ux-improvements.md#imp-1-undo--uncomplete)). The web
|
||||
feature should align with however the CLI exposes this so the mental model is
|
||||
consistent.
|
||||
|
||||
**CLI:** `opal <id> modify status:pending` reverts a completed task. IMP-1
|
||||
proposes `opal <id> uncomplete` and `opal undo` as dedicated commands.
|
||||
|
||||
**Web gap:** Once a task is completed it disappears from the pending list. The
|
||||
only recovery is the `completed` report + CLI. This is also called out in
|
||||
`BUGS.md` as a missing feature.
|
||||
|
||||
**User story:** As a user, I want to undo an accidental completion so that the
|
||||
task reappears in my pending list.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- Given a completed task (visible in the Completed report), when I tap an
|
||||
uncomplete action, then the task's status reverts to pending and it
|
||||
reappears in the pending list
|
||||
- Given I just completed a task, when I tap undo within a brief window (e.g.
|
||||
toast with undo button), then the task is reverted without navigating to the
|
||||
Completed report
|
||||
|
||||
**Implementation notes:**
|
||||
- Uses `PUT /tasks/:uuid` with `{"status": "pending"}` — works today
|
||||
- The undo toast after completion is a UX nicety but not strictly required for
|
||||
parity
|
||||
- If CLI IMP-1 lands an undo log table, the web could use the same mechanism
|
||||
for a more robust undo (revert any action, not just completion)
|
||||
|
||||
**Priority:** MUST
|
||||
|
||||
---
|
||||
|
||||
### 1.5 Delete Confirmation
|
||||
|
||||
**(CLI dep)** — CLI is improving confirmation prompts to show matched tasks
|
||||
before confirming (see [IMP-3](cli-ux-improvements.md#imp-3-show-matched-tasks-in-confirmations)).
|
||||
The web confirmation should follow the same pattern.
|
||||
|
||||
**CLI:** `opal delete` always prompts `Proceed? (y/N)` before deleting. IMP-3
|
||||
proposes showing the affected task(s) in the confirmation.
|
||||
|
||||
**Web gap:** Delete is instant with no confirmation. There is no undo.
|
||||
|
||||
**User story:** As a user, I want a confirmation before deleting a task so that
|
||||
I don't lose work by accident.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- Given I trigger a delete action, when the confirmation appears, then I must
|
||||
explicitly confirm before the delete proceeds
|
||||
- Given I dismiss the confirmation, then the task is not deleted
|
||||
- The confirmation should show the task description (matching CLI IMP-3's
|
||||
approach of showing what will be affected)
|
||||
|
||||
**Priority:** MUST
|
||||
|
||||
---
|
||||
|
||||
## Tier 2 — CLI Parity (brings the web up to feature-complete)
|
||||
|
||||
### ~~2.1 Projects View~~ — DEFERRED
|
||||
|
||||
### ~~2.2 Tags View~~ — DEFERRED
|
||||
|
||||
Projects and tags browsing is deferred. The existing filter syntax
|
||||
(`project:foo`, `+tag`) covers this adequately for now. When filter
|
||||
autocomplete is added later, discoverability will improve without needing
|
||||
dedicated views.
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Display All Date Fields on Task Items
|
||||
|
||||
**CLI:** `opal info` and the table display show scheduled, wait, until, and
|
||||
start dates when present. The `waiting` report makes sense because you can see
|
||||
*when* the wait expires.
|
||||
|
||||
**Web gap:** Only the due date is shown on task rows. Scheduled, wait, until,
|
||||
and start dates are invisible. Users can set them via CLI syntax in the input
|
||||
bar but can't see them afterward.
|
||||
|
||||
**User story:** As a user, I want to see all relevant dates on a task so that I
|
||||
understand when it's scheduled, when it becomes visible, and when it expires.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- Given a task with a `scheduled` date, then a "Scheduled: <date>" indicator
|
||||
appears on the task row
|
||||
- Given a task with a `wait` date, then a "Wait: <date>" indicator appears
|
||||
- Given a task with an `until` date, then an "Until: <date>" indicator appears
|
||||
- Given a task with a `start` time set, then an "Active since <time>" or
|
||||
similar indicator appears
|
||||
- Date fields that are null are not shown (no empty labels)
|
||||
- The detail view (1.1) shows all dates; the task row shows them in a
|
||||
compact/abbreviated form
|
||||
|
||||
**Priority:** SHOULD
|
||||
|
||||
---
|
||||
|
||||
### 2.4 Recurrence Display and Management
|
||||
|
||||
**(CLI dep)** — CLI is improving recurring task feedback on completion (see
|
||||
[IMP-7](cli-ux-improvements.md#imp-7-recurring-task-feedback)). The web should
|
||||
show equivalent feedback when completing a recurring task.
|
||||
|
||||
**CLI:** `opal add "standup" due:mon recur:1w` creates a recurring template +
|
||||
first instance. `opal template` and `opal recurring` list them. Completing an
|
||||
instance spawns the next one. `opal edit <id>` on an instance can update the
|
||||
template's recurrence pattern.
|
||||
|
||||
**Web gap:** Recurring tasks can be created via CLI syntax in the input bar, and
|
||||
the `recurring`/`template` reports work, but:
|
||||
- There is no visual indicator that a task is a recurring instance
|
||||
- There is no way to see or navigate to the parent template
|
||||
- There is no way to edit the recurrence pattern
|
||||
- Completing a recurring task gives no feedback about the next instance
|
||||
|
||||
**User story:** As a user, I want to see which tasks are recurring, view their
|
||||
schedule, and manage the recurrence pattern so that I can adjust repeating
|
||||
commitments.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- Given a recurring instance, when I view it in the list, then a recurrence
|
||||
icon or badge is visible
|
||||
- Given a recurring instance, when I open its detail view, then the recurrence
|
||||
interval and parent template are shown
|
||||
- Given a recurring template, when I open its detail view, then I can edit the
|
||||
recurrence interval
|
||||
- Given I complete a recurring instance, then the next instance is
|
||||
automatically created (this already works server-side; just verify the list
|
||||
refreshes to show it)
|
||||
- Given I complete a recurring instance, then a toast or inline message shows
|
||||
the next instance's due date (mirrors CLI IMP-7)
|
||||
|
||||
**Priority:** SHOULD
|
||||
|
||||
---
|
||||
|
||||
### ~~2.5 Sync Controls~~ — DEFERRED
|
||||
|
||||
Sync is deprioritized. The web has no client-side task parsing, so offline
|
||||
functionality is non-functioning. The web fetches directly from the server on
|
||||
every action — sync is only relevant if we move to an offline-first model,
|
||||
which is not planned.
|
||||
|
||||
---
|
||||
|
||||
### 2.6 Bulk Operations (`opal 1 2 3 modify ...` / `opal +tag done`)
|
||||
|
||||
**CLI:** Filters and numeric IDs can target multiple tasks.
|
||||
`opal +urgent done` completes all tasks tagged `urgent`.
|
||||
`opal 1 2 3 modify project:sprint-2` moves three tasks at once.
|
||||
The CLI prompts for confirmation when multiple tasks are affected.
|
||||
|
||||
**Web gap:** All actions are single-task only. No multi-select, no batch
|
||||
complete, no batch modify.
|
||||
|
||||
**User story:** As a user, I want to select multiple tasks and perform an
|
||||
action on all of them so that I can manage tasks efficiently.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- Given I enter selection mode (e.g., long-press a task), then I can tap
|
||||
additional tasks to add them to the selection
|
||||
- Given I have tasks selected, then I can batch-complete, batch-delete, or
|
||||
batch-modify (at minimum: change project, add/remove tag, change priority)
|
||||
- Given a batch action targets 2+ tasks, then a confirmation prompt appears
|
||||
before executing
|
||||
- Given I tap outside the selection or press a cancel control, then selection
|
||||
mode is exited
|
||||
|
||||
**Priority:** SHOULD
|
||||
|
||||
---
|
||||
|
||||
## Tier 3 — Power User & Polish
|
||||
|
||||
### 3.1 Keyboard Shortcuts
|
||||
|
||||
**CLI:** The CLI is entirely keyboard-driven by nature.
|
||||
|
||||
**Web gap:** No keyboard shortcuts exist. Desktop users must use the mouse for
|
||||
everything.
|
||||
|
||||
**User story:** As a desktop user, I want keyboard shortcuts so that I can
|
||||
manage tasks without reaching for the mouse.
|
||||
|
||||
**Suggested bindings:**
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `n` | Focus the input bar (new task) |
|
||||
| `j` / `k` | Move selection down / up in task list |
|
||||
| `x` | Complete selected task |
|
||||
| `e` | Open detail/edit for selected task |
|
||||
| `d` | Delete selected task (with confirmation) |
|
||||
| `s` | Start/stop selected task |
|
||||
| `Escape` | Close detail view / deselect / blur input |
|
||||
| `/` | Open filter modal |
|
||||
|
||||
**Acceptance criteria:**
|
||||
- Given I press `n` when not focused on an input, then the input bar is focused
|
||||
- Given I press `j`/`k`, then the visual selection moves through the task list
|
||||
- Given I press `x` with a task selected, then that task is completed
|
||||
- Shortcuts are disabled when an input or textarea is focused (except Escape)
|
||||
|
||||
**Priority:** COULD
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Description Search
|
||||
|
||||
**CLI:** Filtering by tags, project, priority, and status covers structured
|
||||
attributes, but there's no full-text description search in the CLI either.
|
||||
|
||||
**Web opportunity:** The web could add a search capability that the CLI lacks.
|
||||
|
||||
**User story:** As a user, I want to search tasks by description text so that I
|
||||
can find a specific task without remembering its tags or project.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- Given I type in a search field, then the task list filters to tasks whose
|
||||
description contains the search text (case-insensitive)
|
||||
- Search can be client-side (filter the loaded report) or server-side (new
|
||||
query param)
|
||||
|
||||
**Interaction:** Extends the existing filter syntax. Bare text in the filter
|
||||
input is interpreted as a description search. The filter modal becomes
|
||||
filter/search — no separate search bar.
|
||||
|
||||
**Priority:** COULD
|
||||
|
||||
---
|
||||
|
||||
### ~~3.3 Display IDs~~ — DROPPED
|
||||
|
||||
Not needed. The web uses touch/tap interaction, not keyboard-driven ID
|
||||
selection. Display IDs are a CLI affordance that doesn't translate to the web
|
||||
interaction model.
|
||||
|
||||
---
|
||||
|
||||
### ~~3.4 API Key Management~~ — ON HOLD
|
||||
|
||||
Blocked on user management investigation. Open questions: Are API keys
|
||||
user-scoped? Should we move to per-user databases? This feature should wait
|
||||
until the auth/user model is settled.
|
||||
|
||||
---
|
||||
|
||||
### 3.5 Task Annotations (`annotate` equivalent)
|
||||
|
||||
**(CLI dep)** — The CLI is adding `opal <id> annotate "<text>"` (see
|
||||
[IMP-11](cli-ux-improvements.md#imp-11-task-annotations)). This requires a
|
||||
backend schema change (annotations storage). Once that lands, the web should
|
||||
surface annotations.
|
||||
|
||||
**CLI (proposed):** `opal 3 annotate "Traced to token expiry"` adds a
|
||||
timestamped note. `opal 3 info` shows annotations. IMP-11 notes potential
|
||||
integration with jade-depo (the gems note management system).
|
||||
|
||||
**Web gap:** No concept of annotations exists. Tasks have only a description
|
||||
field for text.
|
||||
|
||||
**User story:** As a user, I want to add and view notes on a task so that I can
|
||||
record progress and context over time.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- Given a task detail view (1.1), then any existing annotations are shown as a
|
||||
timestamped list below the task fields
|
||||
- Given a task detail view, when I tap an "Add note" control, then I can enter
|
||||
annotation text and it is saved with a timestamp
|
||||
- Annotations are ordered newest-first or oldest-first (decide which)
|
||||
- Annotations are read-only after creation (no inline editing — `denotate`
|
||||
removes the latest)
|
||||
|
||||
**Decisions:**
|
||||
- Annotations are visible only in the detail view (1.1), not on the task row.
|
||||
- jade-depo integration is not relevant to the web UI at this time.
|
||||
|
||||
**Priority:** COULD (blocked on CLI IMP-11 landing the backend schema)
|
||||
|
||||
---
|
||||
|
||||
### 3.6 Task History (`log` equivalent)
|
||||
|
||||
**(CLI dep)** — The CLI is adding `opal <id> log` (see
|
||||
[IMP-12](cli-ux-improvements.md#imp-12-task-history)). This reads the existing
|
||||
`change_log` table which already exists for sync.
|
||||
|
||||
**CLI (proposed):** `opal 3 log` shows timestamped change history (created,
|
||||
modified, completed, etc.).
|
||||
|
||||
**Web gap:** No way to see what happened to a task over time.
|
||||
|
||||
**User story:** As a user, I want to see a task's change history so that I can
|
||||
understand when and how it was modified.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- Given a task detail view (1.1), then a "History" section or tab shows the
|
||||
change log for that task
|
||||
- Each entry shows: timestamp, change type, and what changed (e.g.,
|
||||
"priority: default -> high")
|
||||
- History is read-only
|
||||
|
||||
**Implementation notes:**
|
||||
- The `change_log` table already exists for sync. This likely needs a new API
|
||||
endpoint (`GET /tasks/:uuid/log` or similar) to expose it.
|
||||
- Alternatively, the change log could be included in the task detail response.
|
||||
|
||||
**Priority:** COULD (blocked on CLI IMP-12 / API endpoint)
|
||||
|
||||
---
|
||||
|
||||
### 3.7 Configurable Default Report
|
||||
|
||||
**CLI:** `default_report` in `opal.yml` controls what `opal` with no arguments
|
||||
shows (default: `list`). `default_filter` controls the base filter.
|
||||
|
||||
**Web gap:** The web always starts on the `list` report. There is no user
|
||||
preference for the landing view.
|
||||
|
||||
**User story:** As a user, I want to choose which report I see when I open the
|
||||
app so that my most-used view loads first.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- Given I choose a default report in settings, then the app opens to that
|
||||
report on next launch
|
||||
- The preference is stored in localStorage (no backend change needed)
|
||||
|
||||
**Priority:** COULD
|
||||
|
||||
---
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
| # | Requirement | Priority |
|
||||
|---|-------------|----------|
|
||||
| NFR-1 | All new features must work on Android Chrome and desktop Chrome/Firefox (per architecture doc) | MUST |
|
||||
| NFR-2 | Task detail and edit interactions must be touch-friendly (44px minimum tap targets) | MUST |
|
||||
| NFR-3 | Editing a task must be optimistic — UI updates immediately, rolls back on failure | SHOULD |
|
||||
| NFR-4 | Keyboard shortcuts must not conflict with browser defaults (Ctrl+T, etc.) | MUST |
|
||||
| NFR-5 | New features must work with all three themes (Obsidian, Paper, Midnight) | MUST |
|
||||
| NFR-6 | No new API endpoints are required for Tier 1 — all endpoints already exist | N/A |
|
||||
|
||||
---
|
||||
|
||||
## Constraints & Assumptions
|
||||
|
||||
**Constraints:**
|
||||
- Single-screen architecture per the existing design doc — no new routes for
|
||||
projects/tags (use sheets/modals/filters instead)
|
||||
- Server-side parsing and sorting — the frontend stays a thin shell
|
||||
- SvelteKit + Vite stack, Svelte 5 runes
|
||||
|
||||
**Assumptions:**
|
||||
- The `PUT /tasks/:uuid` endpoint accepts partial updates (only fields present
|
||||
in the request body are changed)
|
||||
- The working set display IDs are not currently exposed via the API; Tier 3.3
|
||||
would require an API change
|
||||
- Sync transport (pull/push) works correctly; the gap is only in applying
|
||||
pulled changes to the store
|
||||
|
||||
---
|
||||
|
||||
## Cross-Reference: CLI UX Improvements
|
||||
|
||||
The following items from [`cli-ux-improvements.md`](cli-ux-improvements.md)
|
||||
directly affect web features in this spec:
|
||||
|
||||
| CLI IMP | CLI Feature | Web Impact |
|
||||
|---------|-------------|------------|
|
||||
| IMP-1 | Undo / uncomplete | Enables 1.4 (uncomplete). If undo log is stored in DB, web can use same mechanism. |
|
||||
| IMP-2 | Better `add` feedback | Web already shows the created task in-list. No direct web change, but if the API response for `POST /tasks/parse` is enriched (display ID, parsed modifiers), the web could show a richer confirmation toast. |
|
||||
| IMP-3 | Show matched tasks in confirmations | Pattern for 1.5 (delete confirmation) and 2.6 (bulk ops). |
|
||||
| IMP-5 | Handle colons in descriptions | Affects web input bar — same parsing runs server-side via `/tasks/parse`. No web change needed, but web benefits automatically. |
|
||||
| IMP-7 | Recurring task feedback | Directly feeds 2.4 (recurrence display). |
|
||||
| IMP-9 | Relative dates in CLI | Web already does this. No change needed. |
|
||||
| IMP-11 | Task annotations | Enables 3.5 (annotations on web). Blocked on backend schema. |
|
||||
| IMP-12 | Task history | Enables 3.6 (history on web). Blocked on API endpoint. |
|
||||
|
||||
Items from the CLI spec with **no web impact**: IMP-4 (delete ID resolution
|
||||
bug), IMP-6 (consistent error codes), IMP-8 (shell completions), IMP-10
|
||||
(dry-run flag), IMP-13 (version command).
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Collaboration / multi-user sharing
|
||||
- Notifications, reminders, or push alerts
|
||||
- Custom fields or metadata
|
||||
- Drag-to-reorder (ordering is report/urgency-driven)
|
||||
- Offline-first / IndexedDB task storage (tasks are server-side only)
|
||||
- iOS Safari support
|
||||
|
||||
---
|
||||
|
||||
## Open Questions Summary
|
||||
|
||||
| # | Question | Blocks | Status |
|
||||
|---|----------|--------|--------|
|
||||
| ~~Q1~~ | ~~Detail view format~~ | ~~1.1~~ | **Resolved** — bottom sheet |
|
||||
| ~~Q2~~ | ~~Start/stop control placement~~ | ~~1.3~~ | **Resolved** — swipe left |
|
||||
| ~~Q3~~ | ~~Projects/tags view format~~ | ~~2.1, 2.2~~ | **Resolved** — deferred, use filters |
|
||||
| ~~Q4~~ | ~~Description search format~~ | ~~3.2~~ | **Resolved** — extend filter syntax |
|
||||
| ~~Q5~~ | ~~Display IDs~~ | ~~3.3~~ | **Resolved** — dropped |
|
||||
| ~~Q6~~ | ~~Annotation visibility~~ | ~~3.5~~ | **Resolved** — detail view only |
|
||||
| ~~Q7~~ | ~~jade-depo integration~~ | ~~3.5~~ | **Resolved** — not for web |
|
||||
@@ -18,9 +18,10 @@ export const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8080';
|
||||
export async function apiRequest(endpoint, options = {}) {
|
||||
const auth = get(authStore);
|
||||
|
||||
/** @type {Record<string, string>} */
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
.../** @type {Record<string, string>} */ (options.headers)
|
||||
};
|
||||
|
||||
if (auth.accessToken) {
|
||||
|
||||
@@ -76,7 +76,7 @@ export const tasks = {
|
||||
|
||||
/**
|
||||
* @param {string} input - Raw opal CLI syntax
|
||||
* @returns {Promise<Task>}
|
||||
* @returns {Promise<{task?: Task} & Task>}
|
||||
*/
|
||||
async parse(input) {
|
||||
return apiRequest('/tasks/parse', {
|
||||
@@ -143,7 +143,7 @@ export const sync = {
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Task[]} tasks
|
||||
* @param {Partial<Task>[]} tasks
|
||||
* @param {string} clientId
|
||||
* @returns {Promise<{processed: number, conflicts: number}>}
|
||||
*/
|
||||
|
||||
@@ -56,7 +56,8 @@ function createSyncStore() {
|
||||
const state = loadSyncState();
|
||||
const queue = getQueue();
|
||||
|
||||
let result = {
|
||||
/** @type {SyncResult} */
|
||||
const result = {
|
||||
pulled: 0,
|
||||
pushed: 0,
|
||||
conflicts_resolved: 0,
|
||||
@@ -70,7 +71,7 @@ function createSyncStore() {
|
||||
await syncAPI.push(tasks, state.clientId);
|
||||
clearQueue();
|
||||
result.pushed = queue.length;
|
||||
} catch (error) {
|
||||
} catch (/** @type {any} */ error) {
|
||||
result.errors.push(`Failed to push queue: ${error.message}`);
|
||||
}
|
||||
}
|
||||
@@ -79,7 +80,7 @@ function createSyncStore() {
|
||||
const changes = await syncAPI.getChanges(state.lastSync, state.clientId);
|
||||
result.pulled = changes.length;
|
||||
// TODO: Apply changes to local state
|
||||
} catch (error) {
|
||||
} catch (/** @type {any} */ error) {
|
||||
result.errors.push(`Failed to pull changes: ${error.message}`);
|
||||
}
|
||||
|
||||
@@ -95,7 +96,7 @@ function createSyncStore() {
|
||||
}));
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
} catch (/** @type {any} */ error) {
|
||||
update(state => ({
|
||||
...state,
|
||||
status: 'error',
|
||||
|
||||
@@ -42,7 +42,7 @@ function createTasksStore() {
|
||||
async parseAndCreate(input) {
|
||||
try {
|
||||
const result = await tasksAPI.parse(input);
|
||||
const created = result.task ?? result;
|
||||
const created = /** @type {Task} */ (result.task ?? result);
|
||||
update(tasks => [created, ...tasks]);
|
||||
return created;
|
||||
} catch (error) {
|
||||
@@ -64,7 +64,7 @@ function createTasksStore() {
|
||||
|
||||
/**
|
||||
* Optimistic create — queues offline on failure.
|
||||
* @param {Partial<Task>} task
|
||||
* @param {Task} task
|
||||
*/
|
||||
async add(task) {
|
||||
try {
|
||||
@@ -172,6 +172,7 @@ export const completedTasks = derived(
|
||||
export const tasksByProject = derived(
|
||||
tasksStore,
|
||||
$tasks => {
|
||||
/** @type {Record<string, Task[]>} */
|
||||
const grouped = {};
|
||||
$tasks.forEach(task => {
|
||||
const project = task.project || 'No Project';
|
||||
|
||||
Reference in New Issue
Block a user