mrstack 1.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.
- mrstack/__init__.py +4 -0
- mrstack/_data/config/com.mrstack.claude-telegram.plist +25 -0
- mrstack/_data/config/mcp-config.example.json +23 -0
- mrstack/_data/config/start-daemon.sh +53 -0
- mrstack/_data/config/start.sh +29 -0
- mrstack/_data/schedulers/manage-jobs.sh +87 -0
- mrstack/_data/schedulers/morning-briefing.sh +29 -0
- mrstack/_data/schedulers/register-jobs.py +182 -0
- mrstack/_data/schedulers/run-threads-briefing.sh +36 -0
- mrstack/_data/schedulers/weekly-review.sh +26 -0
- mrstack/_data/templates/DESIGN-GUIDE.md +160 -0
- mrstack/_data/templates/alert.md +56 -0
- mrstack/_data/templates/evening-summary.md +73 -0
- mrstack/_data/templates/jarvis-alert.md +64 -0
- mrstack/_data/templates/morning-briefing.md +53 -0
- mrstack/_data/templates/weekly-review.md +79 -0
- mrstack/_overlay/api/dashboard.py +223 -0
- mrstack/_overlay/api/templates/dashboard.html +328 -0
- mrstack/_overlay/bot/handlers/callback.py +1432 -0
- mrstack/_overlay/bot/handlers/command.py +1541 -0
- mrstack/_overlay/bot/utils/keyboards.py +125 -0
- mrstack/_overlay/bot/utils/ui_components.py +166 -0
- mrstack/_overlay/claude/session.py +341 -0
- mrstack/_overlay/jarvis/__init__.py +77 -0
- mrstack/_overlay/jarvis/coach.py +122 -0
- mrstack/_overlay/jarvis/context_engine.py +463 -0
- mrstack/_overlay/jarvis/pattern_learner.py +255 -0
- mrstack/_overlay/jarvis/persona.py +84 -0
- mrstack/_overlay/jarvis/platform.py +182 -0
- mrstack/_overlay/knowledge/__init__.py +6 -0
- mrstack/_overlay/knowledge/manager.py +464 -0
- mrstack/_overlay/knowledge/memory_index.py +180 -0
- mrstack/cli.py +330 -0
- mrstack/constants.py +77 -0
- mrstack/daemon.py +325 -0
- mrstack/patcher.py +169 -0
- mrstack/wizard.py +271 -0
- mrstack-1.1.0.dist-info/METADATA +640 -0
- mrstack-1.1.0.dist-info/RECORD +42 -0
- mrstack-1.1.0.dist-info/WHEEL +4 -0
- mrstack-1.1.0.dist-info/entry_points.txt +2 -0
- mrstack-1.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Alert Template
|
|
2
|
+
|
|
3
|
+
## 형식
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
━━━ Mr.Stack 알림 ━━━
|
|
7
|
+
{emoji} {alert_type}
|
|
8
|
+
|
|
9
|
+
▸ 내용
|
|
10
|
+
{description}
|
|
11
|
+
|
|
12
|
+
▸ 발생 시각
|
|
13
|
+
{timestamp}
|
|
14
|
+
|
|
15
|
+
▸ 영향
|
|
16
|
+
{impact}
|
|
17
|
+
|
|
18
|
+
▸ 권장 조치
|
|
19
|
+
· {action_1}
|
|
20
|
+
· {action_2}
|
|
21
|
+
━━━━━━━━━━━━━━━━━━━━
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 텔레그램 HTML 변환
|
|
25
|
+
|
|
26
|
+
```html
|
|
27
|
+
<b>━━━ Mr.Stack 알림 ━━━</b>
|
|
28
|
+
{emoji} <b>{alert_type}</b>
|
|
29
|
+
|
|
30
|
+
<b>▸ 내용</b>
|
|
31
|
+
{description}
|
|
32
|
+
|
|
33
|
+
<b>▸ 발생 시각</b>
|
|
34
|
+
{timestamp}
|
|
35
|
+
|
|
36
|
+
<b>▸ 영향</b>
|
|
37
|
+
{impact}
|
|
38
|
+
|
|
39
|
+
<b>▸ 권장 조치</b>
|
|
40
|
+
· {action_1}
|
|
41
|
+
· {action_2}
|
|
42
|
+
━━━━━━━━━━━━━━━━━━━━
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 알림 등급
|
|
46
|
+
- 🔴 <b>긴급</b>: 즉시 조치 필요 (서비스 장애, 보안 이슈)
|
|
47
|
+
- 🟡 <b>주의</b>: 확인 필요 (성능 저하, 이상 패턴)
|
|
48
|
+
- 🟢 <b>정보</b>: 참고용 (작업 완료, 상태 변경)
|
|
49
|
+
- ⚪ <b>시스템</b>: 내부 상태 변경 (토큰 갱신, 동기화 완료)
|
|
50
|
+
|
|
51
|
+
## 규칙
|
|
52
|
+
- 간결하게: 최대 500자
|
|
53
|
+
- 결론 먼저, 상세 나중
|
|
54
|
+
- 조치 가능한 내용 반드시 포함
|
|
55
|
+
- 구분선/섹션 prefix는 디자인 가이드 기준 준수
|
|
56
|
+
- 한국어로 작성
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Evening Summary Template
|
|
2
|
+
|
|
3
|
+
## 형식
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
━━━ Mr.Stack 데일리 리포트 ━━━
|
|
7
|
+
📊 {date} ({weekday})
|
|
8
|
+
|
|
9
|
+
▸ 생산성 스코어: {score_bar} {score}/10
|
|
10
|
+
|
|
11
|
+
▸ 오늘의 하이라이트
|
|
12
|
+
✓ {completed_1} ({detail_1})
|
|
13
|
+
✓ {completed_2}
|
|
14
|
+
|
|
15
|
+
▸ 미완료
|
|
16
|
+
◻ {pending_1} → 내일 이어서
|
|
17
|
+
|
|
18
|
+
▸ 시간 분석
|
|
19
|
+
코딩: {coding_bar} {coding_hours}h
|
|
20
|
+
리뷰: {review_bar} {review_hours}h
|
|
21
|
+
회의: {meeting_bar} {meeting_hours}h
|
|
22
|
+
기타: {other_bar} {other_hours}h
|
|
23
|
+
|
|
24
|
+
▸ 코칭 노트
|
|
25
|
+
잘한 점: {strength}
|
|
26
|
+
개선점: {improvement}
|
|
27
|
+
내일 제안: {suggestion}
|
|
28
|
+
━━━━━━━━━━━━━━━━━━━━
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 텔레그램 HTML 변환
|
|
32
|
+
|
|
33
|
+
```html
|
|
34
|
+
<b>━━━ Mr.Stack 데일리 리포트 ━━━</b>
|
|
35
|
+
📊 {date} ({weekday})
|
|
36
|
+
|
|
37
|
+
<b>▸ 생산성 스코어:</b> {score_bar} {score}/10
|
|
38
|
+
|
|
39
|
+
<b>▸ 오늘의 하이라이트</b>
|
|
40
|
+
✓ {completed_1} ({detail_1})
|
|
41
|
+
✓ {completed_2}
|
|
42
|
+
|
|
43
|
+
<b>▸ 미완료</b>
|
|
44
|
+
◻ {pending_1} → 내일 이어서
|
|
45
|
+
|
|
46
|
+
<b>▸ 시간 분석</b>
|
|
47
|
+
코딩: {coding_bar} {coding_hours}h
|
|
48
|
+
리뷰: {review_bar} {review_hours}h
|
|
49
|
+
회의: {meeting_bar} {meeting_hours}h
|
|
50
|
+
기타: {other_bar} {other_hours}h
|
|
51
|
+
|
|
52
|
+
<b>▸ 코칭 노트</b>
|
|
53
|
+
잘한 점: {strength}
|
|
54
|
+
개선점: {improvement}
|
|
55
|
+
<i>내일 제안: {suggestion}</i>
|
|
56
|
+
━━━━━━━━━━━━━━━━━━━━
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 프로그레스 바 생성 규칙
|
|
60
|
+
- 생산성 스코어: 10칸, score 값만큼 █, 나머지 ░
|
|
61
|
+
- 예: 8/10 → ████████░░
|
|
62
|
+
- 시간 분석: 10칸, (hours / total_hours * 10) 반올림
|
|
63
|
+
- 예: 4.2h / 8h → ██████░░░░
|
|
64
|
+
|
|
65
|
+
## 규칙
|
|
66
|
+
- 하이라이트: 완료된 항목만, 커밋 수 등 수치 포함
|
|
67
|
+
- 미완료: 다음 날 이어서 할 항목 최대 3개
|
|
68
|
+
- 시간 분석: Jarvis ContextEngine 데이터 기반 (osascript 앱 사용 시간)
|
|
69
|
+
- 코칭 노트: PatternLearner의 일일 분석 결과 활용
|
|
70
|
+
- 이 리포트의 "내일 제안"이 다음 날 morning-briefing의 코칭 라인으로 연결됨
|
|
71
|
+
- 전체 길이: 최대 1500자
|
|
72
|
+
- 한국어로 작성
|
|
73
|
+
- 일일 메모리 저장: ~/claude-telegram/memory/daily/{date}.md 에도 기록
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Jarvis Alert Template
|
|
2
|
+
|
|
3
|
+
## 형식
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
[Mr.Stack] {message}
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## 알림 유형별 예시
|
|
10
|
+
|
|
11
|
+
### 배터리/하드웨어
|
|
12
|
+
```
|
|
13
|
+
[Mr.Stack] 배터리 {pct}% — 저장하시고 충전하세요.
|
|
14
|
+
[Mr.Stack] 디스크 {pct}% 사용 — 정리 필요합니다.
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### 집중/휴식
|
|
18
|
+
```
|
|
19
|
+
[Mr.Stack] {hours}시간째 코딩 중 — 5분 쉬어가시죠.
|
|
20
|
+
[Mr.Stack] 딥워크 {hours}시간 달성. 훌륭합니다.
|
|
21
|
+
[Mr.Stack] 컨텍스트 전환 {count}회 감지 — 한 가지에 집중해보세요.
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 복귀/상태 변화
|
|
25
|
+
```
|
|
26
|
+
[Mr.Stack] 돌아오셨네요. {branch} 브랜치, 변경 {count}개 uncommitted.
|
|
27
|
+
[Mr.Stack] {minutes}분째 자리 비움 — 회의 중이시면 무시하세요.
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 스케줄
|
|
31
|
+
```
|
|
32
|
+
[Mr.Stack] {minutes}분 후 {event_title} 시작입니다.
|
|
33
|
+
[Mr.Stack] {event_title} 5분 전 — 준비하세요.
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 시스템
|
|
37
|
+
```
|
|
38
|
+
[Mr.Stack] 토큰 갱신 완료. 다음 만료: {expiry_date}.
|
|
39
|
+
[Mr.Stack] 메모리 동기화 완료 ({count}개 항목 업데이트).
|
|
40
|
+
[Mr.Stack] GitHub: {repo}에 새 이슈 #{number}.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 텔레그램 HTML 변환
|
|
44
|
+
|
|
45
|
+
```html
|
|
46
|
+
<b>[Mr.Stack]</b> {message}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
필요시 강조:
|
|
50
|
+
```html
|
|
51
|
+
<b>[Mr.Stack]</b> 배터리 <b>{pct}%</b> — 저장하시고 충전하세요.
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 규칙
|
|
55
|
+
- prefix는 반드시 [Mr.Stack]으로 시작
|
|
56
|
+
- 한 줄로 끝내는 것이 원칙 (최대 100자)
|
|
57
|
+
- 존댓말 사용하되 간결하게
|
|
58
|
+
- 조치가 필요하면 "—" 뒤에 구체적 행동 제안
|
|
59
|
+
- 정보 전달만이면 사실만 기술
|
|
60
|
+
- 불필요한 이모지 사용 금지 (prefix의 [Mr.Stack]이 브랜딩 역할)
|
|
61
|
+
- Jarvis ContextEngine 트리거에 의해 자동 발송
|
|
62
|
+
- 긴급도에 따라 텔레그램 알림음 설정:
|
|
63
|
+
- 긴급 (배터리 < 10%, 서비스 장애): disable_notification=false
|
|
64
|
+
- 일반: disable_notification=true
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Morning Briefing Template
|
|
2
|
+
|
|
3
|
+
## 형식
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
━━━ Mr.Stack 모닝 브리핑 ━━━
|
|
7
|
+
📅 {date} ({weekday})
|
|
8
|
+
|
|
9
|
+
▸ 오늘 일정
|
|
10
|
+
{time} {title} ({location})
|
|
11
|
+
{time} {title} ({location})
|
|
12
|
+
— 일정 없음
|
|
13
|
+
|
|
14
|
+
▸ Pending Tasks
|
|
15
|
+
◻ {task_1}
|
|
16
|
+
◻ {task_2}
|
|
17
|
+
|
|
18
|
+
▸ AI/Tech 뉴스
|
|
19
|
+
· {headline_1} — {summary_1}
|
|
20
|
+
· {headline_2} — {summary_2}
|
|
21
|
+
|
|
22
|
+
💡 어제 코칭: "{coaching_quote}"
|
|
23
|
+
━━━━━━━━━━━━━━━━━━━━
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 텔레그램 HTML 변환
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<b>━━━ Mr.Stack 모닝 브리핑 ━━━</b>
|
|
30
|
+
📅 {date} ({weekday})
|
|
31
|
+
|
|
32
|
+
<b>▸ 오늘 일정</b>
|
|
33
|
+
{time} {title} ({location})
|
|
34
|
+
|
|
35
|
+
<b>▸ Pending Tasks</b>
|
|
36
|
+
◻ {task}
|
|
37
|
+
|
|
38
|
+
<b>▸ AI/Tech 뉴스</b>
|
|
39
|
+
· {headline} — {summary}
|
|
40
|
+
|
|
41
|
+
💡 <i>어제 코칭: "{coaching_quote}"</i>
|
|
42
|
+
━━━━━━━━━━━━━━━━━━━━
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 규칙
|
|
46
|
+
- 전체 길이: 최대 1500자
|
|
47
|
+
- 일정이 없으면 "— 일정 없음" 한 줄로
|
|
48
|
+
- 뉴스는 최대 3개, 1줄 요약
|
|
49
|
+
- Pending Tasks 최대 5개
|
|
50
|
+
- 코칭 라인은 어제 evening-summary의 코칭 노트에서 핵심 1줄 발췌
|
|
51
|
+
- 구분선: ━ (U+2501) 사용, 상단 20자 / 하단 20자
|
|
52
|
+
- 섹션 prefix: ▸ (U+25B8)
|
|
53
|
+
- 한국어로 작성
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Weekly Review Template
|
|
2
|
+
|
|
3
|
+
## 형식
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
━━━ Mr.Stack 주간 회고 ━━━
|
|
7
|
+
📊 {start_date} ~ {end_date}
|
|
8
|
+
|
|
9
|
+
▸ 주간 통계
|
|
10
|
+
총 대화: {total_conversations}회 | 커밋: {total_commits}개
|
|
11
|
+
가장 생산적: {best_day} ({best_day_count}회)
|
|
12
|
+
피크 시간: {peak_hours}
|
|
13
|
+
|
|
14
|
+
▸ 프로젝트별
|
|
15
|
+
{project_1} {bar_1} {pct_1}%
|
|
16
|
+
{project_2} {bar_2} {pct_2}%
|
|
17
|
+
{project_3} {bar_3} {pct_3}%
|
|
18
|
+
|
|
19
|
+
▸ 이번 주 잘한 점
|
|
20
|
+
· {highlight_1}
|
|
21
|
+
· {highlight_2}
|
|
22
|
+
|
|
23
|
+
▸ 주요 결정사항
|
|
24
|
+
· {decision_1}: {reason_1}
|
|
25
|
+
|
|
26
|
+
▸ 배운 것
|
|
27
|
+
· {learning_1}
|
|
28
|
+
|
|
29
|
+
▸ 다음 주 추천
|
|
30
|
+
· {recommendation_1}
|
|
31
|
+
· {recommendation_2}
|
|
32
|
+
|
|
33
|
+
🏷️ 키워드: {keyword_1}, {keyword_2}, {keyword_3}
|
|
34
|
+
━━━━━━━━━━━━━━━━━━━━
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 텔레그램 HTML 변환
|
|
38
|
+
|
|
39
|
+
```html
|
|
40
|
+
<b>━━━ Mr.Stack 주간 회고 ━━━</b>
|
|
41
|
+
📊 {start_date} ~ {end_date}
|
|
42
|
+
|
|
43
|
+
<b>▸ 주간 통계</b>
|
|
44
|
+
총 대화: {total_conversations}회 | 커밋: {total_commits}개
|
|
45
|
+
가장 생산적: {best_day} ({best_day_count}회)
|
|
46
|
+
피크 시간: {peak_hours}
|
|
47
|
+
|
|
48
|
+
<b>▸ 프로젝트별</b>
|
|
49
|
+
{project_1} {bar_1} {pct_1}%
|
|
50
|
+
{project_2} {bar_2} {pct_2}%
|
|
51
|
+
|
|
52
|
+
<b>▸ 이번 주 잘한 점</b>
|
|
53
|
+
· {highlight}
|
|
54
|
+
|
|
55
|
+
<b>▸ 주요 결정사항</b>
|
|
56
|
+
· {decision}: {reason}
|
|
57
|
+
|
|
58
|
+
<b>▸ 배운 것</b>
|
|
59
|
+
· {learning}
|
|
60
|
+
|
|
61
|
+
<b>▸ 다음 주 추천</b>
|
|
62
|
+
· {recommendation}
|
|
63
|
+
|
|
64
|
+
🏷️ <i>키워드: {keyword_1}, {keyword_2}, {keyword_3}</i>
|
|
65
|
+
━━━━━━━━━━━━━━━━━━━━
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 프로그레스 바 생성 규칙
|
|
69
|
+
- 10칸 기준: █ (채워진) + ░ (빈)
|
|
70
|
+
- 퍼센트를 10으로 나눠서 반올림한 개수만큼 █
|
|
71
|
+
- 예: 40% → ████░░░░░░
|
|
72
|
+
|
|
73
|
+
## 규칙
|
|
74
|
+
- ~/claude-telegram/memory/daily/ 파일 기반으로 작성
|
|
75
|
+
- 성과는 구체적 결과물 중심 (커밋 수, 완료 태스크 수 포함)
|
|
76
|
+
- 결정사항은 "왜"를 반드시 포함
|
|
77
|
+
- 다음 주 추천은 데이터 기반 (피크 시간, 생산적 패턴 분석)
|
|
78
|
+
- 전체 길이: 최대 2000자
|
|
79
|
+
- 한국어로 작성
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Dashboard Mini App API for Telegram Web App.
|
|
2
|
+
|
|
3
|
+
Serves a single-page dashboard and JSON APIs for:
|
|
4
|
+
- Usage statistics
|
|
5
|
+
- Scheduled jobs (list, toggle, run)
|
|
6
|
+
- Memory status
|
|
7
|
+
- Recent activity timeline
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
import structlog
|
|
16
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
17
|
+
from fastapi.responses import HTMLResponse
|
|
18
|
+
|
|
19
|
+
logger = structlog.get_logger()
|
|
20
|
+
|
|
21
|
+
dashboard_router = APIRouter(prefix="/dashboard", tags=["dashboard"])
|
|
22
|
+
|
|
23
|
+
# Path to dashboard HTML template
|
|
24
|
+
_TEMPLATE_DIR = Path(__file__).parent / "templates"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dashboard_router.get("/", response_class=HTMLResponse)
|
|
28
|
+
async def serve_dashboard() -> HTMLResponse:
|
|
29
|
+
"""Serve the dashboard HTML."""
|
|
30
|
+
template_path = _TEMPLATE_DIR / "dashboard.html"
|
|
31
|
+
if not template_path.exists():
|
|
32
|
+
raise HTTPException(status_code=404, detail="Dashboard template not found")
|
|
33
|
+
return HTMLResponse(content=template_path.read_text(encoding="utf-8"))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dashboard_router.get("/api/stats")
|
|
37
|
+
async def get_stats(request: Request) -> Dict[str, Any]:
|
|
38
|
+
"""Return usage statistics."""
|
|
39
|
+
db_manager = request.app.state.db_manager
|
|
40
|
+
if not db_manager:
|
|
41
|
+
return {"error": "Database not available"}
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
async with db_manager.get_connection() as conn:
|
|
45
|
+
# Total interactions today
|
|
46
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
47
|
+
cursor = await conn.execute(
|
|
48
|
+
"SELECT COUNT(*) FROM claude_interactions "
|
|
49
|
+
"WHERE created_at >= ?",
|
|
50
|
+
(today,),
|
|
51
|
+
)
|
|
52
|
+
row = await cursor.fetchone()
|
|
53
|
+
today_count = row[0] if row else 0
|
|
54
|
+
|
|
55
|
+
# Total sessions
|
|
56
|
+
cursor = await conn.execute(
|
|
57
|
+
"SELECT COUNT(DISTINCT session_id) FROM claude_interactions"
|
|
58
|
+
)
|
|
59
|
+
row = await cursor.fetchone()
|
|
60
|
+
total_sessions = row[0] if row else 0
|
|
61
|
+
|
|
62
|
+
# Active jobs count
|
|
63
|
+
cursor = await conn.execute(
|
|
64
|
+
"SELECT COUNT(*) FROM scheduled_jobs WHERE is_active = 1"
|
|
65
|
+
)
|
|
66
|
+
row = await cursor.fetchone()
|
|
67
|
+
active_jobs = row[0] if row else 0
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
"today_interactions": today_count,
|
|
71
|
+
"total_sessions": total_sessions,
|
|
72
|
+
"active_jobs": active_jobs,
|
|
73
|
+
"timestamp": datetime.now().isoformat(),
|
|
74
|
+
}
|
|
75
|
+
except Exception as e:
|
|
76
|
+
logger.error("Failed to get stats", error=str(e))
|
|
77
|
+
return {"error": str(e)}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dashboard_router.get("/api/jobs")
|
|
81
|
+
async def get_jobs(request: Request) -> List[Dict[str, Any]]:
|
|
82
|
+
"""Return scheduled jobs list."""
|
|
83
|
+
db_manager = request.app.state.db_manager
|
|
84
|
+
if not db_manager:
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
async with db_manager.get_connection() as conn:
|
|
89
|
+
cursor = await conn.execute(
|
|
90
|
+
"SELECT job_name, cron_expression, is_active, "
|
|
91
|
+
"last_run_at, next_run_at "
|
|
92
|
+
"FROM scheduled_jobs ORDER BY job_name"
|
|
93
|
+
)
|
|
94
|
+
rows = await cursor.fetchall()
|
|
95
|
+
|
|
96
|
+
return [
|
|
97
|
+
{
|
|
98
|
+
"name": row[0],
|
|
99
|
+
"cron": row[1],
|
|
100
|
+
"active": bool(row[2]),
|
|
101
|
+
"last_run": row[3],
|
|
102
|
+
"next_run": row[4],
|
|
103
|
+
}
|
|
104
|
+
for row in rows
|
|
105
|
+
]
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.error("Failed to get jobs", error=str(e))
|
|
108
|
+
return []
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dashboard_router.post("/api/jobs/{name}/toggle")
|
|
112
|
+
async def toggle_job(name: str, request: Request) -> Dict[str, Any]:
|
|
113
|
+
"""Toggle a scheduled job on/off."""
|
|
114
|
+
db_manager = request.app.state.db_manager
|
|
115
|
+
if not db_manager:
|
|
116
|
+
raise HTTPException(status_code=500, detail="Database not available")
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
async with db_manager.get_connection() as conn:
|
|
120
|
+
cursor = await conn.execute(
|
|
121
|
+
"SELECT is_active FROM scheduled_jobs WHERE job_name = ?",
|
|
122
|
+
(name,),
|
|
123
|
+
)
|
|
124
|
+
row = await cursor.fetchone()
|
|
125
|
+
if not row:
|
|
126
|
+
raise HTTPException(status_code=404, detail=f"Job not found: {name}")
|
|
127
|
+
|
|
128
|
+
new_state = not bool(row[0])
|
|
129
|
+
await conn.execute(
|
|
130
|
+
"UPDATE scheduled_jobs SET is_active = ? WHERE job_name = ?",
|
|
131
|
+
(int(new_state), name),
|
|
132
|
+
)
|
|
133
|
+
await conn.commit()
|
|
134
|
+
|
|
135
|
+
return {"name": name, "active": new_state}
|
|
136
|
+
except HTTPException:
|
|
137
|
+
raise
|
|
138
|
+
except Exception as e:
|
|
139
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dashboard_router.post("/api/jobs/{name}/run")
|
|
143
|
+
async def run_job(name: str, request: Request) -> Dict[str, Any]:
|
|
144
|
+
"""Trigger immediate job execution."""
|
|
145
|
+
event_bus = getattr(request.app.state, "event_bus", None)
|
|
146
|
+
if not event_bus:
|
|
147
|
+
raise HTTPException(status_code=500, detail="Event bus not available")
|
|
148
|
+
|
|
149
|
+
from ..events.types import ScheduledEvent
|
|
150
|
+
|
|
151
|
+
settings = getattr(request.app.state, "settings", None)
|
|
152
|
+
working_dir = Path(".")
|
|
153
|
+
chat_ids: list = []
|
|
154
|
+
if settings:
|
|
155
|
+
working_dir = settings.approved_directory
|
|
156
|
+
chat_ids = settings.notification_chat_ids or []
|
|
157
|
+
|
|
158
|
+
event = ScheduledEvent(
|
|
159
|
+
job_name=name,
|
|
160
|
+
working_directory=working_dir,
|
|
161
|
+
target_chat_ids=chat_ids,
|
|
162
|
+
)
|
|
163
|
+
await event_bus.publish(event)
|
|
164
|
+
|
|
165
|
+
return {"name": name, "status": "triggered"}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dashboard_router.get("/api/memory")
|
|
169
|
+
async def get_memory(request: Request) -> Dict[str, Any]:
|
|
170
|
+
"""Return memory system status."""
|
|
171
|
+
memory_dir = Path.home() / "claude-telegram" / "memory"
|
|
172
|
+
if not memory_dir.exists():
|
|
173
|
+
return {"status": "not_configured", "files": []}
|
|
174
|
+
|
|
175
|
+
files = []
|
|
176
|
+
for p in sorted(memory_dir.rglob("*.md")):
|
|
177
|
+
rel = p.relative_to(memory_dir)
|
|
178
|
+
files.append({
|
|
179
|
+
"path": str(rel),
|
|
180
|
+
"size": p.stat().st_size,
|
|
181
|
+
"modified": datetime.fromtimestamp(p.stat().st_mtime).isoformat(),
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
patterns_file = memory_dir / "patterns" / "interactions.jsonl"
|
|
185
|
+
interaction_count = 0
|
|
186
|
+
if patterns_file.exists():
|
|
187
|
+
interaction_count = sum(1 for _ in patterns_file.open())
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
"status": "active",
|
|
191
|
+
"file_count": len(files),
|
|
192
|
+
"files": files[:20],
|
|
193
|
+
"interaction_count": interaction_count,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dashboard_router.get("/api/activity")
|
|
198
|
+
async def get_activity(request: Request) -> List[Dict[str, Any]]:
|
|
199
|
+
"""Return recent interaction timeline."""
|
|
200
|
+
db_manager = request.app.state.db_manager
|
|
201
|
+
if not db_manager:
|
|
202
|
+
return []
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
async with db_manager.get_connection() as conn:
|
|
206
|
+
cursor = await conn.execute(
|
|
207
|
+
"SELECT prompt, session_id, created_at "
|
|
208
|
+
"FROM claude_interactions "
|
|
209
|
+
"ORDER BY created_at DESC LIMIT 20"
|
|
210
|
+
)
|
|
211
|
+
rows = await cursor.fetchall()
|
|
212
|
+
|
|
213
|
+
return [
|
|
214
|
+
{
|
|
215
|
+
"prompt": row[0][:100] if row[0] else "",
|
|
216
|
+
"session_id": row[1][:8] if row[1] else "",
|
|
217
|
+
"timestamp": row[2],
|
|
218
|
+
}
|
|
219
|
+
for row in rows
|
|
220
|
+
]
|
|
221
|
+
except Exception as e:
|
|
222
|
+
logger.error("Failed to get activity", error=str(e))
|
|
223
|
+
return []
|