Compare commits

...
7 Commits
Author SHA1 Message Date
steven 33f4ce42e4 Merge branch 'master' of github.com:oventh/TaskPulse 2026-06-20 12:09:19 +08:00
steven 4215137f26 fix: dayjs UTC import, API pagination, execution list endpoint 2026-06-20 12:09:00 +08:00
oventhandGitHub 78df16f770 Update README.en.md 2026-06-19 16:09:38 +08:00
steven 4cdccf904a Merge branch 'master' of github.com:oventh/TaskPulse 2026-06-19 16:07:29 +08:00
oventhandGitHub e8f931b4a1 Update README.md 2026-06-18 00:38:20 +08:00
oventhandGitHub c54cc82612 Update README.md 2026-06-17 20:38:17 +08:00
oventhandGitHub 56dfa5b418 Update README.md 2026-06-17 20:37:04 +08:00
6 changed files with 108 additions and 15 deletions
-1
View File
@@ -6,7 +6,6 @@
TaskPulse is a tracking and monitoring platform designed for AI agents' scheduled tasks. Unlike traditional CRUD-heavy admin panels, TaskPulse is **AI-native**: give your agents an API spec, and they autonomously register themselves, report execution results, and update their status — no manual form-filling required. TaskPulse is a tracking and monitoring platform designed for AI agents' scheduled tasks. Unlike traditional CRUD-heavy admin panels, TaskPulse is **AI-native**: give your agents an API spec, and they autonomously register themselves, report execution results, and update their status — no manual form-filling required.
**Live Demo**: https://task.pags.cn (login: `admin` / `admin123`)
[📖 中文文档](./README.md) [📖 中文文档](./README.md)
+2 -2
View File
@@ -5,8 +5,8 @@
> **让 AI Agent 自己汇报工作,你只需要打开看板看一眼。** > **让 AI Agent 自己汇报工作,你只需要打开看板看一眼。**
TaskPulse 是一个面向 AI Agent 的定时任务跟踪与监控平台。它不是传统的"你去填表创建任务"的后台管理系统,而是**给 AI Agent 一份 API 说明书,让 Agent 自主注册、自动汇报**——你只需要在统一的 Dashboard 上看全局状态。 TaskPulse 是一个面向 AI Agent 的定时任务跟踪与监控平台。它不是传统的"你去填表创建任务"的后台管理系统,而是**给 AI Agent 一份 API 说明书,让 Agent 自主注册、自动汇报**——你只需要在统一的 Dashboard 上看全局状态。
## 系统截图
**线上 Demo**: https://task.pags.cn (账号: `admin` / `admin123` <img src="https://pub-44c5bd2a850e4bc7aab6f5f8701493fd.r2.dev/images/111.png"></img>
[🌐 English](./README.en.md) [🌐 English](./README.en.md)
+26 -5
View File
@@ -1,15 +1,25 @@
"""API router — TaskExecution (reporting + query).""" """API router — TaskExecution (reporting + query)."""
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db from app.database import get_db
from app.schemas import ExecutionOut, ExecutionReport from app.schemas import ExecutionOut, ExecutionReport
from app.services import ExecutionService, TaskService, AgentService from app.services import ExecutionService, TaskService, AgentService
from app.models import TaskExecution
router = APIRouter(prefix="/api/tasks/{task_id}/executions", tags=["executions"]) router = APIRouter(prefix="/api/tasks/{task_id}/executions", tags=["executions"])
class PaginatedExecutions(BaseModel):
items: list[ExecutionOut]
total: int
page: int
page_size: int
@router.post("", response_model=ExecutionOut, status_code=201) @router.post("", response_model=ExecutionOut, status_code=201)
async def report_execution(task_id: int, body: ExecutionReport, async def report_execution(task_id: int, body: ExecutionReport,
db: AsyncSession = Depends(get_db)): db: AsyncSession = Depends(get_db)):
@@ -46,16 +56,27 @@ async def report_execution(task_id: int, body: ExecutionReport,
return ExecutionOut(**record.__dict__) return ExecutionOut(**record.__dict__)
@router.get("", response_model=list[ExecutionOut]) @router.get("", response_model=PaginatedExecutions)
async def list_executions( async def list_executions(
task_id: int, task_id: int,
limit: int = Query(50, ge=1, le=200), page: int = Query(1, ge=1),
offset: int = Query(0, ge=0), page_size: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
exec_svc = ExecutionService(db) exec_svc = ExecutionService(db)
records = await exec_svc.get_executions(task_id, limit=limit, offset=offset) offset = (page - 1) * page_size
return [ExecutionOut(**r.__dict__) for r in records] records = await exec_svc.get_executions(task_id, limit=page_size, offset=offset)
# Get total count
count_stmt = select(func.count()).select_from(TaskExecution).where(TaskExecution.task_id == task_id)
total = (await db.execute(count_stmt)).scalar() or 0
return PaginatedExecutions(
items=[ExecutionOut(**r.__dict__) for r in records],
total=total,
page=page,
page_size=page_size,
)
@router.get("/recent", response_model=list[ExecutionOut]) @router.get("/recent", response_model=list[ExecutionOut])
+1 -1
View File
@@ -42,7 +42,7 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { beijing } from '../utils/time.js' import dayjs from 'dayjs'
import { listAlerts, acknowledgeAlert } from '../api/index.js' import { listAlerts, acknowledgeAlert } from '../api/index.js'
const alerts = ref([]) const alerts = ref([])
+1 -1
View File
@@ -152,7 +152,7 @@
<script setup> <script setup>
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed } from 'vue'
import { beijing } from '../utils/time.js' import dayjs from 'dayjs'
import { getDashboardSummary, getDashboardTasks, getSystemConfig } from '../api/index.js' import { getDashboardSummary, getDashboardTasks, getSystemConfig } from '../api/index.js'
const summary = ref({ const summary = ref({
+78 -5
View File
@@ -72,6 +72,20 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<!-- Pagination -->
<div class="pagination" v-if="total > pageSize && totalPages > 1">
<span class="page-info"> {{ total }} </span>
<div class="page-btns">
<button class="page-btn" :disabled="currentPage <= 1" @click="goPage(currentPage - 1)">上一页</button>
<template v-for="p in (totalPages || 1)" :key="p">
<button v-if="Math.abs(p - currentPage) <= 2 || p === 1 || p === totalPages"
class="page-btn" :class="{ active: p === currentPage }"
@click="goPage(p)">{{ p }}</button>
<span v-else-if="p === currentPage - 3 || p === currentPage + 3" class="page-ellipsis"></span>
</template>
<button class="page-btn" :disabled="currentPage >= totalPages" @click="goPage(currentPage + 1)">下一页</button>
</div>
</div>
</div> </div>
<!-- Log Dialog --> <!-- Log Dialog -->
@@ -90,9 +104,9 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted, computed } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { beijing } from '../utils/time.js' import dayjs from 'dayjs'
import { getTask, listExecutions } from '../api/index.js' import { getTask, listExecutions } from '../api/index.js'
const route = useRoute() const route = useRoute()
@@ -103,14 +117,37 @@ const logVisible = ref(false)
const currentLog = ref('') const currentLog = ref('')
const logExecution = ref(null) const logExecution = ref(null)
// Pagination
const currentPage = ref(1)
const pageSize = ref(20)
const total = ref(0)
const totalPages = computed(() => Math.ceil(total.value / pageSize.value) || 1)
const loadExecutions = async (page) => {
try {
const res = await listExecutions(taskId, { page, page_size: pageSize.value })
const data = res.data || {}
executions.value = data.items || []
total.value = data.total || 0
currentPage.value = data.page || page
} catch (e) {
console.error(e)
executions.value = []
}
}
const goPage = (p) => {
if (p < 1 || p > totalPages.value) return
loadExecutions(p)
}
onMounted(async () => { onMounted(async () => {
try { try {
const [taskRes, execRes] = await Promise.all([ const [taskRes] = await Promise.all([
getTask(taskId), getTask(taskId),
listExecutions(taskId, { limit: 100 }),
]) ])
task.value = taskRes.data task.value = taskRes.data
executions.value = execRes.data await loadExecutions(1)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
@@ -243,4 +280,40 @@ const showLog = (row) => {
word-break: break-all; word-break: break-all;
margin: 0; margin: 0;
} }
/* ── Pagination ──────────────────────────── */
.pagination {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
border-top: 1px solid var(--border-color);
}
.page-info {
font-size: 12px;
color: var(--text-muted);
}
.page-btns {
display: flex;
align-items: center;
gap: 4px;
}
.page-btn {
padding: 4px 10px;
border-radius: 6px;
border: 1px solid var(--border-color);
background: transparent;
color: var(--text-secondary);
font-size: 12px;
cursor: pointer;
transition: all 0.15s;
}
.page-btn:hover { border-color: var(--accent-1); color: var(--accent-1); }
.page-btn.active {
background: var(--accent-1);
border-color: var(--accent-1);
color: #fff;
}
.page-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.page-ellipsis { color: var(--text-muted); font-size: 12px; padding: 0 2px; }
</style> </style>