taskdog-ui 0.18.1__py3-none-any.whl
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.
- taskdog/__init__.py +8 -0
- taskdog/cli/__init__.py +1 -0
- taskdog/cli/commands/__init__.py +1 -0
- taskdog/cli/commands/add.py +148 -0
- taskdog/cli/commands/add_dependency.py +40 -0
- taskdog/cli/commands/audit_logs.py +226 -0
- taskdog/cli/commands/cancel.py +29 -0
- taskdog/cli/commands/common_options.py +116 -0
- taskdog/cli/commands/done.py +30 -0
- taskdog/cli/commands/export.py +155 -0
- taskdog/cli/commands/fix_actual.py +187 -0
- taskdog/cli/commands/gantt.py +153 -0
- taskdog/cli/commands/note.py +218 -0
- taskdog/cli/commands/optimize.py +152 -0
- taskdog/cli/commands/pause.py +36 -0
- taskdog/cli/commands/remove_dependency.py +40 -0
- taskdog/cli/commands/reopen.py +40 -0
- taskdog/cli/commands/restore.py +29 -0
- taskdog/cli/commands/rm.py +56 -0
- taskdog/cli/commands/show.py +31 -0
- taskdog/cli/commands/start.py +32 -0
- taskdog/cli/commands/stats.py +65 -0
- taskdog/cli/commands/table.py +96 -0
- taskdog/cli/commands/table_helpers.py +27 -0
- taskdog/cli/commands/tags.py +95 -0
- taskdog/cli/commands/timeline.py +90 -0
- taskdog/cli/commands/tui.py +64 -0
- taskdog/cli/commands/update.py +128 -0
- taskdog/cli/commands/update_helpers.py +45 -0
- taskdog/cli/context.py +34 -0
- taskdog/cli/error_handler.py +98 -0
- taskdog/cli_main.py +187 -0
- taskdog/console/__init__.py +1 -0
- taskdog/console/console_writer.py +139 -0
- taskdog/console/rich_console_writer.py +212 -0
- taskdog/constants/__init__.py +102 -0
- taskdog/constants/ascii_art.py +13 -0
- taskdog/constants/audit_log.py +41 -0
- taskdog/constants/colors.py +33 -0
- taskdog/constants/common.py +39 -0
- taskdog/constants/formatting.py +15 -0
- taskdog/constants/gantt.py +33 -0
- taskdog/constants/icons.py +7 -0
- taskdog/constants/symbols.py +18 -0
- taskdog/constants/task_table.py +59 -0
- taskdog/constants/timeline.py +14 -0
- taskdog/exporters/__init__.py +13 -0
- taskdog/exporters/csv_task_exporter.py +54 -0
- taskdog/exporters/json_task_exporter.py +22 -0
- taskdog/exporters/markdown_table_exporter.py +154 -0
- taskdog/exporters/task_exporter.py +47 -0
- taskdog/formatters/__init__.py +1 -0
- taskdog/formatters/date_time_formatter.py +195 -0
- taskdog/formatters/duration_formatter.py +76 -0
- taskdog/infrastructure/__init__.py +12 -0
- taskdog/infrastructure/cli_config_manager.py +234 -0
- taskdog/presenters/__init__.py +18 -0
- taskdog/presenters/gantt_presenter.py +87 -0
- taskdog/presenters/statistics_presenter.py +137 -0
- taskdog/presenters/table_presenter.py +59 -0
- taskdog/presenters/timeline_presenter.py +191 -0
- taskdog/py.typed +0 -0
- taskdog/renderers/__init__.py +1 -0
- taskdog/renderers/gantt_cell_formatter.py +749 -0
- taskdog/renderers/rich_detail_renderer.py +148 -0
- taskdog/renderers/rich_gantt_renderer.py +361 -0
- taskdog/renderers/rich_renderer_base.py +23 -0
- taskdog/renderers/rich_statistics_renderer.py +413 -0
- taskdog/renderers/rich_table_renderer.py +415 -0
- taskdog/renderers/rich_timeline_renderer.py +248 -0
- taskdog/renderers/timeline_cell_formatter.py +131 -0
- taskdog/services/__init__.py +1 -0
- taskdog/services/task_data_loader.py +109 -0
- taskdog/shared/__init__.py +0 -0
- taskdog/shared/click_types/__init__.py +5 -0
- taskdog/shared/click_types/field_list.py +87 -0
- taskdog/shared/click_types/positive_number.py +95 -0
- taskdog/tui/__init__.py +1 -0
- taskdog/tui/app.py +523 -0
- taskdog/tui/commands/__init__.py +44 -0
- taskdog/tui/commands/add.py +65 -0
- taskdog/tui/commands/audit.py +15 -0
- taskdog/tui/commands/base.py +235 -0
- taskdog/tui/commands/batch_command_base.py +141 -0
- taskdog/tui/commands/cancel.py +24 -0
- taskdog/tui/commands/decorators.py +37 -0
- taskdog/tui/commands/done.py +16 -0
- taskdog/tui/commands/edit.py +188 -0
- taskdog/tui/commands/export.py +93 -0
- taskdog/tui/commands/factory.py +74 -0
- taskdog/tui/commands/fix_actual.py +53 -0
- taskdog/tui/commands/hard_delete.py +28 -0
- taskdog/tui/commands/help.py +14 -0
- taskdog/tui/commands/note.py +69 -0
- taskdog/tui/commands/optimize.py +121 -0
- taskdog/tui/commands/pause.py +16 -0
- taskdog/tui/commands/refresh.py +12 -0
- taskdog/tui/commands/reopen.py +24 -0
- taskdog/tui/commands/rm.py +28 -0
- taskdog/tui/commands/show.py +74 -0
- taskdog/tui/commands/start.py +16 -0
- taskdog/tui/commands/stats.py +13 -0
- taskdog/tui/constants/__init__.py +1 -0
- taskdog/tui/constants/command_mapping.py +21 -0
- taskdog/tui/constants/export_config.py +9 -0
- taskdog/tui/constants/keybindings.py +92 -0
- taskdog/tui/constants/ui_settings.py +45 -0
- taskdog/tui/context.py +35 -0
- taskdog/tui/dialogs/__init__.py +11 -0
- taskdog/tui/dialogs/algorithm_selection_dialog.py +217 -0
- taskdog/tui/dialogs/base_dialog.py +119 -0
- taskdog/tui/dialogs/confirmation_dialog.py +73 -0
- taskdog/tui/dialogs/fix_actual_dialog.py +248 -0
- taskdog/tui/dialogs/form_dialog.py +88 -0
- taskdog/tui/dialogs/help_dialog.py +171 -0
- taskdog/tui/dialogs/scrollable_dialog.py +86 -0
- taskdog/tui/dialogs/stats_dialog.py +528 -0
- taskdog/tui/dialogs/task_detail_dialog.py +397 -0
- taskdog/tui/dialogs/task_form_dialog.py +176 -0
- taskdog/tui/events.py +135 -0
- taskdog/tui/forms/__init__.py +3 -0
- taskdog/tui/forms/suggesters/__init__.py +9 -0
- taskdog/tui/forms/suggesters/comma_separated_suggester.py +58 -0
- taskdog/tui/forms/task_form_fields.py +224 -0
- taskdog/tui/forms/validators/__init__.py +11 -0
- taskdog/tui/forms/validators/datetime_validator.py +87 -0
- taskdog/tui/forms/validators/optimization_validators.py +66 -0
- taskdog/tui/messages.py +143 -0
- taskdog/tui/palette/__init__.py +13 -0
- taskdog/tui/palette/providers/__init__.py +27 -0
- taskdog/tui/palette/providers/audit_provider.py +13 -0
- taskdog/tui/palette/providers/base.py +122 -0
- taskdog/tui/palette/providers/export_providers.py +54 -0
- taskdog/tui/palette/providers/help_provider.py +13 -0
- taskdog/tui/palette/providers/optimize_providers.py +13 -0
- taskdog/tui/palette/providers/sort_providers.py +57 -0
- taskdog/tui/screens/__init__.py +1 -0
- taskdog/tui/screens/audit_log_screen.py +66 -0
- taskdog/tui/screens/main_screen.py +218 -0
- taskdog/tui/selection.py +69 -0
- taskdog/tui/services/__init__.py +7 -0
- taskdog/tui/services/connection_monitor.py +42 -0
- taskdog/tui/services/event_handler_registry.py +212 -0
- taskdog/tui/services/task_ui_manager.py +245 -0
- taskdog/tui/services/websocket_handler.py +37 -0
- taskdog/tui/state/__init__.py +7 -0
- taskdog/tui/state/connection_status.py +19 -0
- taskdog/tui/state/connection_status_manager.py +84 -0
- taskdog/tui/state/tui_state.py +257 -0
- taskdog/tui/styles/components.tcss +149 -0
- taskdog/tui/styles/dialogs.tcss +254 -0
- taskdog/tui/styles/main.tcss +74 -0
- taskdog/tui/styles/theme.tcss +8 -0
- taskdog/tui/utils/__init__.py +1 -0
- taskdog/tui/utils/css_loader.py +32 -0
- taskdog/tui/widgets/__init__.py +15 -0
- taskdog/tui/widgets/audit_log_entry_builder.py +184 -0
- taskdog/tui/widgets/audit_log_table.py +153 -0
- taskdog/tui/widgets/base_widget.py +60 -0
- taskdog/tui/widgets/custom_footer.py +251 -0
- taskdog/tui/widgets/gantt_data_table.py +652 -0
- taskdog/tui/widgets/gantt_widget.py +412 -0
- taskdog/tui/widgets/search_query_parser.py +127 -0
- taskdog/tui/widgets/task_search_filter.py +238 -0
- taskdog/tui/widgets/task_table.py +423 -0
- taskdog/tui/widgets/task_table_row_builder.py +229 -0
- taskdog/tui/widgets/vi_navigation_mixin.py +134 -0
- taskdog/tui/widgets/vi_select.py +77 -0
- taskdog/utils/__init__.py +0 -0
- taskdog/utils/editor.py +38 -0
- taskdog/utils/note_editor.py +211 -0
- taskdog/utils/notes_template.py +57 -0
- taskdog/utils/template_loader.py +86 -0
- taskdog/view_models/__init__.py +26 -0
- taskdog/view_models/base.py +25 -0
- taskdog/view_models/gantt_view_model.py +87 -0
- taskdog/view_models/statistics_view_model.py +108 -0
- taskdog/view_models/task_view_model.py +60 -0
- taskdog/view_models/timeline_view_model.py +68 -0
- taskdog_ui-0.18.1.dist-info/METADATA +190 -0
- taskdog_ui-0.18.1.dist-info/RECORD +184 -0
- taskdog_ui-0.18.1.dist-info/WHEEL +5 -0
- taskdog_ui-0.18.1.dist-info/entry_points.txt +2 -0
- taskdog_ui-0.18.1.dist-info/top_level.txt +1 -0
taskdog/__init__.py
ADDED
taskdog/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI interface."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI commands."""
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Add command - Add a new task."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from taskdog.cli.error_handler import handle_task_errors
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
from taskdog.cli.context import CliContext
|
|
15
|
+
from taskdog_core.domain.exceptions.task_exceptions import TaskValidationError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.command(name="add", help="Add a new task.")
|
|
19
|
+
@click.argument("name", type=str)
|
|
20
|
+
@click.option(
|
|
21
|
+
"--priority",
|
|
22
|
+
"-p",
|
|
23
|
+
type=int,
|
|
24
|
+
default=None,
|
|
25
|
+
help="Task priority (default: from config or 5, higher value = higher priority)",
|
|
26
|
+
)
|
|
27
|
+
@click.option(
|
|
28
|
+
"--fixed",
|
|
29
|
+
"-f",
|
|
30
|
+
is_flag=True,
|
|
31
|
+
help="Mark task as fixed (won't be rescheduled by optimizer)",
|
|
32
|
+
)
|
|
33
|
+
@click.option(
|
|
34
|
+
"--depends-on",
|
|
35
|
+
"-d",
|
|
36
|
+
multiple=True,
|
|
37
|
+
type=int,
|
|
38
|
+
help="Task IDs this task depends on (can be specified multiple times)",
|
|
39
|
+
)
|
|
40
|
+
@click.option(
|
|
41
|
+
"--tag",
|
|
42
|
+
"-t",
|
|
43
|
+
multiple=True,
|
|
44
|
+
type=str,
|
|
45
|
+
help="Tags for categorization and filtering (can be specified multiple times)",
|
|
46
|
+
)
|
|
47
|
+
@click.option(
|
|
48
|
+
"--deadline",
|
|
49
|
+
"-D",
|
|
50
|
+
type=click.DateTime(formats=["%Y-%m-%d %H:%M:%S"]),
|
|
51
|
+
default=None,
|
|
52
|
+
help="Task deadline (format: YYYY-MM-DD HH:MM:SS)",
|
|
53
|
+
)
|
|
54
|
+
@click.option(
|
|
55
|
+
"--estimate",
|
|
56
|
+
"-e",
|
|
57
|
+
type=float,
|
|
58
|
+
default=None,
|
|
59
|
+
help="Estimated duration in hours (must be > 0)",
|
|
60
|
+
)
|
|
61
|
+
@click.option(
|
|
62
|
+
"--start",
|
|
63
|
+
"-s",
|
|
64
|
+
type=click.DateTime(formats=["%Y-%m-%d %H:%M:%S"]),
|
|
65
|
+
default=None,
|
|
66
|
+
help="Planned start time (format: YYYY-MM-DD HH:MM:SS)",
|
|
67
|
+
)
|
|
68
|
+
@click.option(
|
|
69
|
+
"--end",
|
|
70
|
+
"-E",
|
|
71
|
+
type=click.DateTime(formats=["%Y-%m-%d %H:%M:%S"]),
|
|
72
|
+
default=None,
|
|
73
|
+
help="Planned end time (format: YYYY-MM-DD HH:MM:SS)",
|
|
74
|
+
)
|
|
75
|
+
@click.pass_context
|
|
76
|
+
@handle_task_errors("adding task")
|
|
77
|
+
def add_command(
|
|
78
|
+
ctx: click.Context,
|
|
79
|
+
name: str,
|
|
80
|
+
priority: int,
|
|
81
|
+
fixed: bool,
|
|
82
|
+
depends_on: tuple[int, ...],
|
|
83
|
+
tag: tuple[str, ...],
|
|
84
|
+
deadline: datetime | None,
|
|
85
|
+
estimate: float | None,
|
|
86
|
+
start: datetime | None,
|
|
87
|
+
end: datetime | None,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Add a new task.
|
|
90
|
+
|
|
91
|
+
You can set all task properties at creation time, or use dedicated commands
|
|
92
|
+
after creation (deadline, estimate, schedule) for updates.
|
|
93
|
+
|
|
94
|
+
Usage:
|
|
95
|
+
# Basic task
|
|
96
|
+
taskdog add "Task name"
|
|
97
|
+
|
|
98
|
+
# With priority and tags
|
|
99
|
+
taskdog add "Task name" --priority 3 --tag backend
|
|
100
|
+
|
|
101
|
+
# Full task with all properties
|
|
102
|
+
taskdog add "Implement auth" -p 5 -D "2025-12-01" -e 8 -s "2025-11-20" -t backend
|
|
103
|
+
|
|
104
|
+
Examples:
|
|
105
|
+
# Simple task
|
|
106
|
+
taskdog add "Implement authentication"
|
|
107
|
+
|
|
108
|
+
# With priority
|
|
109
|
+
taskdog add "Fix login bug" -p 5
|
|
110
|
+
|
|
111
|
+
# With deadline and estimate
|
|
112
|
+
taskdog add "Add unit tests" --deadline "2025-12-15" --estimate 4
|
|
113
|
+
|
|
114
|
+
# Complete task with schedule
|
|
115
|
+
taskdog add "Code review" -p 3 -s "2025-11-20 09:00" -e "2025-11-20 12:00"
|
|
116
|
+
|
|
117
|
+
# With dependencies and tags
|
|
118
|
+
taskdog add "Deploy feature" -d 123 -t deployment -p 5
|
|
119
|
+
"""
|
|
120
|
+
ctx_obj: CliContext = ctx.obj
|
|
121
|
+
console_writer = ctx_obj.console_writer
|
|
122
|
+
|
|
123
|
+
# Create task via API client
|
|
124
|
+
task = ctx_obj.api_client.create_task(
|
|
125
|
+
name=name,
|
|
126
|
+
priority=priority,
|
|
127
|
+
is_fixed=fixed,
|
|
128
|
+
tags=list(tag) if tag else None,
|
|
129
|
+
deadline=deadline,
|
|
130
|
+
estimated_duration=estimate,
|
|
131
|
+
planned_start=start,
|
|
132
|
+
planned_end=end,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Add dependencies if specified
|
|
136
|
+
if depends_on:
|
|
137
|
+
for dep_id in depends_on:
|
|
138
|
+
try:
|
|
139
|
+
task = ctx_obj.api_client.add_dependency(task.id, dep_id)
|
|
140
|
+
except TaskValidationError as e:
|
|
141
|
+
console_writer.validation_error(str(e))
|
|
142
|
+
# Continue adding other dependencies even if one fails
|
|
143
|
+
|
|
144
|
+
console_writer.task_success("Added", task)
|
|
145
|
+
if task.depends_on:
|
|
146
|
+
console_writer.info(f"Dependencies: {task.depends_on}")
|
|
147
|
+
if task.tags:
|
|
148
|
+
console_writer.info(f"Tags: {', '.join(task.tags)}")
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Add-dependency command - Add a task dependency."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from taskdog.cli.error_handler import handle_task_errors
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from taskdog.cli.context import CliContext
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.command(name="add-dependency", help="Add a dependency to a task.")
|
|
16
|
+
@click.argument("task_id", type=int)
|
|
17
|
+
@click.argument("depends_on_id", type=int)
|
|
18
|
+
@click.pass_context
|
|
19
|
+
@handle_task_errors("adding dependency")
|
|
20
|
+
def add_dependency_command(
|
|
21
|
+
ctx: click.Context, task_id: int, depends_on_id: int
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Add a dependency to a task.
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
taskdog add-dependency <TASK_ID> <DEPENDS_ON_ID>
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
taskdog add-dependency 5 3 # Task 5 depends on task 3
|
|
30
|
+
"""
|
|
31
|
+
ctx_obj: CliContext = ctx.obj
|
|
32
|
+
console_writer = ctx_obj.console_writer
|
|
33
|
+
|
|
34
|
+
# Add dependency via API client
|
|
35
|
+
task = ctx_obj.api_client.add_dependency(task_id, depends_on_id)
|
|
36
|
+
|
|
37
|
+
console_writer.success(
|
|
38
|
+
f"Added dependency: Task {task_id} now depends on task {depends_on_id}"
|
|
39
|
+
)
|
|
40
|
+
console_writer.info(f"Task {task_id} dependencies: {task.depends_on}")
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Audit logs command - Display operation history."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from taskdog.cli.error_handler import handle_command_errors
|
|
12
|
+
from taskdog.constants.audit_log import (
|
|
13
|
+
AUDIT_CHANGES_WIDTH,
|
|
14
|
+
AUDIT_CLIENT_WIDTH,
|
|
15
|
+
AUDIT_ID_WIDTH,
|
|
16
|
+
AUDIT_OPERATION_WIDTH,
|
|
17
|
+
AUDIT_STATUS_WIDTH,
|
|
18
|
+
AUDIT_TIMESTAMP_WIDTH,
|
|
19
|
+
COLUMN_AUDIT_ID_STYLE,
|
|
20
|
+
COLUMN_AUDIT_STATUS_FAIL_STYLE,
|
|
21
|
+
COLUMN_AUDIT_STATUS_OK_STYLE,
|
|
22
|
+
HEADER_AUDIT_CHANGES,
|
|
23
|
+
HEADER_AUDIT_CLIENT,
|
|
24
|
+
HEADER_AUDIT_OPERATION,
|
|
25
|
+
HEADER_AUDIT_RESOURCE,
|
|
26
|
+
HEADER_AUDIT_STATUS,
|
|
27
|
+
HEADER_AUDIT_TIMESTAMP,
|
|
28
|
+
JUSTIFY_AUDIT_CHANGES,
|
|
29
|
+
)
|
|
30
|
+
from taskdog.constants.common import HEADER_ID, TABLE_HEADER_STYLE
|
|
31
|
+
from taskdog.constants.formatting import format_table_title
|
|
32
|
+
from taskdog.tui.widgets.audit_log_entry_builder import format_audit_changes
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from taskdog.cli.context import CliContext
|
|
36
|
+
from taskdog_core.application.dto.audit_log_dto import AuditLogOutput
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _parse_date_filter(date_str: str, end_of_day: bool = False) -> datetime:
|
|
40
|
+
"""Parse a date filter string to datetime.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
date_str: ISO format date or datetime string
|
|
44
|
+
end_of_day: If True and only date provided, use end of day
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Parsed datetime object
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
return datetime.fromisoformat(date_str)
|
|
51
|
+
except ValueError:
|
|
52
|
+
suffix = "T23:59:59" if end_of_day else "T00:00:00"
|
|
53
|
+
return datetime.fromisoformat(f"{date_str}{suffix}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _format_resource(log: AuditLogOutput) -> str:
|
|
57
|
+
"""Format resource display for audit log entry."""
|
|
58
|
+
if log.resource_id and log.resource_name:
|
|
59
|
+
return f"[cyan]#{log.resource_id}[/cyan] {log.resource_name}"
|
|
60
|
+
if log.resource_id:
|
|
61
|
+
return f"[cyan]#{log.resource_id}[/cyan]"
|
|
62
|
+
if log.resource_name:
|
|
63
|
+
return log.resource_name
|
|
64
|
+
return "[dim]-[/dim]"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _format_changes(log: AuditLogOutput) -> str:
|
|
68
|
+
"""Format changes display for audit log entry."""
|
|
69
|
+
if not log.success and log.error_message:
|
|
70
|
+
error_msg = log.error_message[:AUDIT_CHANGES_WIDTH]
|
|
71
|
+
if len(log.error_message) > AUDIT_CHANGES_WIDTH:
|
|
72
|
+
error_msg += "..."
|
|
73
|
+
return f"[red]{error_msg}[/red]"
|
|
74
|
+
return format_audit_changes(
|
|
75
|
+
log.old_values, log.new_values, max_length=AUDIT_CHANGES_WIDTH
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@click.command(
|
|
80
|
+
name="audit-logs",
|
|
81
|
+
help="""Display operation history (audit logs).
|
|
82
|
+
|
|
83
|
+
Shows a history of operations performed via the API, including task creates,
|
|
84
|
+
updates, status changes, and other modifications.
|
|
85
|
+
|
|
86
|
+
Use filters to narrow down the logs by client name, operation type, task ID, etc.
|
|
87
|
+
|
|
88
|
+
Examples:
|
|
89
|
+
taskdog audit-logs # Show latest 100 logs
|
|
90
|
+
taskdog audit-logs --client claude-code # Filter by client
|
|
91
|
+
taskdog audit-logs --task 123 # Filter by task ID
|
|
92
|
+
taskdog audit-logs --operation complete_task # Filter by operation
|
|
93
|
+
taskdog audit-logs --since 2025-12-01 # Filter by date
|
|
94
|
+
""",
|
|
95
|
+
)
|
|
96
|
+
@click.option(
|
|
97
|
+
"--client",
|
|
98
|
+
"-c",
|
|
99
|
+
"client_filter",
|
|
100
|
+
type=str,
|
|
101
|
+
default=None,
|
|
102
|
+
help="Filter by client name (e.g., 'claude-code')",
|
|
103
|
+
)
|
|
104
|
+
@click.option(
|
|
105
|
+
"--operation",
|
|
106
|
+
"-o",
|
|
107
|
+
type=str,
|
|
108
|
+
default=None,
|
|
109
|
+
help="Filter by operation type (e.g., 'create_task', 'complete_task')",
|
|
110
|
+
)
|
|
111
|
+
@click.option(
|
|
112
|
+
"--task",
|
|
113
|
+
"-t",
|
|
114
|
+
"task_id",
|
|
115
|
+
type=int,
|
|
116
|
+
default=None,
|
|
117
|
+
help="Filter by task ID",
|
|
118
|
+
)
|
|
119
|
+
@click.option(
|
|
120
|
+
"--since",
|
|
121
|
+
type=str,
|
|
122
|
+
default=None,
|
|
123
|
+
help="Show logs since this date (ISO format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS)",
|
|
124
|
+
)
|
|
125
|
+
@click.option(
|
|
126
|
+
"--until",
|
|
127
|
+
type=str,
|
|
128
|
+
default=None,
|
|
129
|
+
help="Show logs until this date (ISO format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS)",
|
|
130
|
+
)
|
|
131
|
+
@click.option(
|
|
132
|
+
"--limit",
|
|
133
|
+
"-n",
|
|
134
|
+
type=click.IntRange(1, 1000),
|
|
135
|
+
default=100,
|
|
136
|
+
help="Maximum number of logs to show (default: 100, max: 1000)",
|
|
137
|
+
)
|
|
138
|
+
@click.option(
|
|
139
|
+
"--failed",
|
|
140
|
+
is_flag=True,
|
|
141
|
+
default=False,
|
|
142
|
+
help="Show only failed operations",
|
|
143
|
+
)
|
|
144
|
+
@click.pass_context
|
|
145
|
+
@handle_command_errors("fetching audit logs")
|
|
146
|
+
def audit_logs_command(
|
|
147
|
+
ctx: click.Context,
|
|
148
|
+
client_filter: str | None,
|
|
149
|
+
operation: str | None,
|
|
150
|
+
task_id: int | None,
|
|
151
|
+
since: str | None,
|
|
152
|
+
until: str | None,
|
|
153
|
+
limit: int,
|
|
154
|
+
failed: bool,
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Display operation history (audit logs)."""
|
|
157
|
+
ctx_obj: CliContext = ctx.obj
|
|
158
|
+
console_writer = ctx_obj.console_writer
|
|
159
|
+
api_client = ctx_obj.api_client
|
|
160
|
+
|
|
161
|
+
# Parse date filters
|
|
162
|
+
start_date = _parse_date_filter(since) if since else None
|
|
163
|
+
end_date = _parse_date_filter(until, end_of_day=True) if until else None
|
|
164
|
+
|
|
165
|
+
# Fetch audit logs via API
|
|
166
|
+
result = api_client.list_audit_logs(
|
|
167
|
+
client_filter=client_filter,
|
|
168
|
+
operation=operation,
|
|
169
|
+
resource_type="task" if task_id else None,
|
|
170
|
+
resource_id=task_id,
|
|
171
|
+
success=False if failed else None,
|
|
172
|
+
start_date=start_date,
|
|
173
|
+
end_date=end_date,
|
|
174
|
+
limit=limit,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if not result.logs:
|
|
178
|
+
console_writer.info("No audit logs found matching the criteria.")
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
# Create Rich table
|
|
182
|
+
table = Table(
|
|
183
|
+
title=format_table_title(
|
|
184
|
+
f"Audit Logs ({len(result.logs)} of {result.total_count})"
|
|
185
|
+
),
|
|
186
|
+
show_header=True,
|
|
187
|
+
header_style=TABLE_HEADER_STYLE,
|
|
188
|
+
)
|
|
189
|
+
table.add_column(HEADER_ID, style=COLUMN_AUDIT_ID_STYLE, width=AUDIT_ID_WIDTH)
|
|
190
|
+
table.add_column(HEADER_AUDIT_TIMESTAMP, width=AUDIT_TIMESTAMP_WIDTH)
|
|
191
|
+
table.add_column(HEADER_AUDIT_RESOURCE)
|
|
192
|
+
table.add_column(HEADER_AUDIT_OPERATION, width=AUDIT_OPERATION_WIDTH)
|
|
193
|
+
table.add_column(
|
|
194
|
+
HEADER_AUDIT_CHANGES,
|
|
195
|
+
width=AUDIT_CHANGES_WIDTH,
|
|
196
|
+
justify=JUSTIFY_AUDIT_CHANGES,
|
|
197
|
+
)
|
|
198
|
+
table.add_column(HEADER_AUDIT_CLIENT, width=AUDIT_CLIENT_WIDTH)
|
|
199
|
+
table.add_column(HEADER_AUDIT_STATUS, width=AUDIT_STATUS_WIDTH)
|
|
200
|
+
|
|
201
|
+
for log in result.logs:
|
|
202
|
+
status_style = (
|
|
203
|
+
COLUMN_AUDIT_STATUS_OK_STYLE
|
|
204
|
+
if log.success
|
|
205
|
+
else COLUMN_AUDIT_STATUS_FAIL_STYLE
|
|
206
|
+
)
|
|
207
|
+
status_label = "OK" if log.success else "FAILED"
|
|
208
|
+
changes = _format_changes(log)
|
|
209
|
+
table.add_row(
|
|
210
|
+
str(log.id),
|
|
211
|
+
log.timestamp.strftime("%Y-%m-%d %H:%M:%S"),
|
|
212
|
+
_format_resource(log),
|
|
213
|
+
log.operation,
|
|
214
|
+
changes,
|
|
215
|
+
log.client_name or "[dim]anonymous[/dim]",
|
|
216
|
+
f"[{status_style}]{status_label}[/{status_style}]",
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
console_writer.print(table)
|
|
220
|
+
|
|
221
|
+
# Show pagination info if there are more logs
|
|
222
|
+
if result.total_count > len(result.logs):
|
|
223
|
+
console_writer.info(
|
|
224
|
+
f"Showing {len(result.logs)} of {result.total_count} logs. "
|
|
225
|
+
f"Use --limit to see more."
|
|
226
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Cancel command - Mark a task as canceled."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from taskdog.cli.context import CliContext
|
|
11
|
+
from taskdog_core.shared.constants import StatusVerbs
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.command(name="cancel", help="Mark task(s) as canceled.")
|
|
15
|
+
@click.argument("task_ids", nargs=-1, type=int, required=True)
|
|
16
|
+
@click.pass_context
|
|
17
|
+
def cancel_command(ctx: click.Context, task_ids: tuple[int, ...]) -> None:
|
|
18
|
+
"""Mark task(s) as canceled."""
|
|
19
|
+
ctx_obj: CliContext = ctx.obj
|
|
20
|
+
console_writer = ctx_obj.console_writer
|
|
21
|
+
|
|
22
|
+
results = ctx_obj.api_client.bulk_cancel(list(task_ids))
|
|
23
|
+
for result in results.results:
|
|
24
|
+
if result.success and result.task is not None:
|
|
25
|
+
console_writer.task_success(StatusVerbs.CANCELED, result.task)
|
|
26
|
+
elif result.error is not None:
|
|
27
|
+
console_writer.validation_error(result.error)
|
|
28
|
+
if len(task_ids) > 1:
|
|
29
|
+
console_writer.empty_line()
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Common CLI option decorators for reuse across commands."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def filter_options() -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
11
|
+
"""Add common filter options (--all, --status) to a command.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
@click.command()
|
|
15
|
+
@filter_options()
|
|
16
|
+
def my_command(ctx, include_archived, status, ...):
|
|
17
|
+
pass
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
21
|
+
@click.option(
|
|
22
|
+
"--status",
|
|
23
|
+
type=click.Choice(
|
|
24
|
+
["pending", "in_progress", "completed", "canceled"],
|
|
25
|
+
case_sensitive=False,
|
|
26
|
+
),
|
|
27
|
+
default=None,
|
|
28
|
+
help="Filter tasks by status (overrides --all). Note: archived tasks are controlled by the --all flag, not --status.",
|
|
29
|
+
)
|
|
30
|
+
@click.option(
|
|
31
|
+
"--all",
|
|
32
|
+
"-a",
|
|
33
|
+
"include_archived",
|
|
34
|
+
is_flag=True,
|
|
35
|
+
help="Show all tasks including completed, canceled, and archived",
|
|
36
|
+
)
|
|
37
|
+
@wraps(f)
|
|
38
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
39
|
+
return f(*args, **kwargs)
|
|
40
|
+
|
|
41
|
+
return wrapper
|
|
42
|
+
|
|
43
|
+
return decorator
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def sort_options(
|
|
47
|
+
default_sort: str = "id",
|
|
48
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
49
|
+
"""Add common sort options (--sort, --reverse) to a command.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
default_sort: Default field to sort by (default: "id")
|
|
53
|
+
|
|
54
|
+
Usage:
|
|
55
|
+
@click.command()
|
|
56
|
+
@sort_options(default_sort="deadline")
|
|
57
|
+
def my_command(ctx, sort, reverse, ...):
|
|
58
|
+
pass
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
62
|
+
@click.option(
|
|
63
|
+
"--reverse",
|
|
64
|
+
"-r",
|
|
65
|
+
is_flag=True,
|
|
66
|
+
help="Reverse sort order",
|
|
67
|
+
)
|
|
68
|
+
@click.option(
|
|
69
|
+
"--sort",
|
|
70
|
+
type=click.Choice(
|
|
71
|
+
["id", "priority", "deadline", "name", "status", "planned_start"]
|
|
72
|
+
),
|
|
73
|
+
default=default_sort,
|
|
74
|
+
help=f"Sort tasks by specified field (default: {default_sort})",
|
|
75
|
+
)
|
|
76
|
+
@wraps(f)
|
|
77
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
78
|
+
return f(*args, **kwargs)
|
|
79
|
+
|
|
80
|
+
return wrapper
|
|
81
|
+
|
|
82
|
+
return decorator
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def date_range_options() -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
86
|
+
"""Add common date range options (--start-date, --end-date) to a command.
|
|
87
|
+
|
|
88
|
+
Usage:
|
|
89
|
+
@click.command()
|
|
90
|
+
@date_range_options()
|
|
91
|
+
def my_command(ctx, start_date, end_date, ...):
|
|
92
|
+
pass
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
96
|
+
@click.option(
|
|
97
|
+
"--end-date",
|
|
98
|
+
"-e",
|
|
99
|
+
type=click.DateTime(),
|
|
100
|
+
help="End date for filtering (YYYY-MM-DD). "
|
|
101
|
+
"Shows tasks with any date field <= end date.",
|
|
102
|
+
)
|
|
103
|
+
@click.option(
|
|
104
|
+
"--start-date",
|
|
105
|
+
"-s",
|
|
106
|
+
type=click.DateTime(),
|
|
107
|
+
help="Start date for filtering (YYYY-MM-DD). "
|
|
108
|
+
"Shows tasks with any date field >= start date.",
|
|
109
|
+
)
|
|
110
|
+
@wraps(f)
|
|
111
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
112
|
+
return f(*args, **kwargs)
|
|
113
|
+
|
|
114
|
+
return wrapper
|
|
115
|
+
|
|
116
|
+
return decorator
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Done command - Mark a task as completed."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from taskdog.cli.context import CliContext
|
|
11
|
+
from taskdog_core.shared.constants import StatusVerbs
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.command(name="done", help="Mark task(s) as completed.")
|
|
15
|
+
@click.argument("task_ids", nargs=-1, type=int, required=True)
|
|
16
|
+
@click.pass_context
|
|
17
|
+
def done_command(ctx: click.Context, task_ids: tuple[int, ...]) -> None:
|
|
18
|
+
"""Mark task(s) as completed."""
|
|
19
|
+
ctx_obj: CliContext = ctx.obj
|
|
20
|
+
console_writer = ctx_obj.console_writer
|
|
21
|
+
|
|
22
|
+
results = ctx_obj.api_client.bulk_complete(list(task_ids))
|
|
23
|
+
for result in results.results:
|
|
24
|
+
if result.success and result.task is not None:
|
|
25
|
+
console_writer.task_success(StatusVerbs.COMPLETED, result.task)
|
|
26
|
+
console_writer.task_completion_details(result.task)
|
|
27
|
+
elif result.error is not None:
|
|
28
|
+
console_writer.validation_error(result.error)
|
|
29
|
+
if len(task_ids) > 1:
|
|
30
|
+
console_writer.empty_line()
|