mirror of
https://github.com/R0m1k3/TodoFlow.git
synced 2026-10-11 17:28:17 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e759a1a7d5 | ||
|
|
3b5ede5872 | ||
|
|
a635aa29a4 | ||
|
|
fcac504693 | ||
|
|
27d3fb7d50 | ||
|
|
d9c1720523 | ||
|
|
e4a56f2fc6 | ||
|
|
3e6dbb5c76 | ||
|
|
1ca25917d4 |
No files matched your search
@@ -3,7 +3,9 @@ name: Release Tauri App
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
# Accept lowercase and uppercase tags, for example v1.1.0.0 or V1.1.0.0.
|
||||
- 'v*'
|
||||
- 'V*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -38,20 +40,37 @@ jobs:
|
||||
cd widget
|
||||
npm install
|
||||
|
||||
- name: Update version in tauri.conf.json
|
||||
- name: Update app version from tag
|
||||
shell: bash
|
||||
run: |
|
||||
TAG_VERSION=$(echo "${{ github.ref_name }}" | sed 's/^v//')
|
||||
if [[ ! "$TAG_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
||||
TAG_VERSION="1.0.0"
|
||||
# Keep Tauri/Cargo on valid SemVer while allowing release tags like V1.1.0.0.
|
||||
RAW_VERSION=$(echo "${{ github.ref_name }}" | sed 's/^[vV]//')
|
||||
APP_VERSION="1.1.0"
|
||||
if [[ "$RAW_VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)(\.[0-9]+)?$ ]]; then
|
||||
APP_VERSION="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
echo "Updating tauri.conf.json version to $TAG_VERSION"
|
||||
echo "Updating app version to $APP_VERSION"
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const file = './widget/src-tauri/tauri.conf.json';
|
||||
const config = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
config.package.version = '$TAG_VERSION';
|
||||
fs.writeFileSync(file, JSON.stringify(config, null, 2), 'utf8');
|
||||
|
||||
const updateJson = (file, update) => {
|
||||
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
update(data);
|
||||
fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
||||
};
|
||||
|
||||
updateJson('./widget/package.json', (pkg) => {
|
||||
pkg.version = '$APP_VERSION';
|
||||
});
|
||||
|
||||
updateJson('./widget/src-tauri/tauri.conf.json', (config) => {
|
||||
config.package.version = '$APP_VERSION';
|
||||
});
|
||||
|
||||
const cargoFile = './widget/src-tauri/Cargo.toml';
|
||||
const cargo = fs.readFileSync(cargoFile, 'utf8')
|
||||
.replace(/^version = \".*\"$/m, 'version = \"$APP_VERSION\"');
|
||||
fs.writeFileSync(cargoFile, cargo, 'utf8');
|
||||
"
|
||||
|
||||
- name: Build the Tauri app
|
||||
@@ -61,7 +80,10 @@ jobs:
|
||||
with:
|
||||
tagName: ${{ github.ref_name }}
|
||||
releaseName: 'TodoFlow ${{ github.ref_name }}'
|
||||
releaseBody: 'Release generated automatically by GitHub Actions.'
|
||||
releaseDraft: true
|
||||
releaseBody: |
|
||||
Installeur Windows de TodoFlow ${{ github.ref_name }}.
|
||||
|
||||
Telechargez le fichier `.exe` (NSIS) ou `.msi` ci-dessous pour installer l'application.
|
||||
releaseDraft: false
|
||||
prerelease: false
|
||||
projectPath: './widget'
|
||||
+2
-1
@@ -30,11 +30,12 @@ ENV PYTHONUNBUFFERED=1
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
# Création d'un utilisateur système non-privilégié pour des raisons de sécurité
|
||||
RUN useradd -u 8888 appuser && chown -R appuser:appuser /app
|
||||
RUN chmod +x /app/entrypoint.sh && useradd -u 8888 appuser && chown -R appuser:appuser /app
|
||||
USER appuser
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
# Lancement d'Uvicorn
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
|
||||
+253
-2
@@ -62,7 +62,23 @@ Permet à l'Agent IA d'ajouter un nouvel objectif pour l'utilisateur.
|
||||
```
|
||||
* **Réponse (201 Created) :** Renvoie l'objet créé avec son UUID unique généré par le serveur et son horodatage de création (`created_at`).
|
||||
|
||||
### C. Mettre à jour une tâche (PUT `/api/v1/tasks/{id}`)
|
||||
### C. Récupérer les détails d'une tâche (GET `/api/v1/tasks/{id}`)
|
||||
Permet à l'Agent IA d'obtenir les informations complètes d'une tâche spécifique à partir de son UUID.
|
||||
|
||||
* **Réponse (200 OK) :**
|
||||
```json
|
||||
{
|
||||
"id": "e1a77ff0-2c32-4f37-baef-e68b903f806e",
|
||||
"title": "Acheter du pain",
|
||||
"description": "Prendre une tradition bien cuite",
|
||||
"status": "A faire",
|
||||
"created_at": "2026-05-26T14:15:30Z",
|
||||
"updated_at": "2026-05-26T14:15:30Z"
|
||||
}
|
||||
```
|
||||
* **Réponse (404 Not Found) :** Si aucune tâche ne correspond à l'identifiant fourni.
|
||||
|
||||
### D. Mettre à jour une tâche (PUT `/api/v1/tasks/{id}`)
|
||||
Permet à l'Agent IA de marquer une tâche comme **"En cours"** ou **"Termine"**, ou d'en modifier le titre/description.
|
||||
|
||||
* **Request Body (JSON - Mise à jour partielle acceptée) :**
|
||||
@@ -73,7 +89,7 @@ Permet à l'Agent IA de marquer une tâche comme **"En cours"** ou **"Termine"**
|
||||
```
|
||||
* **Réponse (200 OK) :** Renvoie l'objet mis à jour avec son nouvel horodatage `updated_at`.
|
||||
|
||||
### D. Supprimer une tâche (DELETE `/api/v1/tasks/{id}`)
|
||||
### E. Supprimer une tâche (DELETE `/api/v1/tasks/{id}`)
|
||||
Permet à l'Agent IA de nettoyer la liste en supprimant une tâche.
|
||||
|
||||
* **Réponse (204 No Content) :** Succès de la suppression (aucun corps de réponse).
|
||||
@@ -125,6 +141,241 @@ if create_resp.status_code == 201:
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 5. Définition des Outils pour Agents IA (Function Calling)
|
||||
|
||||
Si vous configurez un agent IA externe utilisant les API d'OpenAI, Anthropic ou Gemini, vous pouvez copier-coller les schémas de fonctions ci-dessous pour lui donner un accès direct et natif à la Todo List.
|
||||
|
||||
### A. Format OpenAI / Gemini (JSON Schema)
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_tasks",
|
||||
"description": "Récupère la liste de toutes les tâches stockées dans TodoFlow. Permet de filtrer optionnellement par statut.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["A faire", "En cours", "Termine"],
|
||||
"description": "Filtre optionnel pour récupérer uniquement les tâches ayant ce statut ('A faire', 'En cours', 'Termine')."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_task",
|
||||
"description": "Récupère les détails précis d'une tâche existante à partir de son UUID.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "L'UUID unique de la tâche à récupérer."
|
||||
}
|
||||
},
|
||||
"required": ["task_id"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "create_task",
|
||||
"description": "Crée une nouvelle tâche dans la Todo list.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 100,
|
||||
"description": "Le titre court et descriptif de la tâche (ex: 'Acheter du pain')."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Description détaillée de la tâche ou consignes supplémentaires."
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["A faire", "En cours", "Termine"],
|
||||
"default": "A faire",
|
||||
"description": "Le statut initial de la tâche (par défaut 'A faire')."
|
||||
}
|
||||
},
|
||||
"required": ["title"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "update_task",
|
||||
"description": "Met à jour une tâche existante (modifier le titre, la description, ou changer de statut comme passer à 'En cours' ou 'Termine').",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "L'UUID unique de la tâche à modifier."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 100,
|
||||
"description": "Nouveau titre pour la tâche."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Nouvelle description pour la tâche."
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["A faire", "En cours", "Termine"],
|
||||
"description": "Nouveau statut pour la tâche."
|
||||
}
|
||||
},
|
||||
"required": ["task_id"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "delete_task",
|
||||
"description": "Supprime définitivement une tâche de la liste à partir de son UUID.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "L'UUID unique de la tâche à supprimer."
|
||||
}
|
||||
},
|
||||
"required": ["task_id"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### B. Format Anthropic / Claude (Messages API Tools)
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "get_tasks",
|
||||
"description": "Récupère la liste de toutes les tâches stockées dans TodoFlow. Permet de filtrer optionnellement par statut.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["A faire", "En cours", "Termine"],
|
||||
"description": "Filtre optionnel pour récupérer uniquement les tâches ayant ce statut."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "get_task",
|
||||
"description": "Récupère les détails précis d'une tâche existante à partir de son UUID.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "L'UUID unique de la tâche à récupérer."
|
||||
}
|
||||
},
|
||||
"required": ["task_id"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "create_task",
|
||||
"description": "Crée une nouvelle tâche dans la Todo list.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 100,
|
||||
"description": "Le titre court et descriptif de la tâche."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Description détaillée de la tâche."
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["A faire", "En cours", "Termine"],
|
||||
"default": "A faire",
|
||||
"description": "Le statut initial de la tâche."
|
||||
}
|
||||
},
|
||||
"required": ["title"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "update_task",
|
||||
"description": "Met à jour une tâche existante par son UUID (modifier le titre, la description, ou changer de statut).",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "L'UUID unique de la tâche à modifier."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 100,
|
||||
"description": "Nouveau titre pour la tâche."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Nouvelle description."
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["A faire", "En cours", "Termine"],
|
||||
"description": "Nouveau statut."
|
||||
}
|
||||
},
|
||||
"required": ["task_id"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "delete_task",
|
||||
"description": "Supprime définitivement une tâche de la liste à partir de son UUID.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "L'UUID unique de la tâche à supprimer."
|
||||
}
|
||||
},
|
||||
"required": ["task_id"]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Conseils pour le Prompt System de l'Agent IA
|
||||
|
||||
Si vous intégrez cette API à un LLM (comme GPT-4 ou Gemini), vous pouvez ajouter cette consigne dans son **System Prompt** :
|
||||
|
||||
+259
-1
@@ -1,10 +1,13 @@
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime
|
||||
import json
|
||||
import os
|
||||
from uuid import UUID
|
||||
from typing import List, Optional
|
||||
from fastapi import FastAPI, HTTPException, Depends, Security, status
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.security.api_key import APIKeyHeader
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.config import settings
|
||||
@@ -40,7 +43,7 @@ async def lifespan(app: FastAPI):
|
||||
app = FastAPI(
|
||||
title="TodoFlow API",
|
||||
description="API centralisée pour la Todo list, connectée au widget de bureau et pilotable par Agent IA.",
|
||||
version="1.0.0",
|
||||
version="1.1.0",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
@@ -53,6 +56,261 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# --- DOCUMENTATION ET REDIRECTIONS ---
|
||||
|
||||
@app.get("/", include_in_schema=False)
|
||||
def redirect_root_to_doc():
|
||||
"""Redirige les requêtes de la racine vers la page de documentation."""
|
||||
return RedirectResponse(url="/doc")
|
||||
|
||||
@app.get("/doc", response_class=HTMLResponse, include_in_schema=False)
|
||||
def get_html_documentation():
|
||||
"""
|
||||
Sert une page HTML interactive et magnifiquement stylisée (Glassmorphism)
|
||||
rendant le guide d'intégration pour Agent IA (README_AI_AGENT.md).
|
||||
"""
|
||||
try:
|
||||
# Chemin relatif vers la racine du serveur (où se trouve le README)
|
||||
readme_path = "README_AI_AGENT.md"
|
||||
if not os.path.exists(readme_path):
|
||||
# Essayer un autre chemin au cas où
|
||||
readme_path = "/app/README_AI_AGENT.md"
|
||||
|
||||
if not os.path.exists(readme_path):
|
||||
raise FileNotFoundError("README_AI_AGENT.md introuvable.")
|
||||
|
||||
with open(readme_path, "r", encoding="utf-8") as f:
|
||||
markdown_content = f.read()
|
||||
|
||||
escaped_content = json.dumps(markdown_content)
|
||||
|
||||
html_template = f"""<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Documentation TodoFlow API</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<style>
|
||||
:root {{
|
||||
--bg-gradient: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%);
|
||||
--glass-bg: rgba(30, 41, 59, 0.75);
|
||||
--glass-border: rgba(255, 255, 255, 0.08);
|
||||
--text-main: #f8fafc;
|
||||
--text-muted: #94a3b8;
|
||||
--primary: #6366f1;
|
||||
--primary-glow: rgba(99, 102, 241, 0.15);
|
||||
--accent: #10b981;
|
||||
}}
|
||||
|
||||
body {{
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: var(--bg-gradient);
|
||||
color: var(--text-main);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}}
|
||||
|
||||
.container {{
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
margin: 40px 20px;
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 24px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}}
|
||||
|
||||
/* Sleek scrollbar */
|
||||
::-webkit-scrollbar {{
|
||||
width: 8px;
|
||||
}}
|
||||
::-webkit-scrollbar-track {{
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
}}
|
||||
::-webkit-scrollbar-thumb {{
|
||||
background: rgba(99, 102, 241, 0.3);
|
||||
border-radius: 4px;
|
||||
}}
|
||||
::-webkit-scrollbar-thumb:hover {{
|
||||
background: rgba(99, 102, 241, 0.5);
|
||||
}}
|
||||
|
||||
/* Markdown styling */
|
||||
.markdown-body h1 {{
|
||||
font-size: 2.2rem;
|
||||
font-weight: 700;
|
||||
margin-top: 0;
|
||||
margin-bottom: 24px;
|
||||
background: linear-gradient(135deg, #fff 0%, #cbd5e1 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding-bottom: 16px;
|
||||
}}
|
||||
|
||||
.markdown-body h2 {{
|
||||
font-size: 1.6rem;
|
||||
font-weight: 600;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 16px;
|
||||
color: #e2e8f0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
padding-bottom: 8px;
|
||||
}}
|
||||
|
||||
.markdown-body h3 {{
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 12px;
|
||||
color: #f1f5f9;
|
||||
}}
|
||||
|
||||
.markdown-body p {{
|
||||
line-height: 1.7;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 16px;
|
||||
}}
|
||||
|
||||
.markdown-body code {{
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.9em;
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
padding: 3px 6px;
|
||||
border-radius: 6px;
|
||||
color: #f43f5e;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}}
|
||||
|
||||
.markdown-body pre {{
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}}
|
||||
|
||||
.markdown-body pre code {{
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: #38bdf8;
|
||||
border: none;
|
||||
font-size: 0.92em;
|
||||
}}
|
||||
|
||||
.markdown-body ul, .markdown-body ol {{
|
||||
margin-bottom: 20px;
|
||||
padding-left: 24px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.7;
|
||||
}}
|
||||
|
||||
.markdown-body li {{
|
||||
margin-bottom: 8px;
|
||||
}}
|
||||
|
||||
.markdown-body a {{
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}}
|
||||
|
||||
.markdown-body a:hover {{
|
||||
color: #818cf8;
|
||||
text-decoration: underline;
|
||||
}}
|
||||
|
||||
.markdown-body blockquote {{
|
||||
border-left: 4px solid var(--primary);
|
||||
background: var(--primary-glow);
|
||||
margin: 20px 0;
|
||||
padding: 16px 20px;
|
||||
border-radius: 0 12px 12px 0;
|
||||
}}
|
||||
|
||||
.markdown-body blockquote p {{
|
||||
margin: 0;
|
||||
color: #cbd5e1;
|
||||
font-style: italic;
|
||||
}}
|
||||
|
||||
.markdown-body hr {{
|
||||
border: 0;
|
||||
height: 1px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
margin: 32px 0;
|
||||
}}
|
||||
|
||||
/* Premium Header Links */
|
||||
.top-links {{
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}}
|
||||
|
||||
.top-link {{
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
color: var(--text-main);
|
||||
padding: 8px 16px;
|
||||
border-radius: 30px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}}
|
||||
|
||||
.top-link:hover {{
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 15px rgba(99, 102, 241, 0.4);
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="top-links">
|
||||
<a href="/docs" class="top-link">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
||||
Swagger UI (API Interactive)
|
||||
</a>
|
||||
<a href="/openapi.json" class="top-link">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
|
||||
OpenAPI Spec (JSON)
|
||||
</a>
|
||||
</div>
|
||||
<div class="markdown-body" id="content">Chargement de la documentation...</div>
|
||||
</div>
|
||||
<script>
|
||||
const markdownText = {escaped_content};
|
||||
document.getElementById('content').innerHTML = marked.parse(markdownText);
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
return HTMLResponse(content=html_template, status_code=200)
|
||||
except Exception as e:
|
||||
return HTMLResponse(
|
||||
content=f"<h1>Erreur</h1><p>Impossible de charger la documentation : {str(e)}</p>",
|
||||
status_code=500
|
||||
)
|
||||
|
||||
# --- ENDPOINTS API ---
|
||||
|
||||
@app.get(
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
DEFAULT_API_KEYS_RAW="widget-token-secure-789,ai-agent-token-secure-101"
|
||||
PERSISTED_ENV_FILE="${PERSISTED_ENV_FILE:-/app/data/.env}"
|
||||
APP_ENV_FILE="${APP_ENV_FILE:-/app/.env}"
|
||||
|
||||
read_env_value() {
|
||||
file="$1"
|
||||
name="$2"
|
||||
|
||||
if [ ! -f "$file" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
sed -n "s/^[[:space:]]*${name}[[:space:]]*=[[:space:]]*//p" "$file" \
|
||||
| tail -n 1 \
|
||||
| sed 's/^"//; s/"$//; s/^'\''//; s/'\''$//'
|
||||
}
|
||||
|
||||
write_env_value() {
|
||||
file="$1"
|
||||
name="$2"
|
||||
value="$3"
|
||||
|
||||
mkdir -p "$(dirname "$file")"
|
||||
|
||||
if [ -f "$file" ] && grep -q "^[[:space:]]*${name}[[:space:]]*=" "$file"; then
|
||||
escaped_value=$(printf '%s' "$value" | sed 's/[\/&]/\\&/g')
|
||||
sed -i "s/^[[:space:]]*${name}[[:space:]]*=.*/${name}=${escaped_value}/" "$file"
|
||||
else
|
||||
printf '%s=%s\n' "$name" "$value" >> "$file"
|
||||
fi
|
||||
}
|
||||
|
||||
persisted_api_keys="$(read_env_value "$PERSISTED_ENV_FILE" "API_KEYS_RAW" || true)"
|
||||
app_env_api_keys="$(read_env_value "$APP_ENV_FILE" "API_KEYS_RAW" || true)"
|
||||
current_api_keys="${API_KEYS_RAW:-}"
|
||||
|
||||
if [ -n "$persisted_api_keys" ] && { [ -z "$current_api_keys" ] || [ "$current_api_keys" = "$DEFAULT_API_KEYS_RAW" ]; }; then
|
||||
export API_KEYS_RAW="$persisted_api_keys"
|
||||
elif [ -n "$app_env_api_keys" ] && { [ -z "$current_api_keys" ] || [ "$current_api_keys" = "$DEFAULT_API_KEYS_RAW" ]; }; then
|
||||
export API_KEYS_RAW="$app_env_api_keys"
|
||||
if [ "$app_env_api_keys" != "$DEFAULT_API_KEYS_RAW" ]; then
|
||||
write_env_value "$PERSISTED_ENV_FILE" "API_KEYS_RAW" "$app_env_api_keys"
|
||||
fi
|
||||
elif [ -n "$current_api_keys" ] && [ "$current_api_keys" != "$DEFAULT_API_KEYS_RAW" ]; then
|
||||
write_env_value "$PERSISTED_ENV_FILE" "API_KEYS_RAW" "$current_api_keys"
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
@@ -4,10 +4,22 @@
|
||||
Développement d'un widget Windows moderne et simple connecté à un serveur centralisé hébergé dans un conteneur Docker. L'API du serveur doit être accessible par un agent IA pour piloter la liste de tâches. Les tâches doivent afficher leur date de création et pouvoir être marquées comme "En cours" ou "Terminé".
|
||||
|
||||
## Focus Actuel
|
||||
- Phase 3 : Développement du Widget Windows avec Tauri et design Glassmorphism.
|
||||
- Mise en place du démarrage automatique et de l'option "Toujours au premier plan".
|
||||
- Projet stable, fonctionnel et documenté.
|
||||
|
||||
## Master Plan
|
||||
- [x] **Correction du Bug d'Édition du Widget**
|
||||
- [x] Identifier la cause racine (le polling de 4s écrase le DOM et provoque un blur de l'input)
|
||||
- [x] Modifier `fetchTasks()` dans `widget/src/app.js` pour suspendre la synchronisation durant l'édition
|
||||
- [x] Tester et valider la correction localement
|
||||
- [x] **Synchronisation avec le Dépôt Distant**
|
||||
- [x] Mettre à jour `task.md` pour refléter la demande de l'utilisateur
|
||||
- [x] Effectuer un `git pull` pour appliquer les 20 commits de retard sur `origin/main`
|
||||
- [x] Valider l'état des fichiers locaux et s'assurer que la copie locale est propre et fonctionnelle
|
||||
- [x] **Phase 6 : Mise à Jour de la Documentation pour Agent IA Externe (README)**
|
||||
- [x] Concevoir le plan d'action simplifié (`implementation_plan.md`)
|
||||
- [x] Documenter la route manquante `GET /api/v1/tasks/{task_id}` dans `README_AI_AGENT.md`
|
||||
- [x] Ajouter les schémas d'outils (Function Calling) pour LLMs dans `README_AI_AGENT.md`
|
||||
- [x] Vérifier la cohérence de la documentation avec le code réel de l'API
|
||||
- [x] **Phase 1 : Conception & Architecture**
|
||||
- [x] Créer le fichier `task.md`
|
||||
- [x] Consulter le sous-agent `architecte` pour la structure de l'API et du conteneur
|
||||
@@ -42,3 +54,6 @@ Développement d'un widget Windows moderne et simple connecté à un serveur cen
|
||||
## Journal de Progression
|
||||
- **2026-05-26** : Plan validé par l'utilisateur. Ajout des spécifications : configuration de l'URL API dans le widget, création directe de tâches, mode toujours au premier plan, et démarrage automatique. Début du développement du serveur API.
|
||||
- **2026-05-26** : Implémentation du serveur API conteneurisé (FastAPI + SQLModel + Docker Compose) et du widget de bureau Tauri (HTML/CSS/JS, drag-region, always-on-top natif, autostart registre Windows, tray icon). Rédaction des tests unitaires et du guide d'intégration pour Agent IA. Début de la validation.
|
||||
- **2026-05-27** : Récupération des dernières modifications du dépôt distant (`origin/main`). Synchronisation de 20 commits incluant des améliorations de l'interface (style glassmorphic, redimensionnement natif, icônes premium 3D) et des correctifs pour l'intégration Docker/Tauri. La copie locale est désormais à jour.
|
||||
- **2026-05-27** : Mise à jour complète du guide d'intégration `README_AI_AGENT.md`. Ajout de la documentation pour la route `GET /api/v1/tasks/{task_id}` et création de schémas complets d'outils (Function Calling / Tools) pour OpenAI, Gemini et Anthropic Claude pour faciliter l'intégration immédiate d'un agent externe.
|
||||
- **2026-05-27** : Correction du bug d'édition de tâches sur le widget. Ajout d'une condition pour suspendre temporairement le rafraîchissement périodique (polling de 4s) lorsque l'utilisateur édite le titre d'une tâche. Cela évite le rafraîchissement destructif du DOM et la perte de focus.
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "todoflow-widget",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"tauri": "tauri"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "todoflow"
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
description = "Sleek Windows TodoFlow desktop widget"
|
||||
authors = ["Michael"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -67,6 +67,32 @@ fn get_autostart() -> Result<bool, String> {
|
||||
Ok(val.is_ok())
|
||||
}
|
||||
|
||||
// --- GESTION DE LA VISIBILITÉ DE LA FENÊTRE ---
|
||||
|
||||
fn toggle_window_visibility(app: &AppHandle) {
|
||||
// Bascule l'affichage du widget depuis l'icône de la barre d'état.
|
||||
// Comme la fenêtre est configurée avec skipTaskbar:true, l'icône du tray est le
|
||||
// seul point de restauration : il doit donc fonctionner dans tous les états.
|
||||
let window = match app.get_window("main") {
|
||||
Some(w) => w,
|
||||
None => return,
|
||||
};
|
||||
|
||||
// Une fenêtre réduite est toujours rapportée comme "visible" par Tauri. On la
|
||||
// considère donc comme masquée afin de la restaurer correctement (et non la re-masquer).
|
||||
let is_minimized = window.is_minimized().unwrap_or(false);
|
||||
let is_visible = window.is_visible().unwrap_or(false);
|
||||
|
||||
if is_visible && !is_minimized {
|
||||
let _ = window.hide();
|
||||
} else {
|
||||
// Restauration robuste : dé-minimiser, ré-afficher, puis ramener au premier plan.
|
||||
let _ = window.unminimize();
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
|
||||
// --- CONTEXTE ET INITIALISATION ---
|
||||
|
||||
fn main() {
|
||||
@@ -84,26 +110,14 @@ fn main() {
|
||||
.on_system_tray_event(|app, event| match event {
|
||||
SystemTrayEvent::LeftClick { .. } => {
|
||||
// Un simple clic gauche sur l'icône de la barre des tâches bascule la visibilité
|
||||
let window = app.get_window("main").unwrap();
|
||||
if window.is_visible().unwrap() {
|
||||
window.hide().unwrap();
|
||||
} else {
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
}
|
||||
toggle_window_visibility(app);
|
||||
}
|
||||
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
|
||||
"quit" => {
|
||||
std::process::exit(0);
|
||||
}
|
||||
"toggle" => {
|
||||
let window = app.get_window("main").unwrap();
|
||||
if window.is_visible().unwrap() {
|
||||
window.hide().unwrap();
|
||||
} else {
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
}
|
||||
toggle_window_visibility(app);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "TodoFlow",
|
||||
"version": "1.0.0"
|
||||
"version": "1.1.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
||||
+9
-1
@@ -88,7 +88,10 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
|
||||
// Événements Tauri pour le contrôle de la fenêtre de bureau
|
||||
if (appWindow) {
|
||||
btnMinimize.addEventListener("click", () => appWindow.minimize());
|
||||
// La fenêtre utilise skipTaskbar:true : elle n'apparaît jamais dans la barre des tâches.
|
||||
// Un minimize() classique la rendrait donc irrécupérable. On masque plutôt vers le
|
||||
// system tray, d'où elle peut toujours être restaurée via l'icône de la barre d'état.
|
||||
btnMinimize.addEventListener("click", () => appWindow.hide());
|
||||
btnClose.addEventListener("click", () => appWindow.hide()); // Masquer vers le tray au clic sur 'Fermer'
|
||||
|
||||
// Récupération de l'état réel du démarrage automatique depuis le registre Windows
|
||||
@@ -176,6 +179,11 @@ async function saveSettings() {
|
||||
// --- APPELS API (CRUD) ---
|
||||
|
||||
async function fetchTasks() {
|
||||
// Suspendre la synchronisation si l'utilisateur est en train d'éditer une tâche pour éviter de perdre le focus et fermer l'édition
|
||||
if (document.querySelector(".task-card.editing")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupération asynchrone des tâches avec la clé API
|
||||
try {
|
||||
const response = await fetch(`${state.apiUrl}/api/v1/tasks`, {
|
||||
|
||||
Reference in new issue
Block a user