@andypai/agent-kanban 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -22
- package/package.json +4 -2
- package/src/__tests__/activity.test.ts +15 -9
- package/src/__tests__/api.test.ts +96 -0
- package/src/__tests__/board-utils.test.ts +100 -0
- package/src/__tests__/commands/board.test.ts +6 -13
- package/src/__tests__/conflict.test.ts +64 -0
- package/src/__tests__/index.test.ts +233 -56
- package/src/__tests__/jira-adf.test.ts +168 -0
- package/src/__tests__/jira-cache.test.ts +304 -0
- package/src/__tests__/jira-client.test.ts +169 -0
- package/src/__tests__/jira-provider-comment.test.ts +281 -0
- package/src/__tests__/jira-provider-mutations.test.ts +771 -0
- package/src/__tests__/jira-provider-read.test.ts +594 -0
- package/src/__tests__/jira-wiring.test.ts +187 -0
- package/src/__tests__/linear-cache-description-activity.test.ts +142 -0
- package/src/__tests__/linear-provider-comment.test.ts +243 -0
- package/src/__tests__/linear-provider-sync.test.ts +493 -0
- package/src/__tests__/local-provider-comment.test.ts +60 -0
- package/src/__tests__/mcp-core.test.ts +164 -0
- package/src/__tests__/mcp-server.test.ts +252 -0
- package/src/__tests__/server.test.ts +298 -0
- package/src/__tests__/webhooks.test.ts +604 -0
- package/src/activity.ts +1 -11
- package/src/api.ts +154 -19
- package/src/commands/board.ts +1 -11
- package/src/commands/mcp.ts +87 -0
- package/src/db.ts +115 -3
- package/src/errors.ts +2 -0
- package/src/id.ts +1 -1
- package/src/index.ts +72 -18
- package/src/mcp/core.ts +193 -0
- package/src/mcp/errors.ts +109 -0
- package/src/mcp/index.ts +13 -0
- package/src/mcp/server.ts +512 -0
- package/src/mcp/types.ts +72 -0
- package/src/providers/capabilities.ts +15 -0
- package/src/providers/index.ts +31 -1
- package/src/providers/jira-adf.ts +275 -0
- package/src/providers/jira-cache.ts +625 -0
- package/src/providers/jira-client.ts +390 -0
- package/src/providers/jira.ts +778 -0
- package/src/providers/linear-cache.ts +249 -70
- package/src/providers/linear-client.ts +256 -13
- package/src/providers/linear.ts +337 -14
- package/src/providers/local.ts +68 -17
- package/src/providers/types.ts +18 -2
- package/src/server.ts +139 -11
- package/src/tunnel.ts +79 -0
- package/src/types.ts +18 -2
- package/src/webhooks.ts +36 -0
- package/ui/dist/assets/index-DBnoKL_k.css +1 -0
- package/ui/dist/assets/index-qNVJ6clH.js +40 -0
- package/ui/dist/index.html +8 -2
- package/src/__tests__/commands/task.test.ts +0 -144
- package/src/commands/task.ts +0 -117
- package/src/fixtures.ts +0 -128
- package/ui/dist/assets/index-DEnUD0fq.css +0 -1
- package/ui/dist/assets/index-DMRjw1nI.js +0 -40
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test, beforeEach } from 'bun:test'
|
|
2
|
-
import { Database } from 'bun:sqlite'
|
|
3
|
-
import { initSchema, seedDefaultColumns } from '../../db.ts'
|
|
4
|
-
import {
|
|
5
|
-
taskAdd,
|
|
6
|
-
taskList,
|
|
7
|
-
taskView,
|
|
8
|
-
taskUpdate,
|
|
9
|
-
taskDelete,
|
|
10
|
-
taskMove,
|
|
11
|
-
taskAssign,
|
|
12
|
-
taskPrioritize,
|
|
13
|
-
} from '../../commands/task.ts'
|
|
14
|
-
import { KanbanError } from '../../errors.ts'
|
|
15
|
-
import type { TaskWithColumn } from '../../types.ts'
|
|
16
|
-
|
|
17
|
-
let db: Database
|
|
18
|
-
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
db = new Database(':memory:')
|
|
21
|
-
db.run('PRAGMA foreign_keys = ON')
|
|
22
|
-
initSchema(db)
|
|
23
|
-
seedDefaultColumns(db)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
describe('taskAdd', () => {
|
|
27
|
-
test('adds a task with all options', () => {
|
|
28
|
-
const result = taskAdd(db, {
|
|
29
|
-
title: 'Build feature',
|
|
30
|
-
description: 'Do the thing',
|
|
31
|
-
column: 'recurring',
|
|
32
|
-
priority: 'high',
|
|
33
|
-
assignee: 'alice',
|
|
34
|
-
metadata: '{"sprint": 5}',
|
|
35
|
-
})
|
|
36
|
-
expect(result.ok).toBe(true)
|
|
37
|
-
if (result.ok) {
|
|
38
|
-
const task = result.data as TaskWithColumn
|
|
39
|
-
expect(task.title).toBe('Build feature')
|
|
40
|
-
expect(task.description).toBe('Do the thing')
|
|
41
|
-
expect(task.priority).toBe('high')
|
|
42
|
-
expect(task.assignee).toBe('alice')
|
|
43
|
-
expect(task.metadata).toBe('{"sprint": 5}')
|
|
44
|
-
}
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
test('throws without title', () => {
|
|
48
|
-
expect(() => taskAdd(db, {})).toThrow(KanbanError)
|
|
49
|
-
})
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
describe('taskList', () => {
|
|
53
|
-
test('lists all tasks', () => {
|
|
54
|
-
taskAdd(db, { title: 'A' })
|
|
55
|
-
taskAdd(db, { title: 'B' })
|
|
56
|
-
const result = taskList(db, {})
|
|
57
|
-
expect(result.ok).toBe(true)
|
|
58
|
-
if (result.ok) {
|
|
59
|
-
expect(result.data as TaskWithColumn[]).toHaveLength(2)
|
|
60
|
-
}
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
test('filters by column', () => {
|
|
64
|
-
taskAdd(db, { title: 'A', column: 'recurring' })
|
|
65
|
-
taskAdd(db, { title: 'B', column: 'done' })
|
|
66
|
-
const result = taskList(db, { column: 'recurring' })
|
|
67
|
-
if (result.ok) {
|
|
68
|
-
expect(result.data as TaskWithColumn[]).toHaveLength(1)
|
|
69
|
-
}
|
|
70
|
-
})
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
describe('taskView', () => {
|
|
74
|
-
test('returns task details', () => {
|
|
75
|
-
const addResult = taskAdd(db, { title: 'View me' })
|
|
76
|
-
if (!addResult.ok) throw new Error('unexpected')
|
|
77
|
-
const task = addResult.data as TaskWithColumn
|
|
78
|
-
const result = taskView(db, { id: task.id })
|
|
79
|
-
expect(result.ok).toBe(true)
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
test('throws for missing id', () => {
|
|
83
|
-
expect(() => taskView(db, {})).toThrow(KanbanError)
|
|
84
|
-
})
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
describe('taskUpdate', () => {
|
|
88
|
-
test('updates task fields', () => {
|
|
89
|
-
const addResult = taskAdd(db, { title: 'Original' })
|
|
90
|
-
if (!addResult.ok) throw new Error('unexpected')
|
|
91
|
-
const task = addResult.data as TaskWithColumn
|
|
92
|
-
const result = taskUpdate(db, { id: task.id, title: 'Modified' })
|
|
93
|
-
if (result.ok) {
|
|
94
|
-
expect((result.data as TaskWithColumn).title).toBe('Modified')
|
|
95
|
-
}
|
|
96
|
-
})
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
describe('taskDelete', () => {
|
|
100
|
-
test('deletes task', () => {
|
|
101
|
-
const addResult = taskAdd(db, { title: 'Delete me' })
|
|
102
|
-
if (!addResult.ok) throw new Error('unexpected')
|
|
103
|
-
const task = addResult.data as TaskWithColumn
|
|
104
|
-
const result = taskDelete(db, { id: task.id })
|
|
105
|
-
expect(result.ok).toBe(true)
|
|
106
|
-
expect(() => taskView(db, { id: task.id })).toThrow(KanbanError)
|
|
107
|
-
})
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
describe('taskMove', () => {
|
|
111
|
-
test('moves task to new column', () => {
|
|
112
|
-
const addResult = taskAdd(db, { title: 'Move me', column: 'recurring' })
|
|
113
|
-
if (!addResult.ok) throw new Error('unexpected')
|
|
114
|
-
const task = addResult.data as TaskWithColumn
|
|
115
|
-
const result = taskMove(db, { id: task.id, column: 'in-progress' })
|
|
116
|
-
if (result.ok) {
|
|
117
|
-
expect((result.data as TaskWithColumn).column_name).toBe('in-progress')
|
|
118
|
-
}
|
|
119
|
-
})
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
describe('taskAssign', () => {
|
|
123
|
-
test('assigns task', () => {
|
|
124
|
-
const addResult = taskAdd(db, { title: 'Assign me' })
|
|
125
|
-
if (!addResult.ok) throw new Error('unexpected')
|
|
126
|
-
const task = addResult.data as TaskWithColumn
|
|
127
|
-
const result = taskAssign(db, { id: task.id, assignee: 'bob' })
|
|
128
|
-
if (result.ok) {
|
|
129
|
-
expect((result.data as TaskWithColumn).assignee).toBe('bob')
|
|
130
|
-
}
|
|
131
|
-
})
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
describe('taskPrioritize', () => {
|
|
135
|
-
test('sets priority', () => {
|
|
136
|
-
const addResult = taskAdd(db, { title: 'Prioritize me' })
|
|
137
|
-
if (!addResult.ok) throw new Error('unexpected')
|
|
138
|
-
const task = addResult.data as TaskWithColumn
|
|
139
|
-
const result = taskPrioritize(db, { id: task.id, priority: 'urgent' })
|
|
140
|
-
if (result.ok) {
|
|
141
|
-
expect((result.data as TaskWithColumn).priority).toBe('urgent')
|
|
142
|
-
}
|
|
143
|
-
})
|
|
144
|
-
})
|
package/src/commands/task.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import type { Database } from 'bun:sqlite'
|
|
2
|
-
import { addTask, getTask, listTasks, updateTask, deleteTask, moveTask } from '../db.ts'
|
|
3
|
-
import { ErrorCode, KanbanError } from '../errors.ts'
|
|
4
|
-
import { success } from '../output.ts'
|
|
5
|
-
import type { CliOutput, Priority } from '../types.ts'
|
|
6
|
-
|
|
7
|
-
export function taskAdd(
|
|
8
|
-
db: Database,
|
|
9
|
-
args: {
|
|
10
|
-
title?: string
|
|
11
|
-
description?: string
|
|
12
|
-
column?: string
|
|
13
|
-
priority?: string
|
|
14
|
-
assignee?: string
|
|
15
|
-
project?: string
|
|
16
|
-
metadata?: string
|
|
17
|
-
},
|
|
18
|
-
): CliOutput {
|
|
19
|
-
if (!args.title) {
|
|
20
|
-
throw new KanbanError(ErrorCode.MISSING_ARGUMENT, 'Task title is required')
|
|
21
|
-
}
|
|
22
|
-
return success(
|
|
23
|
-
addTask(db, args.title, {
|
|
24
|
-
description: args.description,
|
|
25
|
-
column: args.column,
|
|
26
|
-
priority: args.priority as Priority | undefined,
|
|
27
|
-
assignee: args.assignee,
|
|
28
|
-
project: args.project,
|
|
29
|
-
metadata: args.metadata,
|
|
30
|
-
}),
|
|
31
|
-
)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function taskList(
|
|
35
|
-
db: Database,
|
|
36
|
-
opts: {
|
|
37
|
-
column?: string
|
|
38
|
-
priority?: string
|
|
39
|
-
assignee?: string
|
|
40
|
-
project?: string
|
|
41
|
-
limit?: string
|
|
42
|
-
sort?: string
|
|
43
|
-
},
|
|
44
|
-
): CliOutput {
|
|
45
|
-
return success(
|
|
46
|
-
listTasks(db, {
|
|
47
|
-
column: opts.column,
|
|
48
|
-
priority: opts.priority,
|
|
49
|
-
assignee: opts.assignee,
|
|
50
|
-
project: opts.project,
|
|
51
|
-
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
52
|
-
sort: opts.sort,
|
|
53
|
-
}),
|
|
54
|
-
)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function taskView(db: Database, args: { id?: string }): CliOutput {
|
|
58
|
-
if (!args.id) {
|
|
59
|
-
throw new KanbanError(ErrorCode.MISSING_ARGUMENT, 'Task ID is required')
|
|
60
|
-
}
|
|
61
|
-
return success(getTask(db, args.id))
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function taskUpdate(
|
|
65
|
-
db: Database,
|
|
66
|
-
args: {
|
|
67
|
-
id?: string
|
|
68
|
-
title?: string
|
|
69
|
-
description?: string
|
|
70
|
-
priority?: string
|
|
71
|
-
assignee?: string
|
|
72
|
-
project?: string
|
|
73
|
-
metadata?: string
|
|
74
|
-
},
|
|
75
|
-
): CliOutput {
|
|
76
|
-
if (!args.id) {
|
|
77
|
-
throw new KanbanError(ErrorCode.MISSING_ARGUMENT, 'Task ID is required')
|
|
78
|
-
}
|
|
79
|
-
return success(
|
|
80
|
-
updateTask(db, args.id, {
|
|
81
|
-
title: args.title,
|
|
82
|
-
description: args.description,
|
|
83
|
-
priority: args.priority as Priority | undefined,
|
|
84
|
-
assignee: args.assignee,
|
|
85
|
-
project: args.project,
|
|
86
|
-
metadata: args.metadata,
|
|
87
|
-
}),
|
|
88
|
-
)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function taskDelete(db: Database, args: { id?: string }): CliOutput {
|
|
92
|
-
if (!args.id) {
|
|
93
|
-
throw new KanbanError(ErrorCode.MISSING_ARGUMENT, 'Task ID is required')
|
|
94
|
-
}
|
|
95
|
-
return success(deleteTask(db, args.id))
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function taskMove(db: Database, args: { id?: string; column?: string }): CliOutput {
|
|
99
|
-
if (!args.id || !args.column) {
|
|
100
|
-
throw new KanbanError(ErrorCode.MISSING_ARGUMENT, 'Usage: kanban task move <id> <column>')
|
|
101
|
-
}
|
|
102
|
-
return success(moveTask(db, args.id, args.column))
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function taskAssign(db: Database, args: { id?: string; assignee?: string }): CliOutput {
|
|
106
|
-
if (!args.id || !args.assignee) {
|
|
107
|
-
throw new KanbanError(ErrorCode.MISSING_ARGUMENT, 'Usage: kanban task assign <id> <assignee>')
|
|
108
|
-
}
|
|
109
|
-
return success(updateTask(db, args.id, { assignee: args.assignee }))
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function taskPrioritize(db: Database, args: { id?: string; priority?: string }): CliOutput {
|
|
113
|
-
if (!args.id || !args.priority) {
|
|
114
|
-
throw new KanbanError(ErrorCode.MISSING_ARGUMENT, 'Usage: kanban task prioritize <id> <level>')
|
|
115
|
-
}
|
|
116
|
-
return success(updateTask(db, args.id, { priority: args.priority as Priority }))
|
|
117
|
-
}
|
package/src/fixtures.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import type { Database } from 'bun:sqlite'
|
|
2
|
-
import type { Priority } from './types.ts'
|
|
3
|
-
import { addTask, moveTask, updateTask, listTasks } from './db.ts'
|
|
4
|
-
|
|
5
|
-
interface FixtureTask {
|
|
6
|
-
title: string
|
|
7
|
-
description?: string
|
|
8
|
-
column: string
|
|
9
|
-
priority: Priority
|
|
10
|
-
assignee?: string
|
|
11
|
-
project?: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const FIXTURE_TASKS: FixtureTask[] = [
|
|
15
|
-
// recurring
|
|
16
|
-
{
|
|
17
|
-
title: 'Daily standup notes',
|
|
18
|
-
description: 'Capture blockers and progress each morning',
|
|
19
|
-
column: 'recurring',
|
|
20
|
-
priority: 'medium',
|
|
21
|
-
assignee: 'Alex',
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
title: 'Weekly metrics review',
|
|
25
|
-
description: 'Review board throughput and cycle time every Friday',
|
|
26
|
-
column: 'recurring',
|
|
27
|
-
priority: 'low',
|
|
28
|
-
assignee: 'BuildBot',
|
|
29
|
-
project: 'Platform',
|
|
30
|
-
},
|
|
31
|
-
|
|
32
|
-
// backlog
|
|
33
|
-
{
|
|
34
|
-
title: 'Add search functionality',
|
|
35
|
-
description: 'Full-text search across task titles and descriptions',
|
|
36
|
-
column: 'backlog',
|
|
37
|
-
priority: 'high',
|
|
38
|
-
assignee: 'Alex',
|
|
39
|
-
project: 'Platform',
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
title: 'Write API docs',
|
|
43
|
-
description: 'Document all CLI commands with examples',
|
|
44
|
-
column: 'backlog',
|
|
45
|
-
priority: 'medium',
|
|
46
|
-
assignee: 'BuildBot',
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
title: 'Refactor error handling',
|
|
50
|
-
description: 'Consolidate error codes and improve user-facing messages',
|
|
51
|
-
column: 'backlog',
|
|
52
|
-
priority: 'low',
|
|
53
|
-
project: 'Platform',
|
|
54
|
-
},
|
|
55
|
-
|
|
56
|
-
// in-progress
|
|
57
|
-
{
|
|
58
|
-
title: 'Implement board export',
|
|
59
|
-
description: 'Export board state to JSON and CSV formats',
|
|
60
|
-
column: 'in-progress',
|
|
61
|
-
priority: 'high',
|
|
62
|
-
assignee: 'Alex',
|
|
63
|
-
project: 'Platform',
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
title: 'Fix column reorder bug',
|
|
67
|
-
description: 'Columns shift incorrectly when moving to position 0',
|
|
68
|
-
column: 'in-progress',
|
|
69
|
-
priority: 'high',
|
|
70
|
-
assignee: 'BuildBot',
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
// review
|
|
74
|
-
{
|
|
75
|
-
title: 'Add bulk delete command',
|
|
76
|
-
description: 'Allow deleting multiple tasks by ID or filter',
|
|
77
|
-
column: 'review',
|
|
78
|
-
priority: 'medium',
|
|
79
|
-
assignee: 'Alex',
|
|
80
|
-
project: 'Platform',
|
|
81
|
-
},
|
|
82
|
-
|
|
83
|
-
// done
|
|
84
|
-
{
|
|
85
|
-
title: 'Set up CI pipeline',
|
|
86
|
-
description: 'GitHub Actions for lint, typecheck, and test on every PR',
|
|
87
|
-
column: 'done',
|
|
88
|
-
priority: 'high',
|
|
89
|
-
assignee: 'BuildBot',
|
|
90
|
-
project: 'Platform',
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
title: 'Add activity logging',
|
|
94
|
-
description: 'Track task creates, moves, updates, and deletes',
|
|
95
|
-
column: 'done',
|
|
96
|
-
priority: 'medium',
|
|
97
|
-
assignee: 'Alex',
|
|
98
|
-
},
|
|
99
|
-
]
|
|
100
|
-
|
|
101
|
-
export function seedFixtures(db: Database): { taskCount: number; movedCount: number } {
|
|
102
|
-
let movedCount = 0
|
|
103
|
-
|
|
104
|
-
for (const fixture of FIXTURE_TASKS) {
|
|
105
|
-
// Create all tasks in backlog first (addTask defaults to backlog)
|
|
106
|
-
const task = addTask(db, fixture.title, {
|
|
107
|
-
description: fixture.description,
|
|
108
|
-
priority: fixture.priority,
|
|
109
|
-
assignee: fixture.assignee,
|
|
110
|
-
project: fixture.project,
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
// Move to target column if not already in backlog
|
|
114
|
-
if (fixture.column !== 'backlog') {
|
|
115
|
-
moveTask(db, task.id, fixture.column)
|
|
116
|
-
movedCount++
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Escalate the bug to urgent — generates a realistic "prioritized" activity entry
|
|
121
|
-
const inProgress = listTasks(db, { column: 'in-progress' })
|
|
122
|
-
const bug = inProgress.find((t) => t.title === 'Fix column reorder bug')
|
|
123
|
-
if (bug) {
|
|
124
|
-
updateTask(db, bug.id, { priority: 'urgent' })
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return { taskCount: FIXTURE_TASKS.length, movedCount }
|
|
128
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}:root{--bg-primary: #0f1117;--bg-secondary: #1a1d27;--bg-card: #1e2130;--bg-hover: #252838;--border: #2a2d3a;--border-light: #353849;--text-primary: rgba(255, 255, 255, .92);--text-secondary: #9ca3af;--text-muted: #6b7280;--accent: #6366f1;--accent-hover: #818cf8;--priority-urgent: #ef4444;--priority-high: #f97316;--priority-medium: #eab308;--priority-low: #22c55e;--col-recurring: #6b7280;--col-backlog: #f59e0b;--col-in-progress: #3b82f6;--col-review: #8b5cf6;--col-done: #22c55e;--section-radius: 10px}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;background:var(--bg-primary);color:var(--text-primary);min-height:100vh}#root{width:100%;max-width:1800px;margin:0 auto;padding:16px 24px}.header{padding:12px 0 16px;border-bottom:1px solid var(--border);margin-bottom:16px}.headerTop{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}.header h1{font-size:20px;font-weight:700;letter-spacing:-.02em}.header h1 span{color:var(--accent)}.newTaskBtn{background:var(--accent);color:#fff;border:none;padding:8px 18px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;transition:background .15s}.newTaskBtn:hover{background:var(--accent-hover)}.statsBar{display:flex;gap:12px;margin-bottom:14px}.statCard{background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:10px 16px;display:flex;align-items:baseline;gap:8px}.statValue{font-size:20px;font-weight:700;color:var(--accent)}.statLabel{font-size:12px;color:var(--text-muted)}.filterBar{display:flex;align-items:center;justify-content:space-between;gap:12px}.filterGroup{display:flex;gap:6px;align-items:center}.filterBtn{background:var(--bg-secondary);border:1px solid var(--border);color:var(--text-secondary);padding:5px 14px;border-radius:20px;font-size:12px;cursor:pointer;transition:all .15s}.filterBtn:hover{border-color:var(--border-light);color:var(--text-primary)}.filterBtn.active{background:var(--accent);border-color:var(--accent);color:#fff}.projectDropdown{background:var(--bg-secondary);border:1px solid var(--border);color:var(--text-secondary);padding:5px 28px 5px 14px;border-radius:20px;font-size:12px;cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-image:url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%236b7280' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center}.projectDropdown:focus{outline:none;border-color:var(--accent)}.board{display:flex;gap:12px;overflow-x:auto;padding-bottom:8px;min-height:calc(100vh - 220px)}.column{flex:1;min-width:240px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--section-radius);display:flex;flex-direction:column}.columnHeader{padding:12px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px}.columnDot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.columnName{font-size:13px;font-weight:600;text-transform:capitalize;flex:1}.columnCount{background:var(--border);color:var(--text-secondary);padding:2px 8px;border-radius:10px;font-size:11px;min-width:22px;text-align:center}.columnAddBtn{background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:16px;line-height:1;padding:0 2px;transition:color .15s}.columnAddBtn:hover{color:var(--accent)}.columnBody{padding:8px;display:flex;flex-direction:column;gap:6px;min-height:60px;flex:1}.emptyColumn{display:flex;align-items:center;justify-content:center;color:var(--text-muted);font-size:13px;flex:1}.taskCard{background:var(--bg-card);border:1px solid var(--border);border-radius:8px;padding:12px 14px;cursor:pointer;transition:border-color .15s,background .15s}.taskCard:hover{border-color:var(--border-light);background:var(--bg-hover)}.taskCard.selected{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent)}.taskCardHeader{display:flex;align-items:flex-start;gap:8px;margin-bottom:6px}.taskTitle{font-size:13px;font-weight:500;line-height:1.4;flex:1}.taskDescription{font-size:12px;color:var(--text-muted);line-height:1.4;margin-bottom:10px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.taskFooter{display:flex;align-items:center;justify-content:space-between;gap:8px}.taskFooterLeft{display:flex;align-items:center;gap:8px;min-width:0}.assigneeAvatar{width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;color:#fff;flex-shrink:0}.assigneeName{font-size:12px;color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.projectTag{font-size:11px;padding:2px 8px;border-radius:4px;background:#6366f126;color:var(--accent);white-space:nowrap}.timestamp{font-size:11px;color:var(--text-muted);white-space:nowrap;flex-shrink:0}.priorityDot{width:8px;height:8px;border-radius:50%;flex-shrink:0;margin-top:4px}.priorityDot.urgent{background:var(--priority-urgent)}.priorityDot.high{background:var(--priority-high)}.priorityDot.medium{background:var(--priority-medium)}.priorityDot.low{background:var(--priority-low)}.taskDetailOverlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#0006;z-index:9}.taskDetail{position:fixed;top:0;right:0;width:420px;height:100vh;background:var(--bg-secondary);border-left:1px solid var(--border);padding:24px;overflow-y:auto;z-index:10}.taskDetail .closeBtn{position:absolute;top:16px;right:16px;background:none;border:1px solid var(--border);color:var(--text-secondary);cursor:pointer;padding:4px 10px;border-radius:6px;font-size:14px}.taskDetail .closeBtn:hover{border-color:var(--accent);color:var(--text-primary)}.detailTitle{font-size:18px;font-weight:600;margin-bottom:20px;padding-right:40px;line-height:1.4}.detailField{margin-bottom:16px}.detailLabel{font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:4px}.detailValue{font-size:14px}.detailActions{display:flex;gap:8px;margin-top:24px;padding-top:16px;border-top:1px solid var(--border)}.detailSelect{background:var(--bg-card);border:1px solid var(--border);color:var(--text-primary);padding:6px 12px;border-radius:6px;font-size:13px;flex:1}.detailSelect:focus{outline:none;border-color:var(--accent)}.deleteBtn{background:#ef44441a;border:1px solid rgba(239,68,68,.3);color:#ef4444;padding:6px 16px;border-radius:6px;font-size:13px;cursor:pointer;transition:all .15s}.deleteBtn:hover{background:#ef444433;border-color:#ef444480}.modalOverlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#0009;display:flex;align-items:center;justify-content:center;z-index:20}.modal{background:var(--bg-secondary);border:1px solid var(--border);border-radius:12px;padding:28px;width:480px;max-width:90vw;max-height:90vh;overflow-y:auto}.modal h2{font-size:18px;font-weight:600;margin-bottom:20px}.formField{margin-bottom:16px}.formLabel{display:block;font-size:12px;color:var(--text-secondary);margin-bottom:6px;font-weight:500}.formInput{width:100%;background:var(--bg-card);border:1px solid var(--border);color:var(--text-primary);padding:8px 12px;border-radius:6px;font-size:14px;font-family:inherit}.formInput:focus{outline:none;border-color:var(--accent)}textarea.formInput{resize:vertical;min-height:80px}select.formInput{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding-right:28px;background-image:url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%236b7280' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center}.formRow{display:grid;grid-template-columns:1fr 1fr;gap:12px}.modalActions{display:flex;justify-content:flex-end;gap:10px;margin-top:24px}.btnSecondary{background:var(--bg-card);border:1px solid var(--border);color:var(--text-secondary);padding:8px 18px;border-radius:8px;font-size:13px;cursor:pointer;transition:all .15s}.btnSecondary:hover{border-color:var(--border-light);color:var(--text-primary)}.btnPrimary{background:var(--accent);border:none;color:#fff;padding:8px 18px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;transition:background .15s}.btnPrimary:hover{background:var(--accent-hover)}.btnPrimary:disabled{opacity:.5;cursor:not-allowed}.appLayout{display:grid;grid-template-columns:1fr;gap:0}.loading{display:flex;align-items:center;justify-content:center;padding:40px;color:var(--text-secondary)}.errorBanner{background:#ef44441a;border:1px solid rgba(239,68,68,.3);color:#ef4444;padding:10px 16px;border-radius:8px;font-size:13px;margin-bottom:12px}.wsIndicator{width:8px;height:8px;border-radius:50%;display:inline-block}.wsIndicator.connected{background:var(--col-done)}.wsIndicator.disconnected{background:var(--text-muted)}
|