Analyse le contenu collé dans un bloc (paste2b) et le répartit en blocs selon son format : titres markdown, listes à puces et numérotées, cases à cocher, citations, séparateurs, blocs de code, tableaux et paragraphes. Le texte avant le curseur est conservé dans le bloc courant (réutilisé si vide), les nouveaux blocs sont insérés après, le texte après le curseur garde sa place, refocus sur le dernier bloc inséré. Les blocs non textuels (code, tableau, image...) gardent un collage en texte brut.
183 lines
6.4 KiB
Python
183 lines
6.4 KiB
Python
"""FlowDeck — Kanban léger intégré à Gitea."""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import asyncio
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.staticfiles import StaticFiles
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from starlette.middleware.sessions import SessionMiddleware
|
|
|
|
from app.config import settings
|
|
from app.db import init_db
|
|
from app.middleware.csrf import CSRFMiddleware
|
|
from app.middleware.security import ContentSecurityPolicyMiddleware, RateLimitMiddleware
|
|
from app.routers import dashboard, board, notes, api, auth, webhooks, collections, my_tasks, workspace, library, public_api, admin, sharing, sidebar_config, export, agent, search
|
|
from app.routers.notifications import router as notifications_router
|
|
from app.routers.collaboration import router as collaboration_router
|
|
from app.routers.gitea import router as gitea_router
|
|
from app.routers.github_routes import router as github_router
|
|
from app.services.gitea_client import gitea
|
|
from app.services.webhook_outbound import init_webhook_tables
|
|
|
|
logging.basicConfig(
|
|
level=getattr(logging, settings.log_level.upper(), logging.INFO),
|
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(_app: FastAPI):
|
|
init_db()
|
|
init_webhook_tables()
|
|
from app.db import get_conn
|
|
from app.password_utils import hash_password
|
|
admin_hash = hash_password("FlowDeck2026!")
|
|
with get_conn() as conn:
|
|
conn.execute(
|
|
"INSERT OR IGNORE INTO users (login, full_name, email, password_hash, is_admin) VALUES ('admin', 'Admin', '', ?, 1)",
|
|
(admin_hash,)
|
|
)
|
|
conn.commit()
|
|
|
|
# ── FlowDeck Agent (v4.10.0): scheduled custom-agent triggers ──
|
|
from app.routers.agent import agent_scheduler
|
|
scheduler_task = asyncio.create_task(agent_scheduler())
|
|
|
|
logger.info("FlowDeck v4.10.0 started on port %d", settings.app_port)
|
|
try:
|
|
yield
|
|
finally:
|
|
scheduler_task.cancel()
|
|
try:
|
|
await scheduler_task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
|
|
app = FastAPI(
|
|
title="FlowDeck",
|
|
version="5.1.11",
|
|
docs_url="/docs" if settings.log_level == "DEBUG" else None,
|
|
redoc_url=None,
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
app.add_middleware(SessionMiddleware, secret_key=settings.app_secret_key, max_age=3600)
|
|
app.add_middleware(CSRFMiddleware)
|
|
app.add_middleware(ContentSecurityPolicyMiddleware)
|
|
app.add_middleware(RateLimitMiddleware)
|
|
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
|
|
|
|
app.include_router(auth.router)
|
|
app.include_router(dashboard.router)
|
|
app.include_router(board.router)
|
|
app.include_router(notes.router)
|
|
app.include_router(api.router)
|
|
app.include_router(webhooks.router)
|
|
app.include_router(collections.router)
|
|
app.include_router(my_tasks.router)
|
|
app.include_router(workspace.router)
|
|
app.include_router(library.router)
|
|
app.include_router(admin.router)
|
|
app.include_router(gitea_router)
|
|
app.include_router(github_router)
|
|
app.include_router(public_api.router)
|
|
app.include_router(sharing.router)
|
|
app.include_router(sidebar_config.router)
|
|
app.include_router(export.router)
|
|
app.include_router(notifications_router)
|
|
app.include_router(collaboration_router)
|
|
app.include_router(agent.router)
|
|
app.include_router(search.router)
|
|
|
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
|
|
|
|
@app.get("/manifest.json")
|
|
async def pwa_manifest():
|
|
return {
|
|
"name": "FlowDeck",
|
|
"short_name": "FlowDeck",
|
|
"start_url": "/",
|
|
"display": "standalone",
|
|
"background_color": "#191919",
|
|
"theme_color": "#191919",
|
|
"icons": [{"src": "/static/icon-192.png", "sizes": "192x192", "type": "image/png"}],
|
|
}
|
|
|
|
|
|
# ═══════════ API aliases (v4.0.1) ═══════════
|
|
|
|
|
|
@app.get("/api/csrf-token")
|
|
async def csrf_token_endpoint(request: Request):
|
|
"""Return a fresh CSRF token. Used by the frontend to auto-recover from 403."""
|
|
from fastapi.responses import JSONResponse
|
|
import secrets
|
|
token = secrets.token_hex(32)
|
|
response = JSONResponse({"csrf_token": token})
|
|
response.set_cookie(
|
|
"csrf_token", token,
|
|
httponly=False, samesite="lax", max_age=86400, path="/",
|
|
)
|
|
return response
|
|
|
|
|
|
@app.get("/api/pages")
|
|
async def api_pages_alias(request: Request):
|
|
"""Alias /api/pages → /board/api/pages for API path consistency."""
|
|
from fastapi.responses import RedirectResponse
|
|
qs = str(request.url.query)
|
|
target = f"/board/api/pages{'?' + qs if qs else ''}"
|
|
return RedirectResponse(url=target, status_code=307)
|
|
|
|
|
|
@app.post("/api/pages")
|
|
async def api_pages_post_alias(request: Request):
|
|
"""Alias POST /api/pages → /board/api/pages for API path consistency."""
|
|
from fastapi.responses import RedirectResponse
|
|
return RedirectResponse(url="/board/api/pages", status_code=307)
|
|
|
|
|
|
# ═══════════ Styled 404 handler ═══════════
|
|
|
|
|
|
NOT_FOUND_HTML = """<!DOCTYPE html>
|
|
<html lang="en" data-theme="dark">
|
|
<head>
|
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
<title>404 — FlowDeck</title>
|
|
<style>
|
|
:root{--bg:#191919;--text:#e0e0e0;--text-dim:#999;--accent:#2383E2}
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg);color:var(--text);display:flex;align-items:center;justify-content:center;min-height:100vh;text-align:center}
|
|
.box h1{font-size:6rem;font-weight:800;opacity:.08;line-height:1}
|
|
.box p{font-size:18px;color:var(--text-dim);margin:8px 0 24px}
|
|
.box a{color:var(--accent);text-decoration:none;font-size:14px}
|
|
.box a:hover{text-decoration:underline}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="box">
|
|
<h1>404</h1>
|
|
<p>This page doesn't exist or has been moved.</p>
|
|
<a href="/">← Back to FlowDeck</a>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
|
|
|
|
@app.exception_handler(404)
|
|
async def not_found_handler(request: Request, exc):
|
|
"""Redirect 404 HTML pages to /workspaces. API routes still get JSON."""
|
|
# Preserve JSON 404 for all API-like paths (including /db/xxx/api)
|
|
if "/api" in request.url.path:
|
|
from fastapi.responses import JSONResponse
|
|
return JSONResponse({"detail": "Not found"}, status_code=404)
|
|
from fastapi.responses import RedirectResponse
|
|
return RedirectResponse("/workspaces", status_code=302)
|