From cae33176af1ed655ddb114aeda324053de20d3e8 Mon Sep 17 00:00:00 2001 From: Ching L Date: Thu, 11 Sep 2025 16:04:17 +0800 Subject: [PATCH] feat: add web interface for task management - Added full-featured web UI with drag-and-drop task sorting - Implemented CRUD API endpoints for task management - Added task reordering endpoint with priority support - Created responsive HTML interface with inline styles - Updated Docker configuration for web service deployment - Added requirements.txt for dependency management - Enhanced README with web interface documentation - Made Google Calendar integration optional --- .dockerignore | 50 ++++ .env.example | 11 + Dockerfile | 21 +- README.md | 128 ++++++++- app.py | 96 ++++++- docker-compose.yml | 21 +- requirements.txt | 6 + templates/index.html | 599 +++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 901 insertions(+), 31 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 requirements.txt create mode 100644 templates/index.html diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7549079 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,50 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +ENV/ +.venv + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Git +.git/ +.gitignore + +# Documentation +README.md +*.md + +# Docker +Dockerfile +docker-compose.yml +.dockerignore + +# Environment +.env +.env.example + +# Database (will be mounted as volume) +*.db +data/ + +# Credentials (will be mounted as volume) +credentials.json + +# Temporary files +*.log +*.tmp +.DS_Store + +# Test files +test_* +*_test.py \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..fd9ee66 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# API Configuration +API_KEY=your-secure-api-key-here + +# Google Calendar Configuration +CALENDAR_ID=primary + +# Port Configuration (optional) +# PORT=5000 + +# Database Path (optional, defaults to /app/data/tasks.db in container) +# TASK_DB=/app/data/tasks.db \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index fc86876..2e0d7f2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,25 @@ -# 使用自定义 Docker Registry 中的官方 Python 镜像作为基础镜像 +# 使用自定义 Docker Registry 中已安装依赖的镜像 FROM git.tunpok.com/ching/python-env:latest # 设置工作目录 WORKDIR /app -# 将当前目录下的所有文件复制到容器中 -COPY . . +# 复制应用代码和模板 +COPY app.py . +COPY templates/ templates/ -# port number +# 创建数据目录 +RUN mkdir -p /app/data + +# 设置环境变量默认值 +ENV PORT=5000 +ENV API_KEY=change-me +ENV TASK_DB=/app/data/tasks.db +ENV GOOGLE_CREDENTIALS_FILE=/app/credentials.json +ENV CALENDAR_ID=primary + +# 暴露端口 EXPOSE 5000 -# run flask app +# 运行 Flask 应用 CMD ["python", "app.py"] \ No newline at end of file diff --git a/README.md b/README.md index f7e5f50..53089f9 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,37 @@ # Calendar Widget -一个基于 Flask 的任务管理系统,集成 Google Calendar 日程安排功能,并配套 iOS Scriptable 小组件展示。 +一个功能完整的任务管理系统,提供 Web 界面、RESTful API、Google Calendar 集成,以及 iOS Scriptable 小组件支持。 + +## 快速开始 + +```bash +# 1. 克隆项目 +git clone +cd calendar-widget + +# 2. 启动服务 +docker-compose up -d + +# 3. 访问 Web 界面 +open http://localhost:5000 +``` ## 功能特性 +- 🌐 **Web 管理界面**: 美观的任务管理页面,支持增删改查和拖拽排序 - 📋 **任务管理**: 基于最小执行间隔的任务追踪系统 - 📅 **Google Calendar 集成**: 自动将任务同步到 Google 日历 - 🚦 **智能状态显示**: 根据任务执行时间自动显示红黄绿三色状态 - 📱 **iOS Widget 支持**: 通过 Scriptable 在 iOS 桌面显示任务状态 - 🔄 **自动刷新**: Widget 每分钟自动更新状态 -- 💾 **离线缓存**: API 不可用时自动使用本地缓存 +- 💾 **数据持久化**: SQLite 数据库存储,支持 Docker 卷挂载 +- 🎯 **拖拽排序**: Web 界面支持任务拖拽重新排序 ## 系统架构 -### 后端 API (Flask) -- **app.py**: 主应用程序,提供 RESTful API +### 后端服务 (Flask) +- **app.py**: 主应用程序,提供 RESTful API 和 Web 界面 +- **templates/**: HTML 模板目录,包含任务管理界面 - **数据库**: SQLite 存储任务信息 - **认证**: 基于 API Key 的请求认证 - **时区**: 使用亚洲/上海时区 (北京时间) @@ -29,6 +46,16 @@ - **CI/CD**: Drone CI 自动构建和部署 - **服务编排**: Docker Compose 管理服务 +## 访问入口 + +### Web 管理界面 +直接访问 `http://your-server:5000/` 即可使用完整的任务管理界面,功能包括: +- 创建新任务(设置名称和最小间隔天数) +- 编辑现有任务 +- 删除任务 +- 拖拽排序任务 +- 执行任务(添加到 Google Calendar) + ## API 接口 ### 获取任务列表 @@ -43,11 +70,56 @@ Headers: X-API-Key: { "id": 1, "name": "任务名称", - "color": "green|yellow|red" + "color": "green|yellow|red", + "min_interval_days": 7, + "last_execution_time": "2025-01-01T12:00:00+08:00", + "priority": 1 } ] ``` +### 创建任务 +``` +POST /tasks +Headers: X-API-Key: +Content-Type: application/json + +{ + "name": "任务名称", + "min_interval_days": 7, + "priority": 0 +} +``` + +### 更新任务 +``` +PUT /tasks/ +Headers: X-API-Key: +Content-Type: application/json + +{ + "name": "新任务名称", + "min_interval_days": 5 +} +``` + +### 删除任务 +``` +DELETE /tasks/ +Headers: X-API-Key: +``` + +### 重新排序任务 +``` +POST /tasks/reorder +Headers: X-API-Key: +Content-Type: application/json + +{ + "task_ids": [3, 1, 2] // 按优先级从高到低排列的任务ID +} +``` + ### 创建日程 ``` POST /tasks//schedule @@ -68,16 +140,34 @@ Headers: X-API-Key: ### Docker 部署 -1. 使用 Docker Compose: +1. 克隆项目: +```bash +git clone +cd calendar-widget +``` + +2. 配置环境变量: +```bash +cp .env.example .env +# 编辑 .env 文件,设置 API_KEY +``` + +3. 使用 Docker Compose 启动: ```bash docker-compose up -d ``` -2. 配置要求: - - 挂载 SQLite 数据库文件: `./tasks.db:/app/tasks.db` - - 挂载 Google 凭证文件: `:/app/credentials.json` +4. 数据持久化: + - 任务数据库自动保存到: `./data/tasks.db` + - Google 凭证文件(可选): `./credentials.json` -### Google Calendar 配置 +5. 访问服务: + - Web 界面: `http://localhost:5000/` + - API 端点: `http://localhost:5000/tasks` + +### Google Calendar 配置(可选) + +如果需要将任务同步到 Google Calendar: 1. 创建 Google Cloud 项目 2. 启用 Google Calendar API @@ -85,6 +175,8 @@ docker-compose up -d 4. 将凭证文件放置在项目目录,命名为 `credentials.json` 5. 将服务账号邮箱添加到目标日历的共享用户 +注意:即使没有配置 Google Calendar,任务管理功能仍可正常使用 + ### iOS Widget 配置 1. 在 iPhone 上安装 [Scriptable](https://scriptable.app/) 应用 @@ -104,12 +196,13 @@ docker-compose up -d ## 技术栈 -- **后端**: Python 3.9+, Flask, Peewee ORM +- **后端**: Python 3.10+, Flask, Peewee ORM +- **前端**: HTML5, CSS3, JavaScript (原生) - **数据库**: SQLite -- **容器**: Docker +- **容器**: Docker, Docker Compose - **CI/CD**: Drone CI -- **前端**: JavaScript (Scriptable iOS) -- **集成**: Google Calendar API +- **移动端**: JavaScript (Scriptable iOS) +- **集成**: Google Calendar API (可选) ## 开发环境 @@ -120,9 +213,16 @@ pip install flask peewee google-api-python-client google-auth google-auth-httpli ### 本地运行 ```bash +# 设置环境变量 +export API_KEY=your-api-key +export PORT=5000 + +# 运行应用 python app.py ``` +然后访问 `http://localhost:5000/` 使用 Web 界面 + ## License MIT diff --git a/app.py b/app.py index b54b449..13b5700 100644 --- a/app.py +++ b/app.py @@ -21,7 +21,7 @@ import datetime as dt from functools import wraps from zoneinfo import ZoneInfo # Python 3.9+ standard timezone database -from flask import Flask, jsonify, request, abort +from flask import Flask, jsonify, request, abort, render_template from peewee import ( Model, SqliteDatabase, AutoField, CharField, IntegerField, DateTimeField, ) @@ -65,10 +65,14 @@ with db: # --------------------------------------------------------------------------- # Google Calendar client # --------------------------------------------------------------------------- -creds = service_account.Credentials.from_service_account_file( - CREDENTIALS_FILE, scopes=SCOPES -) -calendar_service = build("calendar", "v3", credentials=creds, cache_discovery=False) +try: + creds = service_account.Credentials.from_service_account_file( + CREDENTIALS_FILE, scopes=SCOPES + ) + calendar_service = build("calendar", "v3", credentials=creds, cache_discovery=False) +except FileNotFoundError: + print(f"Warning: Google credentials file '{CREDENTIALS_FILE}' not found. Calendar integration disabled.") + calendar_service = None # --------------------------------------------------------------------------- # Flask app + helpers @@ -117,6 +121,9 @@ def compute_color(task: Task, now: dt.datetime) -> str: # --------------------------------------------------------------------------- # Routes # --------------------------------------------------------------------------- +@app.route("/") +def index(): + return render_template("index.html", api_key=API_KEY) @app.route("/tasks", methods=["GET"]) @require_api_key def list_tasks(): @@ -129,17 +136,96 @@ def list_tasks(): "id": t.id, "name": t.name, "color": compute_color(t, now), + "min_interval_days": t.min_interval_days, + "last_execution_time": _to_datetime(t.last_execution_time).isoformat() if t.last_execution_time else None, + "priority": t.priority, } for t in tasks ] return jsonify(response) +@app.route("/tasks", methods=["POST"]) +@require_api_key +def create_task(): + data = request.json + if not data or "name" not in data: + abort(400, description="Task name is required") + + task = Task.create( + name=data["name"], + min_interval_days=data.get("min_interval_days", 7), + priority=data.get("priority", 0) + ) + + return jsonify({ + "id": task.id, + "name": task.name, + "min_interval_days": task.min_interval_days, + "priority": task.priority, + }), 201 + +@app.route("/tasks/", methods=["PUT"]) +@require_api_key +def update_task(task_id: int): + task = Task.get_or_none(Task.id == task_id) + if not task: + abort(404, description="Task not found") + + data = request.json + if "name" in data: + task.name = data["name"] + if "min_interval_days" in data: + task.min_interval_days = data["min_interval_days"] + if "priority" in data: + task.priority = data["priority"] + + task.save() + + return jsonify({ + "id": task.id, + "name": task.name, + "min_interval_days": task.min_interval_days, + "priority": task.priority, + }) + +@app.route("/tasks/", methods=["DELETE"]) +@require_api_key +def delete_task(task_id: int): + task = Task.get_or_none(Task.id == task_id) + if not task: + abort(404, description="Task not found") + + task.delete_instance() + return jsonify({"success": True}) + +@app.route("/tasks/reorder", methods=["POST"]) +@require_api_key +def reorder_tasks(): + data = request.json + if not data or "task_ids" not in data: + abort(400, description="task_ids array is required") + + task_ids = data["task_ids"] + priority = len(task_ids) + + for task_id in task_ids: + task = Task.get_or_none(Task.id == task_id) + if task: + task.priority = priority + task.save() + priority -= 1 + + return jsonify({"success": True}) + @app.route("/tasks//schedule", methods=["POST"]) @require_api_key def schedule_task(task_id: int): task = Task.get_or_none(Task.id == task_id) if not task: abort(404, description="Task not found") + + if calendar_service is None: + abort(503, description="Calendar service not available") # Event time is now (Asia/Shanghai) start_dt = dt.datetime.now(TZ).replace(microsecond=0) diff --git a/docker-compose.yml b/docker-compose.yml index 2dd600c..57262ae 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,21 @@ +version: '3.8' + services: calendar-widget-api: + build: . image: git.tunpok.com/ching/calendar-widget-api:latest container_name: calendar-widget-api - restart: always + restart: unless-stopped environment: - - API_KEY=change-me - - CALENDAR_ID=primary + - PORT=5000 + - API_KEY=${API_KEY:-change-me} + - TASK_DB=/app/data/tasks.db + - GOOGLE_CREDENTIALS_FILE=/app/credentials.json + - CALENDAR_ID=${CALENDAR_ID:-primary} ports: - - 5000:5000 - + - "5000:5000" volumes: - - ./tasks.db:/app/tasks.db - - /Users/ching/Nutstore\ Files/我的坚果云/calendar-bot-tunpok-ade3b1a46d71.json:/app/credentials.json + # 数据持久化 + - ./data:/app/data + # Google Calendar 凭证文件(可选,如果存在) + - ./credentials.json:/app/credentials.json:ro diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..731d2dc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +flask==3.0.0 +peewee==3.17.0 +google-api-python-client==2.111.0 +google-auth==2.25.2 +google-auth-httplib2==0.2.0 +google-auth-oauthlib==1.2.0 \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..74a8210 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,599 @@ + + + + + + 任务管理 + + + +
+
+

📋 任务管理系统

+

拖拽排序 · 实时同步 · 智能提醒

+
+ +
+

新增任务

+
+
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+ + + +

暂无任务,点击上方添加第一个任务

+
+
+
+ + + + + + \ No newline at end of file