Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6066890bae | ||
|
|
96c08ed15e | ||
|
|
52e87608ad | ||
|
|
90be279a2b | ||
|
|
eb39c1ed5d | ||
|
|
121c59760e |
@@ -1,5 +1,18 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.22.1 — 2026-10-20
|
||||||
|
|
||||||
|
### Correctif — colonne Terminé vide
|
||||||
|
|
||||||
|
- Les issues **fermées dans Gitea** mais suivies par le board
|
||||||
|
n'apparaissaient pas dans la colonne **Terminé** : la
|
||||||
|
réconciliation plaçait bien leur ligne locale en `done`, mais
|
||||||
|
l'affichage du board ne fusionnait que les issues **ouvertes**.
|
||||||
|
- Le board (page HTML et API JSON) inclut désormais les issues
|
||||||
|
fermées dont la ligne locale est en `done`, y compris dans la
|
||||||
|
vue par milestone.
|
||||||
|
- Test de régression ajouté ; suite complète : 201 tests verts.
|
||||||
|
|
||||||
## 0.22.0 — 2026-10-14
|
## 0.22.0 — 2026-10-14
|
||||||
|
|
||||||
### Écran de connexion (demande de Bruno)
|
### Écran de connexion (demande de Bruno)
|
||||||
|
|||||||
+1
-1
@@ -95,7 +95,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
APP_VERSION = "0.22.0"
|
APP_VERSION = "0.22.1"
|
||||||
|
|
||||||
|
|
||||||
def _env_bool(name: str, default: bool) -> bool:
|
def _env_bool(name: str, default: bool) -> bool:
|
||||||
|
|||||||
+1
-1
@@ -143,7 +143,7 @@ async def _mcp_session(client: httpx.AsyncClient, url: str, token: str) -> None:
|
|||||||
{
|
{
|
||||||
"protocolVersion": MCP_PROTOCOL_VERSION,
|
"protocolVersion": MCP_PROTOCOL_VERSION,
|
||||||
"capabilities": {},
|
"capabilities": {},
|
||||||
"clientInfo": {"name": "kanbante", "version": "0.22.0"},
|
"clientInfo": {"name": "kanbante", "version": "0.22.1"},
|
||||||
},
|
},
|
||||||
req_id=1,
|
req_id=1,
|
||||||
)
|
)
|
||||||
|
|||||||
+25
-5
@@ -832,13 +832,25 @@ async def board(request: Request, owner: str, repo: str, milestone: str = ""):
|
|||||||
# A kanban:* label changed outside Kanbante wins over local state.
|
# A kanban:* label changed outside Kanbante wins over local state.
|
||||||
_adopt_label_columns(full_name, open_issues, get_board_state(full_name))
|
_adopt_label_columns(full_name, open_issues, get_board_state(full_name))
|
||||||
|
|
||||||
|
# Closed issues stay visible while the board tracks them as done
|
||||||
|
# (reconcile_board_state moved their row). Merging open issues only
|
||||||
|
# made the "Terminé" column permanently empty.
|
||||||
|
done_numbers = {
|
||||||
|
c["issue_number"] for c in get_board_state(full_name).get("done", [])
|
||||||
|
}
|
||||||
|
closed_display = [
|
||||||
|
i
|
||||||
|
for i in issues
|
||||||
|
if i.get("state") != "open" and i["number"] in done_numbers
|
||||||
|
]
|
||||||
|
|
||||||
# Milestone view: filter the board to one milestone (sprint-style),
|
# Milestone view: filter the board to one milestone (sprint-style),
|
||||||
# with its deadline and completion progress.
|
# with its deadline and completion progress.
|
||||||
milestones: list[dict] = []
|
milestones: list[dict] = []
|
||||||
selected_milestone = None
|
selected_milestone = None
|
||||||
milestone_progress = None
|
milestone_progress = None
|
||||||
milestone_forecast = None
|
milestone_forecast = None
|
||||||
display_issues = open_issues
|
display_issues = open_issues + closed_display
|
||||||
try:
|
try:
|
||||||
milestones = await gitea.get_milestones(owner, repo)
|
milestones = await gitea.get_milestones(owner, repo)
|
||||||
except Exception as exc: # milestones are decoration, never fatal
|
except Exception as exc: # milestones are decoration, never fatal
|
||||||
@@ -851,7 +863,7 @@ async def board(request: Request, owner: str, repo: str, milestone: str = ""):
|
|||||||
str(ms.get("id")) == milestone or ms.get("title") == milestone
|
str(ms.get("id")) == milestone or ms.get("title") == milestone
|
||||||
)
|
)
|
||||||
|
|
||||||
display_issues = [i for i in open_issues if _in_milestone(i)]
|
display_issues = [i for i in display_issues if _in_milestone(i)]
|
||||||
selected_milestone = next(
|
selected_milestone = next(
|
||||||
(
|
(
|
||||||
m for m in milestones
|
m for m in milestones
|
||||||
@@ -941,7 +953,16 @@ def _board_columns_for(
|
|||||||
for number in changes["closed_numbers"]:
|
for number in changes["closed_numbers"]:
|
||||||
log_event(full_name, number, "closed", to_column="done", source="gitea")
|
log_event(full_name, number, "closed", to_column="done", source="gitea")
|
||||||
_adopt_label_columns(full_name, open_issues, get_board_state(full_name))
|
_adopt_label_columns(full_name, open_issues, get_board_state(full_name))
|
||||||
display_issues = open_issues
|
board_state = get_board_state(full_name)
|
||||||
|
# Closed issues stay visible while the board tracks them as done
|
||||||
|
# (reconcile_board_state moved their row). Merging open issues only
|
||||||
|
# made the "Terminé" column permanently empty.
|
||||||
|
done_numbers = {c["issue_number"] for c in board_state.get("done", [])}
|
||||||
|
display_issues = open_issues + [
|
||||||
|
i
|
||||||
|
for i in issues
|
||||||
|
if i.get("state") != "open" and i["number"] in done_numbers
|
||||||
|
]
|
||||||
if milestone:
|
if milestone:
|
||||||
def _in_ms(issue: dict) -> bool:
|
def _in_ms(issue: dict) -> bool:
|
||||||
ms = issue.get("milestone") or {}
|
ms = issue.get("milestone") or {}
|
||||||
@@ -949,8 +970,7 @@ def _board_columns_for(
|
|||||||
str(ms.get("id")) == milestone or ms.get("title") == milestone
|
str(ms.get("id")) == milestone or ms.get("title") == milestone
|
||||||
)
|
)
|
||||||
|
|
||||||
display_issues = [i for i in open_issues if _in_ms(i)]
|
display_issues = [i for i in display_issues if _in_ms(i)]
|
||||||
board_state = get_board_state(full_name)
|
|
||||||
columns = _merge_issues_with_board(display_issues, board_state)
|
columns = _merge_issues_with_board(display_issues, board_state)
|
||||||
return columns, display_issues, board_state
|
return columns, display_issues, board_state
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "kanbante"
|
name = "kanbante"
|
||||||
version = "0.22.0"
|
version = "0.22.1"
|
||||||
description = "Kanban project manager integrated with Gitea"
|
description = "Kanban project manager integrated with Gitea"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -39,6 +39,19 @@ def test_board_hides_pull_requests_and_reconciles(client):
|
|||||||
assert [c["issue_number"] for c in state["done"]] == [3] # reconciled
|
assert [c["issue_number"] for c in state["done"]] == [3] # reconciled
|
||||||
|
|
||||||
|
|
||||||
|
def test_board_renders_closed_issue_in_done_column(client):
|
||||||
|
# Regression: reconcile moved the closed issue's row to "done", but
|
||||||
|
# the board merged open issues only, so the card never rendered and
|
||||||
|
# the "Terminé" column stayed empty.
|
||||||
|
models.move_issue("Projets/demo", 3, "todo") # closed in Gitea
|
||||||
|
r = client.get("/board/Projets/demo")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert "Ancienne tâche" in r.text
|
||||||
|
payload = client.get("/api/board/Projets/demo").json()
|
||||||
|
assert payload["columns"]["done"]["count"] == 1
|
||||||
|
assert "Ancienne tâche" in payload["columns"]["done"]["html"]
|
||||||
|
|
||||||
|
|
||||||
def test_focus_lists_in_progress_across_projects(client):
|
def test_focus_lists_in_progress_across_projects(client):
|
||||||
models.move_issue("Projets/demo", 5, "in_progress")
|
models.move_issue("Projets/demo", 5, "in_progress")
|
||||||
r = client.get("/focus")
|
r = client.get("/focus")
|
||||||
|
|||||||
Reference in New Issue
Block a user