taskwatch 0.1.0__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.
- taskwatch/__init__.py +3 -0
- taskwatch/cli/__init__.py +0 -0
- taskwatch/cli/log.py +128 -0
- taskwatch/cli/main.py +219 -0
- taskwatch/cli/report.py +104 -0
- taskwatch/cli/run.py +23 -0
- taskwatch/cli/task.py +349 -0
- taskwatch/core/__init__.py +0 -0
- taskwatch/core/executor.py +232 -0
- taskwatch/core/models.py +654 -0
- taskwatch/core/notifier.py +298 -0
- taskwatch/core/reporter.py +273 -0
- taskwatch/core/scheduler.py +345 -0
- taskwatch/utils/__init__.py +0 -0
- taskwatch/utils/config.py +288 -0
- taskwatch/utils/logger.py +49 -0
- taskwatch/web/__init__.py +0 -0
- taskwatch/web/api/__init__.py +0 -0
- taskwatch/web/api/reports.py +141 -0
- taskwatch/web/api/runs.py +74 -0
- taskwatch/web/api/settings.py +97 -0
- taskwatch/web/api/stats.py +24 -0
- taskwatch/web/api/tasks.py +203 -0
- taskwatch/web/app.py +75 -0
- taskwatch/web/routes/__init__.py +0 -0
- taskwatch/web/routes/dashboard.py +33 -0
- taskwatch/web/routes/logs.py +30 -0
- taskwatch/web/routes/reports.py +36 -0
- taskwatch/web/routes/settings.py +20 -0
- taskwatch/web/routes/tasks.py +71 -0
- taskwatch/web/static/css/style.css +32 -0
- taskwatch/web/templates/base.html +106 -0
- taskwatch/web/templates/dashboard.html +192 -0
- taskwatch/web/templates/logs.html +333 -0
- taskwatch/web/templates/reports.html +185 -0
- taskwatch/web/templates/settings.html +222 -0
- taskwatch/web/templates/task_detail.html +142 -0
- taskwatch/web/templates/tasks.html +306 -0
- taskwatch-0.1.0.dist-info/METADATA +499 -0
- taskwatch-0.1.0.dist-info/RECORD +43 -0
- taskwatch-0.1.0.dist-info/WHEEL +5 -0
- taskwatch-0.1.0.dist-info/entry_points.txt +3 -0
- taskwatch-0.1.0.dist-info/top_level.txt +1 -0
taskwatch/__init__.py
ADDED
|
File without changes
|
taskwatch/cli/log.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Log viewing CLI commands."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from taskwatch.core.models import Database
|
|
12
|
+
from taskwatch.utils.config import get_config
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
log_app = typer.Typer(help="Log viewing commands", no_args_is_help=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@log_app.callback(invoke_without_command=True)
|
|
19
|
+
def view_log(
|
|
20
|
+
task_id: int = typer.Argument(..., help="Task ID to view logs for"),
|
|
21
|
+
tail: int = typer.Option(20, "--tail", "-n", help="Number of recent lines to show"),
|
|
22
|
+
follow: bool = typer.Option(False, "--follow", "-f", help="Follow log output in real-time"),
|
|
23
|
+
run_id: Optional[int] = typer.Option(None, "--run-id", help="Specific run ID to view"),
|
|
24
|
+
):
|
|
25
|
+
"""View task logs."""
|
|
26
|
+
db = Database().connect()
|
|
27
|
+
config = get_config()
|
|
28
|
+
|
|
29
|
+
task = db.get_task(task_id)
|
|
30
|
+
if task is None:
|
|
31
|
+
console.print(f"[red]Task {task_id} not found.[/red]")
|
|
32
|
+
raise typer.Exit(1)
|
|
33
|
+
|
|
34
|
+
if run_id:
|
|
35
|
+
# View specific run log
|
|
36
|
+
run = db.get_run(run_id)
|
|
37
|
+
if run is None or run["task_id"] != task_id:
|
|
38
|
+
console.print(f"[red]Run {run_id} not found for task {task_id}.[/red]")
|
|
39
|
+
raise typer.Exit(1)
|
|
40
|
+
|
|
41
|
+
_display_run_log(run, config, tail)
|
|
42
|
+
else:
|
|
43
|
+
# View recent runs
|
|
44
|
+
runs = db.get_recent_runs(task_id, limit=10)
|
|
45
|
+
if not runs:
|
|
46
|
+
console.print(f"[yellow]No runs found for task '{task['name']}'.[/yellow]")
|
|
47
|
+
db.close()
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
# Show recent runs table
|
|
51
|
+
table = Table(title=f"Recent Runs for '{task['name']}'", show_lines=True)
|
|
52
|
+
table.add_column("Run ID", style="cyan", width=7)
|
|
53
|
+
table.add_column("Start Time", style="white", width=20)
|
|
54
|
+
table.add_column("Status", style="yellow", width=10)
|
|
55
|
+
table.add_column("Duration", style="green", width=10)
|
|
56
|
+
table.add_column("Log File", style="blue", width=40)
|
|
57
|
+
|
|
58
|
+
for r in runs:
|
|
59
|
+
duration_s = f"{r['duration'] / 1000:.1f}s" if r.get("duration") else "-"
|
|
60
|
+
status_color = {
|
|
61
|
+
"success": "green",
|
|
62
|
+
"failed": "red",
|
|
63
|
+
"timeout": "red",
|
|
64
|
+
"running": "yellow",
|
|
65
|
+
"killed": "red",
|
|
66
|
+
}.get(r.get("status", ""), "white")
|
|
67
|
+
table.add_row(
|
|
68
|
+
str(r["id"]),
|
|
69
|
+
r.get("start_time", "-")[:19],
|
|
70
|
+
f"[{status_color}]{r.get('status', '-')}[/{status_color}]",
|
|
71
|
+
duration_s,
|
|
72
|
+
r.get("log_file", "-"),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
console.print(table)
|
|
76
|
+
|
|
77
|
+
# Show the latest run's log
|
|
78
|
+
latest = runs[0]
|
|
79
|
+
if latest.get("log_file") and Path(latest["log_file"]).exists():
|
|
80
|
+
console.print(f"\n[bold]Latest log (Run #{latest['id']}):[/bold]")
|
|
81
|
+
_display_run_log(latest, config, tail, follow)
|
|
82
|
+
|
|
83
|
+
db.close()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _display_run_log(
|
|
87
|
+
run: dict,
|
|
88
|
+
config,
|
|
89
|
+
tail: int = 20,
|
|
90
|
+
follow: bool = False,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Display a run's log content."""
|
|
93
|
+
log_file = run.get("log_file")
|
|
94
|
+
if log_file and Path(log_file).exists():
|
|
95
|
+
path = Path(log_file)
|
|
96
|
+
if follow:
|
|
97
|
+
_follow_file(path)
|
|
98
|
+
else:
|
|
99
|
+
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
100
|
+
display_lines = lines[-tail:] if len(lines) > tail else lines
|
|
101
|
+
for line in display_lines:
|
|
102
|
+
console.print(line)
|
|
103
|
+
else:
|
|
104
|
+
# Fallback to database summaries
|
|
105
|
+
if run.get("stdout_summary"):
|
|
106
|
+
console.print("[bold]STDOUT:[/bold]")
|
|
107
|
+
console.print(run["stdout_summary"][:tail * 100])
|
|
108
|
+
if run.get("stderr_summary"):
|
|
109
|
+
console.print("[bold red]STDERR:[/bold red]")
|
|
110
|
+
console.print(run["stderr_summary"][:tail * 100])
|
|
111
|
+
if not run.get("stdout_summary") and not run.get("stderr_summary"):
|
|
112
|
+
console.print("[yellow]No log content available.[/yellow]")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _follow_file(path: Path) -> None:
|
|
116
|
+
"""Follow a log file in real-time (like tail -f)."""
|
|
117
|
+
try:
|
|
118
|
+
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
119
|
+
# Seek to end
|
|
120
|
+
f.seek(0, 2)
|
|
121
|
+
while True:
|
|
122
|
+
line = f.readline()
|
|
123
|
+
if line:
|
|
124
|
+
console.print(line, end="")
|
|
125
|
+
else:
|
|
126
|
+
time.sleep(0.5)
|
|
127
|
+
except KeyboardInterrupt:
|
|
128
|
+
pass
|
taskwatch/cli/main.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""TaskWatch CLI main application (Typer)."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
app = typer.Typer(
|
|
6
|
+
name="taskwatch",
|
|
7
|
+
help="TaskWatch - A lightweight local task scheduler with CLI, web UI, and email reporting.",
|
|
8
|
+
no_args_is_help=True,
|
|
9
|
+
add_completion=False,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.callback()
|
|
14
|
+
def main():
|
|
15
|
+
"""TaskWatch - Lightweight task scheduler and monitor."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command(name="init")
|
|
20
|
+
def init_cmd():
|
|
21
|
+
"""Initialize TaskWatch: create directories, database, and config file."""
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from taskwatch.utils.config import _get_default_dir, Config
|
|
24
|
+
from taskwatch.core.models import Database
|
|
25
|
+
from taskwatch.utils.logger import get_logger
|
|
26
|
+
|
|
27
|
+
logger = get_logger()
|
|
28
|
+
|
|
29
|
+
# Resolve the working directory
|
|
30
|
+
work_dir = _get_default_dir()
|
|
31
|
+
|
|
32
|
+
# Create directory
|
|
33
|
+
work_dir.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
logger.info(f"Created directory: {work_dir}")
|
|
35
|
+
|
|
36
|
+
# Create log directory
|
|
37
|
+
log_dir = work_dir / "logs"
|
|
38
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
logger.info(f"Created log directory: {log_dir}")
|
|
40
|
+
|
|
41
|
+
# Initialize database
|
|
42
|
+
db_path = work_dir / "taskwatch.db"
|
|
43
|
+
db = Database(db_path)
|
|
44
|
+
db.connect()
|
|
45
|
+
db.initialize()
|
|
46
|
+
db.close()
|
|
47
|
+
logger.info(f"Initialized database: {db_path}")
|
|
48
|
+
|
|
49
|
+
# Create default config
|
|
50
|
+
config_path = work_dir / "config.toml"
|
|
51
|
+
if not config_path.exists():
|
|
52
|
+
config = Config(config_path)
|
|
53
|
+
config.load()
|
|
54
|
+
config.save()
|
|
55
|
+
logger.info(f"Created config file: {config_path}")
|
|
56
|
+
else:
|
|
57
|
+
logger.info(f"Config file already exists: {config_path}")
|
|
58
|
+
|
|
59
|
+
# Interactive SMTP setup
|
|
60
|
+
smtp_setup = typer.confirm("Would you like to configure SMTP for email alerts?", default=False)
|
|
61
|
+
if smtp_setup:
|
|
62
|
+
config = Config(config_path).load()
|
|
63
|
+
|
|
64
|
+
host = typer.prompt("SMTP server host", default="smtp.gmail.com")
|
|
65
|
+
port = typer.prompt("SMTP server port", default=587, type=int)
|
|
66
|
+
username = typer.prompt("SMTP username (email address)")
|
|
67
|
+
password = typer.prompt("SMTP password", hide_input=True)
|
|
68
|
+
from_addr = typer.prompt("From email address", default=username)
|
|
69
|
+
to_addrs_str = typer.prompt("Recipient email addresses (comma-separated)")
|
|
70
|
+
|
|
71
|
+
config.set("smtp", "enabled", value=True)
|
|
72
|
+
config.set("smtp", "host", value=host)
|
|
73
|
+
config.set("smtp", "port", value=port)
|
|
74
|
+
config.set("smtp", "username", value=username)
|
|
75
|
+
config.set("smtp", "password", value=password)
|
|
76
|
+
config.set("smtp", "from_addr", value=from_addr)
|
|
77
|
+
config.set("smtp", "to_addrs", value=[a.strip() for a in to_addrs_str.split(",")])
|
|
78
|
+
config.save()
|
|
79
|
+
|
|
80
|
+
logger.info("SMTP configuration saved")
|
|
81
|
+
|
|
82
|
+
# Test email
|
|
83
|
+
test_mail = typer.confirm("Send a test email to verify configuration?", default=False)
|
|
84
|
+
if test_mail:
|
|
85
|
+
from taskwatch.core.notifier import Notifier
|
|
86
|
+
notifier = Notifier()
|
|
87
|
+
if notifier.send_test_email():
|
|
88
|
+
typer.echo("Test email sent successfully!")
|
|
89
|
+
else:
|
|
90
|
+
typer.echo("Failed to send test email. Please check your configuration.", err=True)
|
|
91
|
+
|
|
92
|
+
typer.echo("\nTaskWatch initialized successfully!")
|
|
93
|
+
typer.echo(f" Data directory: {work_dir}")
|
|
94
|
+
typer.echo(f" Config file: {config_path}")
|
|
95
|
+
typer.echo(f" Database: {db_path}")
|
|
96
|
+
typer.echo("\nNext steps:")
|
|
97
|
+
typer.echo(" tw task add --name 'my_task' --command 'echo hello' --schedule '0 9 * * *'")
|
|
98
|
+
typer.echo(" tw run")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@app.command(name="config")
|
|
102
|
+
def config_cmd(
|
|
103
|
+
key: str = typer.Argument(None, help="Config key (e.g., 'smtp.host')"),
|
|
104
|
+
value: str = typer.Argument(None, help="Config value to set"),
|
|
105
|
+
list_all: bool = typer.Option(False, "--list", "-l", help="List all configuration"),
|
|
106
|
+
):
|
|
107
|
+
"""View or modify TaskWatch configuration."""
|
|
108
|
+
from taskwatch.utils.config import get_config, reload_config
|
|
109
|
+
|
|
110
|
+
config = reload_config()
|
|
111
|
+
|
|
112
|
+
if list_all or key is None:
|
|
113
|
+
import json
|
|
114
|
+
typer.echo(json.dumps(config.data, indent=2, ensure_ascii=False))
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
keys = key.split(".")
|
|
118
|
+
if value is None:
|
|
119
|
+
# Get value
|
|
120
|
+
val = config.get(*keys)
|
|
121
|
+
if val is None:
|
|
122
|
+
typer.echo(f"Key '{key}' not found", err=True)
|
|
123
|
+
raise typer.Exit(1)
|
|
124
|
+
typer.echo(f"{key} = {val}")
|
|
125
|
+
else:
|
|
126
|
+
# Set value
|
|
127
|
+
parsed_value = value
|
|
128
|
+
if value.lower() in ("true", "false"):
|
|
129
|
+
parsed_value = value.lower() == "true"
|
|
130
|
+
elif value.isdigit():
|
|
131
|
+
parsed_value = int(value)
|
|
132
|
+
else:
|
|
133
|
+
try:
|
|
134
|
+
parsed_value = float(value)
|
|
135
|
+
except ValueError:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
config.set(*keys, value=parsed_value)
|
|
139
|
+
config.save()
|
|
140
|
+
typer.echo(f"Set {key} = {parsed_value}")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@app.command(name="mail")
|
|
144
|
+
def mail_cmd(
|
|
145
|
+
action: str = typer.Argument(..., help="Mail action: 'test'"),
|
|
146
|
+
):
|
|
147
|
+
"""Mail-related commands. Use 'mail test' to test SMTP configuration."""
|
|
148
|
+
if action == "test":
|
|
149
|
+
from taskwatch.core.notifier import Notifier
|
|
150
|
+
notifier = Notifier()
|
|
151
|
+
if not notifier.is_configured:
|
|
152
|
+
typer.echo("SMTP is not configured. Run 'tw init' or 'tw config' to set up SMTP.", err=True)
|
|
153
|
+
raise typer.Exit(1)
|
|
154
|
+
if notifier.send_test_email():
|
|
155
|
+
typer.echo("Test email sent successfully!")
|
|
156
|
+
else:
|
|
157
|
+
typer.echo("Failed to send test email.", err=True)
|
|
158
|
+
raise typer.Exit(1)
|
|
159
|
+
else:
|
|
160
|
+
typer.echo(f"Unknown mail action: {action}. Use 'mail test'.", err=True)
|
|
161
|
+
raise typer.Exit(1)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@app.command(name="web")
|
|
165
|
+
def web_cmd(
|
|
166
|
+
host: str = typer.Option("127.0.0.1", "--host", help="Host to bind to"),
|
|
167
|
+
port: int = typer.Option(8899, "--port", "-p", help="Port to listen on"),
|
|
168
|
+
no_browser: bool = typer.Option(False, "--no-browser", help="Don't auto-open browser"),
|
|
169
|
+
):
|
|
170
|
+
"""Start the TaskWatch web management interface."""
|
|
171
|
+
from taskwatch.utils.config import get_config
|
|
172
|
+
from taskwatch.utils.logger import get_logger
|
|
173
|
+
|
|
174
|
+
logger = get_logger()
|
|
175
|
+
|
|
176
|
+
config = get_config()
|
|
177
|
+
actual_host = host or config.web_host
|
|
178
|
+
actual_port = port or config.web_port
|
|
179
|
+
|
|
180
|
+
logger.info(f"Starting TaskWatch web server on {actual_host}:{actual_port}")
|
|
181
|
+
|
|
182
|
+
# Auto-open browser
|
|
183
|
+
open_browser = not no_browser and config.web_auto_open_browser
|
|
184
|
+
if open_browser:
|
|
185
|
+
import webbrowser
|
|
186
|
+
import threading
|
|
187
|
+
|
|
188
|
+
def _open():
|
|
189
|
+
import time
|
|
190
|
+
time.sleep(1.5)
|
|
191
|
+
webbrowser.open(f"http://{actual_host}:{actual_port}")
|
|
192
|
+
|
|
193
|
+
threading.Thread(target=_open, daemon=True).start()
|
|
194
|
+
|
|
195
|
+
# Start uvicorn
|
|
196
|
+
import uvicorn
|
|
197
|
+
uvicorn.run(
|
|
198
|
+
"taskwatch.web.app:create_app",
|
|
199
|
+
host=actual_host,
|
|
200
|
+
port=actual_port,
|
|
201
|
+
factory=True,
|
|
202
|
+
log_level="info",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# Register sub-commands from other modules
|
|
207
|
+
from taskwatch.cli.task import task_app
|
|
208
|
+
from taskwatch.cli.run import run_app
|
|
209
|
+
from taskwatch.cli.log import log_app
|
|
210
|
+
from taskwatch.cli.report import report_app
|
|
211
|
+
|
|
212
|
+
app.add_typer(task_app, name="task", help="Task management commands")
|
|
213
|
+
app.add_typer(run_app, name="run", help="Run the scheduler")
|
|
214
|
+
app.add_typer(log_app, name="log", help="View task logs")
|
|
215
|
+
app.add_typer(report_app, name="report", help="Generate reports")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
app()
|
taskwatch/cli/report.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Report commands for TaskWatch CLI."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
report_app = typer.Typer(help="Report generation commands", no_args_is_help=True)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@report_app.command("daily")
|
|
9
|
+
def generate_daily(
|
|
10
|
+
date: str = typer.Argument(None, help="Date in YYYY-MM-DD format (default: today)"),
|
|
11
|
+
send: bool = typer.Option(False, "--send", "-s", help="Send report via email after generation"),
|
|
12
|
+
):
|
|
13
|
+
"""Generate a daily report."""
|
|
14
|
+
from taskwatch.core.models import Database
|
|
15
|
+
from taskwatch.core.reporter import Reporter
|
|
16
|
+
from taskwatch.core.notifier import Notifier
|
|
17
|
+
from taskwatch.utils.logger import get_logger
|
|
18
|
+
|
|
19
|
+
logger = get_logger()
|
|
20
|
+
|
|
21
|
+
db = Database().connect()
|
|
22
|
+
reporter = Reporter(db)
|
|
23
|
+
result = reporter.generate_daily_report(date)
|
|
24
|
+
|
|
25
|
+
if result is None:
|
|
26
|
+
db.close()
|
|
27
|
+
typer.echo("No data available for daily report.")
|
|
28
|
+
raise typer.Exit(1)
|
|
29
|
+
|
|
30
|
+
typer.echo(f"Daily report generated (ID: {result['report_id']})")
|
|
31
|
+
typer.echo(f" Date: {result['date']}")
|
|
32
|
+
typer.echo(f" Total: {result['total']}, Success: {result['success_count']}, Fail: {result['fail_count']}")
|
|
33
|
+
typer.echo(f" Success rate: {result['success_rate']}%")
|
|
34
|
+
|
|
35
|
+
if send:
|
|
36
|
+
notifier = Notifier(db)
|
|
37
|
+
if not notifier.is_configured:
|
|
38
|
+
db.close()
|
|
39
|
+
typer.echo("SMTP not configured, cannot send report.", err=True)
|
|
40
|
+
raise typer.Exit(1)
|
|
41
|
+
|
|
42
|
+
from datetime import datetime
|
|
43
|
+
html = result["html_content"]
|
|
44
|
+
subject = f"[TaskWatch] 日报 - {result['date']}"
|
|
45
|
+
recipients = notifier._get_recipients()
|
|
46
|
+
success = notifier._send_email(recipients, subject, html) if recipients else False
|
|
47
|
+
|
|
48
|
+
db.update_report(result["report_id"], {
|
|
49
|
+
"sent_status": "success" if success else "failed",
|
|
50
|
+
"sent_at": datetime.now().isoformat(),
|
|
51
|
+
})
|
|
52
|
+
typer.echo(f" Email sent: {'OK' if success else 'FAILED'}")
|
|
53
|
+
|
|
54
|
+
db.close()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@report_app.command("weekly")
|
|
58
|
+
def generate_weekly(
|
|
59
|
+
start_date: str = typer.Argument(None, help="Start date in YYYY-MM-DD format"),
|
|
60
|
+
end_date: str = typer.Argument(None, help="End date in YYYY-MM-DD format"),
|
|
61
|
+
send: bool = typer.Option(False, "--send", "-s", help="Send report via email after generation"),
|
|
62
|
+
):
|
|
63
|
+
"""Generate a weekly report."""
|
|
64
|
+
from taskwatch.core.models import Database
|
|
65
|
+
from taskwatch.core.reporter import Reporter
|
|
66
|
+
from taskwatch.core.notifier import Notifier
|
|
67
|
+
from taskwatch.utils.logger import get_logger
|
|
68
|
+
|
|
69
|
+
logger = get_logger()
|
|
70
|
+
|
|
71
|
+
db = Database().connect()
|
|
72
|
+
reporter = Reporter(db)
|
|
73
|
+
result = reporter.generate_weekly_report(start_date, end_date)
|
|
74
|
+
|
|
75
|
+
if result is None:
|
|
76
|
+
db.close()
|
|
77
|
+
typer.echo("No data available for weekly report.")
|
|
78
|
+
raise typer.Exit(1)
|
|
79
|
+
|
|
80
|
+
typer.echo(f"Weekly report generated (ID: {result['report_id']})")
|
|
81
|
+
typer.echo(f" Period: {result['start_date']} ~ {result['end_date']}")
|
|
82
|
+
typer.echo(f" Total: {result['total']}, Success: {result['success_count']}")
|
|
83
|
+
typer.echo(f" Success rate: {result['success_rate']}%")
|
|
84
|
+
|
|
85
|
+
if send:
|
|
86
|
+
notifier = Notifier(db)
|
|
87
|
+
if not notifier.is_configured:
|
|
88
|
+
db.close()
|
|
89
|
+
typer.echo("SMTP not configured, cannot send report.", err=True)
|
|
90
|
+
raise typer.Exit(1)
|
|
91
|
+
|
|
92
|
+
from datetime import datetime
|
|
93
|
+
html = result["html_content"]
|
|
94
|
+
subject = f"[TaskWatch] 周报 - {result['start_date']} ~ {result['end_date']}"
|
|
95
|
+
recipients = notifier._get_recipients()
|
|
96
|
+
success = notifier._send_email(recipients, subject, html) if recipients else False
|
|
97
|
+
|
|
98
|
+
db.update_report(result["report_id"], {
|
|
99
|
+
"sent_status": "success" if success else "failed",
|
|
100
|
+
"sent_at": datetime.now().isoformat(),
|
|
101
|
+
})
|
|
102
|
+
typer.echo(f" Email sent: {'OK' if success else 'FAILED'}")
|
|
103
|
+
|
|
104
|
+
db.close()
|
taskwatch/cli/run.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Run command for TaskWatch CLI - starts the scheduler."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
run_app = typer.Typer(help="Scheduler commands", no_args_is_help=True)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@run_app.callback(invoke_without_command=True)
|
|
9
|
+
def run_scheduler(
|
|
10
|
+
daemon: bool = typer.Option(False, "--daemon", "-d", help="Run as background daemon (Unix-like only)"),
|
|
11
|
+
):
|
|
12
|
+
"""Start the task scheduler."""
|
|
13
|
+
from taskwatch.core.scheduler import TaskScheduler
|
|
14
|
+
from taskwatch.utils.logger import get_logger
|
|
15
|
+
|
|
16
|
+
logger = get_logger()
|
|
17
|
+
|
|
18
|
+
scheduler = TaskScheduler()
|
|
19
|
+
|
|
20
|
+
if daemon:
|
|
21
|
+
scheduler.run_daemon()
|
|
22
|
+
else:
|
|
23
|
+
scheduler.run_forever()
|