Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a74ea8bbd | ||
|
|
13dc8fdaad |
@@ -13,7 +13,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
python-version: '3.13'
|
||||
- name: Install lint tools
|
||||
run: pip install -r requirements-dev.txt
|
||||
- name: Ruff (Python)
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
python-version: '3.13'
|
||||
- name: Install system dependencies (WeasyPrint / emoji fonts)
|
||||
run: |-
|
||||
SUDO=""
|
||||
|
||||
@@ -1,5 +1,55 @@
|
||||
# Changelog - FlowDeck
|
||||
|
||||
## v7.35.0 (2026-10-01) — Audit : A35 TERMINÉ (Python 3.13 aligné + rebuild)
|
||||
|
||||
### Changed
|
||||
|
||||
- **Drift Python résolu** (dernier reliquat A35) : tout le projet est en
|
||||
**3.13** — `Dockerfile` (`python:3.13-slim` builder + runtime), CI Gitea
|
||||
(`python-version: '3.13'` ×2), `README.md` ×2, ruff
|
||||
`target-version = "py313"` (0 nouvelle remarque ruff). Restait 3.12 aux
|
||||
6 endroits alors que `uv.lock` = `requires-python >=3.13` et le venv =
|
||||
3.13.14. Zéro référence 3.12 résiduelle.
|
||||
|
||||
### Validation (le point que l'audit laissait « à faire par rebuild »)
|
||||
|
||||
- `docker build` **vert** sur `python:3.13-slim` (image `flowdeck:a35-py313`)
|
||||
- dans le conteneur : `python -V` = **3.13.16**, `import app.main` OK
|
||||
(version 7.35.0) → wheels `requirements.txt` construits et importables
|
||||
sur 3.13
|
||||
- Suite locale complète : **1093/1093** · ruff OK (target py313)
|
||||
|
||||
### Notes
|
||||
|
||||
- A35 = **TERMINÉ** (OpenAPI/README/titre faits en 7.3.9 + drift aujourd'hui)
|
||||
|
||||
## v7.34.0 (2026-10-01) — Audit : A38 phase 2 (0 doublon de fonction globale)
|
||||
|
||||
### Changed
|
||||
|
||||
- **Inventaire exhaustif de la duplication de fonctions** : scan de
|
||||
profondeur de brace sur les 13 noms définis 2+ fois (templates +
|
||||
static/js, strings/comments/backticks gérés) → **12 sont déjà scopés**
|
||||
dans des IIFEs depuis A27 (aucun conflit de page possible)
|
||||
- **Seul doublon global = `openCardDetail`** (corps byte-identiques ×2 dans
|
||||
`board_fragment` + `detailed_board`, fragments de vues mutuellement
|
||||
exclusifs) → dédupliquée vers `static/js/app.js`, les 2 copies
|
||||
supprimées (les onclick/@click des deux fragments appellent la même
|
||||
définition)
|
||||
|
||||
### Tests
|
||||
|
||||
- `test_no_duplicate_global_functions` : garde-fou — 0 `function NAME`
|
||||
globale définie 2+ fois entre templates et static/js (scanner naïf,
|
||||
plafond `ponytail:` commenté : un faux positif se lit au nom signalé)
|
||||
- Suite complète : **1092/1092** · ruff OK · node --check vert
|
||||
|
||||
### Notes
|
||||
|
||||
- Reste A38 : méthodes jumelles library/local_workspace (~9-21 noms
|
||||
communs, corps divergents) → fusion `workspace-tree.js` reportée
|
||||
(réconciliation sans E2E — même logique que A39/A20)
|
||||
|
||||
## v7.33.0 (2026-10-01) — Audit : A43 TERMINÉ + A38 phase 1 (CSRF unifié)
|
||||
|
||||
### Fixed
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
# Stage 1 "builder": build Python wheels once.
|
||||
# Stage 2 "runtime": minimal image with WeasyPrint system libs.
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
FROM python:3.12-slim AS builder
|
||||
FROM python:3.13-slim AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -11,7 +11,7 @@ COPY requirements.txt .
|
||||
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
|
||||
|
||||
# ── runtime stage ───────────────────────────────────────────
|
||||
FROM python:3.12-slim AS runtime
|
||||
FROM python:3.13-slim AS runtime
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -73,9 +73,9 @@ docker compose up -d
|
||||
| Couche | Techno |
|
||||
|--------|--------|
|
||||
| Frontend | Jinja2 + HTMX + Alpine.js + SortableJS + CSS |
|
||||
| Backend | Python 3.12 + FastAPI + httpx |
|
||||
| Backend | Python 3.13 + FastAPI + httpx |
|
||||
| BDD | SQLite (WAL, 21 tables) — `/data/flowdeck.db` |
|
||||
| Déploiement | Docker (python:3.12-slim), docker-compose |
|
||||
| Déploiement | Docker (python:3.13-slim), docker-compose |
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
+3
-3
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,6 +1,6 @@
|
||||
# WORKLOAD — FlowDeck Notion Clone
|
||||
|
||||
> **Début**: 2026-07-08 | **Version**: v7.33.0 (audit — A43 TERMINÉ + A38 phase 1 CSRF : 76 lectures → getCsrf() | **Statut**: EN COURS 🔄
|
||||
> **Début**: 2026-07-08 | **Version**: v7.35.0 (audit — A35 TERMINÉ : Python 3.13 aligné + rebuild image validé | **Statut**: EN COURS 🔄
|
||||
> **Cible**: parité Notion + intégration forge · **Follow-ups v7.3 livrés**: sidebar teamspaces, notif `page.updated`, charts `number` + dashboards multi-DB, unfurl forge, UI Settings → Audit — voir `ROADMAP.md § v7.3.0`
|
||||
|
||||
## Avancement Global
|
||||
|
||||
+1
-1
@@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI):
|
||||
|
||||
app = FastAPI(
|
||||
title="FlowDeck",
|
||||
version="7.33.0",
|
||||
version="7.35.0",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
lifespan=lifespan,
|
||||
|
||||
@@ -49,12 +49,5 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<script nonce="{{ csp_nonce() }}">
|
||||
function openCardDetail(id) {
|
||||
htmx.ajax('GET', `/api/issues/${owner}/${repo}/${id}?format=html`, {
|
||||
target: '#card-modal-content', swap: 'innerHTML'
|
||||
});
|
||||
document.getElementById('card-modal').style.display = 'flex';
|
||||
document.getElementById('card-modal-title').textContent = 'Issue #' + id;
|
||||
}
|
||||
</script>
|
||||
<!-- A38 : openCardDetail vit dans static/js/app.js (dedupliquee,
|
||||
corps identique x2 dans ces deux fragments de vue) -->
|
||||
|
||||
@@ -58,12 +58,5 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<script nonce="{{ csp_nonce() }}">
|
||||
function openCardDetail(id) {
|
||||
htmx.ajax('GET', `/api/issues/${owner}/${repo}/${id}?format=html`, {
|
||||
target: '#card-modal-content', swap: 'innerHTML'
|
||||
});
|
||||
document.getElementById('card-modal').style.display = 'flex';
|
||||
document.getElementById('card-modal-title').textContent = 'Issue #' + id;
|
||||
}
|
||||
</script>
|
||||
<!-- A38 : openCardDetail vit dans static/js/app.js (dedupliquee,
|
||||
corps identique x2 dans ces deux fragments de vue) -->
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "FlowDeck",
|
||||
"version": "7.33.0"
|
||||
"version": "7.35.0"
|
||||
},
|
||||
"paths": {
|
||||
"/auth/register": {
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ pythonpath = ["."]
|
||||
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py312"
|
||||
target-version = "py313"
|
||||
line-length = 110
|
||||
exclude = [".venv", ".venv311", ".pytest_cache", "static/js/vendor"]
|
||||
|
||||
|
||||
@@ -395,3 +395,15 @@
|
||||
});
|
||||
})();
|
||||
|
||||
// A38 : openCardDetail était définie (corps identique) dans board_fragment
|
||||
// ET detailed_board — une seule définition globale ici, appelée par les
|
||||
// onclick/@click des deux fragments (owner/repo globaux fournis par board.js).
|
||||
function openCardDetail(id) {
|
||||
htmx.ajax('GET', `/api/issues/${owner}/${repo}/${id}?format=html`, {
|
||||
target: '#card-modal-content',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
document.getElementById('card-modal').style.display = 'flex';
|
||||
document.getElementById('card-modal-title').textContent = 'Issue #' + id;
|
||||
}
|
||||
|
||||
|
||||
@@ -366,6 +366,73 @@ def test_csrf_server_rendered_no_placeholder(client):
|
||||
assert token == cookie, (token[:8], cookie[:8])
|
||||
|
||||
|
||||
def test_no_duplicate_global_functions():
|
||||
"""A38 : aucune fonction `function NAME` GLOBALE (profondeur 0) définie
|
||||
2+ fois entre les templates et static/js — les paires à risque d'ombre
|
||||
silencieuse (onDoc, escHtml, openCardDetail…) vivent dans des IIFEs ou
|
||||
sont dédupliquées (openCardDetail → app.js)."""
|
||||
import pathlib as _pathlib
|
||||
import re as _re
|
||||
|
||||
files = list(_pathlib.Path("app/templates").glob("*.html")) + list(
|
||||
_pathlib.Path("static/js").glob("*.js")
|
||||
)
|
||||
found: dict[str, list[str]] = {}
|
||||
for p in files:
|
||||
s = p.read_text(encoding="utf-8", errors="ignore")
|
||||
depth = 0
|
||||
line = 1
|
||||
i = 0
|
||||
state = None
|
||||
n = len(s)
|
||||
# ponytail: scanner naïve (strings/comments/backticks) — un faux
|
||||
# positif se voit immédiatement à la lecture du nom signalé
|
||||
while i < n:
|
||||
c = s[i]
|
||||
if c == "\n":
|
||||
line += 1
|
||||
if state is None:
|
||||
if c in ('"', "'"):
|
||||
state = c
|
||||
i += 1
|
||||
continue
|
||||
if c == "`":
|
||||
state = c
|
||||
i += 1
|
||||
continue
|
||||
if c == "/" and i + 1 < n and s[i + 1] == "/":
|
||||
state = "//"
|
||||
i += 2
|
||||
continue
|
||||
if c == "/" and i + 1 < n and s[i + 1] == "*":
|
||||
state = "/*"
|
||||
i += 2
|
||||
continue
|
||||
if c == "{":
|
||||
depth += 1
|
||||
elif c == "}":
|
||||
depth -= 1
|
||||
else:
|
||||
if c == "\\":
|
||||
i += 2
|
||||
continue
|
||||
if (state in ('"', "'") and c == state) or (state == "`" and c == state):
|
||||
state = None
|
||||
elif state == "//" and c == "\n":
|
||||
state = None
|
||||
elif state == "/*" and c == "*" and i + 1 < n and s[i + 1] == "/":
|
||||
state = None
|
||||
i += 2
|
||||
continue
|
||||
i += 1
|
||||
if state is None and depth == 0 and s.startswith("function ", i):
|
||||
m = _re.match(r"function\s+([A-Za-z_]\w*)", s[i : i + 60])
|
||||
if m:
|
||||
found.setdefault(m.group(1), []).append(f"{p.name}:{line}")
|
||||
dups = {k: v for k, v in found.items() if len(v) >= 2}
|
||||
assert dups == {}, dups
|
||||
|
||||
|
||||
def test_no_duplicate_routes():
|
||||
"""A24 : deux routes même méthode+chemin → l'une écrase silencieusement l'autre."""
|
||||
from app.main import app
|
||||
|
||||
Reference in New Issue
Block a user