diff --git a/distgit/tasks.py b/distgit/tasks.py
index 5c9dd55..fb367c0 100644
--- a/distgit/tasks.py
+++ b/distgit/tasks.py
@@ -1,6 +1,6 @@
 import json
 from random import randint
-from pygit2 import Tag, Oid, reference_is_valid_name, Repository
+from pygit2 import Signature, Tag, Oid, reference_is_valid_name, Repository
 from datetime import datetime
 from json import JSONEncoder
 from typing import Any
@@ -29,11 +29,15 @@ class _DateTimeEncoder(JSONEncoder):
         return super().default(o)
 
 
-def _tagger_str(tagger: str) -> str:
-    """Produce a tagger line for git tag object (name <email> timestamp +tz)."""
-    if tagger and " <" in tagger and ">" in tagger:
-        return tagger + " 0 +0000"
-    return (tagger or "distgit <distgit@local>") + " 0 +0000"
+def _tagger_str(tagger: str | Signature | None) -> str:
+    """Produce a tagger line for git tag object (name <email> timestamp +tz). Accepts str or pygit2.Signature."""
+    if isinstance(tagger, Signature):
+        return f"{tagger.name} <{tagger.email}> {tagger.time} {tagger.offset}"
+    if isinstance(tagger, str):
+        if tagger and " <" in tagger and ">" in tagger:
+            return tagger + " 0 +0000"
+        return (tagger or "distgit <distgit@local>") + " 0 +0000"
+    return "distgit <distgit@local> 0 +0000"
 
 
 def _build_tag_raw(
@@ -43,7 +47,7 @@ def _build_tag_raw(
     message: str,
     object_type: str = "commit",
 ) -> bytes:
-    """Build full raw git tag object bytes (header + content) for odb.write(4, data). object_type must match the target (e.g. 'commit' or 'tree')."""
+    """Build tag object body (content only) for odb.write(4, data). libgit2 prepends 'tag <len>\\0'. object_type must match the target (e.g. 'commit' or 'tree')."""
     content = (
         f"object {str(target)}\n"
         f"type {object_type}\n"
@@ -51,9 +55,7 @@ def _build_tag_raw(
         f"tagger {_tagger_str(tagger)}\n"
         f"\n{message}"
     )
-    body = content.encode("utf-8")
-    header = f"tag {len(body)}\0".encode("utf-8")
-    return header + body
+    return content.encode("utf-8")
 
 
 def _read_tag_raw(repo: Repository, oid: Oid) -> tuple[Oid, str, str, str]:
@@ -110,6 +112,7 @@ class Task:
         self.target = target
         self.tagger = tagger or ""
         self.name = name
+        self.title = title
         self.description = description
         self.status = status
         self.priority = priority
@@ -164,7 +167,7 @@ def get_task(repo: Repository, ref: str) -> Task | None:
         tag.target,
         tag.name,
         tag.tagger,
-        j["title"],
+        j.get("title", "Untitled"),
         description=j.get("description", ""),
         status=status,
         priority=priority,
@@ -189,7 +192,7 @@ def get_task_by_oid(repo: Repository, oid: Oid) -> Task | None:
         obj.target,
         obj.name,
         obj.tagger,
-        j["title"],
+        j.get("title", "Untitled"),
         description=j.get("description", ""),
         status=status,
         priority=priority,
@@ -308,16 +311,10 @@ class Board:
 
 
 def get_board(repo: Repository, ref: str) -> Board | None:
-    try:
-        tag = repo.revparse_single(ref)
-    except Exception:
-        return None
+    tag = repo.revparse_single(ref)
     if not isinstance(tag, Tag):
         return None
-    try:
-        j = json.loads(tag.message)
-    except (ValueError, TypeError):
-        return None
+    j = json.loads(tag.message)
     b = Board(tag.target, tag.name, tag.tagger, j.get("description", ""))
     b.tasks = j.get("tasks", [])
     b.created_at = _parse_dt(j.get("created_at")) or b.created_at
diff --git a/pygitweb/main.py b/pygitweb/main.py
index 731353f..77fbceb 100644
--- a/pygitweb/main.py
+++ b/pygitweb/main.py
@@ -130,9 +130,9 @@ def _request_can_add_project(request: Request) -> bool:
 
 
 app.include_router(settings_router)
-app.include_router(board_router, prefix="/project/{project:path}")
-app.include_router(task_router, prefix="/project/{project:path}")
-app.include_router(comment_router, prefix="/project/{project:path}")
+app.include_router(board_router, prefix="/board")
+app.include_router(task_router, prefix="/tasks")
+app.include_router(comment_router, prefix="/comments")
 
 
 @app.on_event("startup")
diff --git a/pygitweb/static/board.js b/pygitweb/static/board.js
index 8712b5f..914cf0e 100644
--- a/pygitweb/static/board.js
+++ b/pygitweb/static/board.js
@@ -3,8 +3,7 @@
   var addCardButtons = document.querySelectorAll('.board-column-add-card');
 
   var ICONS_BASE = '/static/icons/';
-  var ICON_LIST = ['cloud.svg', 'cloud-filled.svg', 'cloud-storm.svg', 'cloud-down.svg', 'cloud-up.svg'];
-  var CLOUD_ICON_URL = ICONS_BASE + 'cloud.svg';
+  var ICON_LIST = ['cloud.svg', 'cloud-filled.svg', 'cloud-storm.svg', 'cloud-down.svg', 'cloud-up.svg', 'cloud-exclamation.svg'];
 
   function preloadIcons() {
     ICON_LIST.forEach(function(name) {
@@ -13,9 +12,13 @@
     });
   }
 
-  function assignCloudIconToCard(card) {
+  function setCardIcon(card, iconName) {
     var icon = card && card.querySelector && card.querySelector('.board-card-icon');
-    if (icon) icon.setAttribute('data-icon', 'cloud');
+    if (icon) icon.setAttribute('data-icon', iconName);
+  }
+
+  function assignCloudIconToCard(card) {
+    setCardIcon(card, 'cloud');
   }
 
   function assignCloudIconToAllCards() {
@@ -23,30 +26,149 @@
   }
 
   preloadIcons();
+  function initBoard() {
+    assignCloudIconToAllCards();
+    loadTasksForBoard();
+  }
   if (document.readyState === 'loading') {
-    document.addEventListener('DOMContentLoaded', assignCloudIconToAllCards);
+    document.addEventListener('DOMContentLoaded', initBoard);
   } else {
-    assignCloudIconToAllCards();
+    initBoard();
+  }
+
+  function getBoardContext() {
+    var boardColumns = document.querySelector('.board-columns');
+    var project = boardColumns && boardColumns.dataset.project;
+    var board = boardColumns && boardColumns.dataset.board;
+    if (!project && location.pathname) {
+      var m = location.pathname.match(/^\/project\/(.+?)\/board\/?$/);
+      if (m) project = m[1];
+    }
+    if (!board) board = (new URLSearchParams(location.search)).get('b') || 'Tasks';
+    return { project: project || '', board: board || 'Tasks' };
+  }
+
+  function createCardFromTask(task) {
+    if (!template || !template.content) return null;
+    var clone = template.content.cloneNode(true);
+    var card = clone.querySelector && clone.querySelector('.board-card');
+    if (!card) return null;
+    var status = task.status || 'TODO';
+    card.dataset.taskRef = task.ref || '';
+    card.dataset.taskStatus = status;
+    var titleEl = card.querySelector('.board-card-title');
+    if (titleEl) titleEl.textContent = task.title || 'Untitled';
+    var descEl = card.querySelector('.board-card-description');
+    if (descEl) descEl.textContent = task.description || '';
+    setCardIcon(card, 'cloud-filled');
+    var meta = card.querySelector('.board-card-meta');
+    if (meta) {
+      var badge = meta.querySelector('.badge');
+      var assigneeSpan = meta.querySelectorAll('.text-truncate')[0];
+      var commentsSpan = meta.querySelector('.ms-auto');
+      if (badge) {
+        badge.textContent = task.priority || 'LOW';
+        badge.className = 'badge badge-outline text-' + (task.priority === 'CRITICAL' ? 'danger' : task.priority === 'HIGH' ? 'warning' : 'secondary');
+      }
+      if (assigneeSpan) assigneeSpan.textContent = task.assignee || 'None';
+      if (commentsSpan) commentsSpan.textContent = (task.comments_count || 0) + ' comment' + ((task.comments_count || 0) !== 1 ? 's' : '');
+    }
+    var details = card.querySelector('.board-card-details');
+    if (details) {
+      var fullDesc = details.querySelector('.board-card-description-full');
+      var dueDate = details.querySelector('.board-card-due-date');
+      var updatedAt = details.querySelector('.board-card-updated-at');
+      var statusEl = details.querySelector('.board-card-status');
+      var priorityEl = details.querySelector('.board-card-priority');
+      var assigneeEl = details.querySelector('.board-card-assignee');
+      var commentsCountEl = details.querySelector('.board-card-comments-count');
+      if (fullDesc) fullDesc.textContent = task.description || '';
+      if (dueDate) dueDate.textContent = task.due_date || 'None';
+      if (updatedAt) updatedAt.textContent = task.updated_at || 'None';
+      if (statusEl) statusEl.textContent = status;
+      if (priorityEl) priorityEl.textContent = task.priority || 'LOW';
+      if (assigneeEl) assigneeEl.textContent = task.assignee || 'None';
+      if (commentsCountEl) commentsCountEl.textContent = (task.comments_count || 0) + ' comment' + ((task.comments_count || 0) !== 1 ? 's' : '');
+    }
+    return clone;
+  }
+
+  function loadTasksForBoard() {
+    var ctx = getBoardContext();
+    if (!ctx.project) return;
+    var listUrl = '/tasks/list?' + new URLSearchParams({ project: ctx.project, board: ctx.board }).toString();
+    fetch(listUrl).then(function(res) {
+      if (!res.ok) return;
+      return res.json();
+    }).then(function(tasks) {
+      if (!Array.isArray(tasks)) return;
+      var byStatus = {};
+      tasks.forEach(function(task) {
+        var s = task.status || 'TODO';
+        if (!byStatus[s]) byStatus[s] = [];
+        byStatus[s].push(task);
+      });
+      var columns = document.querySelectorAll('.board-column.dropzone');
+      columns.forEach(function(column) {
+        var status = column.dataset.status || 'TODO';
+        var cardsContainer = column.querySelector('.board-column-cards');
+        if (!cardsContainer) return;
+        cardsContainer.innerHTML = '';
+        (byStatus[status] || []).forEach(function(task) {
+          var frag = createCardFromTask(task);
+          if (frag) cardsContainer.appendChild(frag);
+        });
+        updateColumnBadge(column);
+      });
+    }).catch(function() {});
   }
 
   function addDefaultCard(ev) {
     if (!template || !template.content) return;
     var column = ev.currentTarget.closest('.board-column');
     var cardsContainer = column && column.querySelector('.board-column-cards');
-    if (!cardsContainer) return;
+    var boardColumns = document.querySelector('.board-columns');
+    if (!cardsContainer || !boardColumns) return;
+    var ctx = getBoardContext();
+    var project = ctx.project;
+    var board = ctx.board;
+    if (!project) return;
     var clone = template.content.cloneNode(true);
     var card = clone.querySelector && clone.querySelector('.board-card');
-    if (card) {
-      var titleEl = card.querySelector('.board-card-title');
-      if (titleEl) titleEl.textContent = 'New task';
-      assignCloudIconToCard(card);
-    }
+    if (!card) return;
+    var titleEl = card.querySelector('.board-card-title');
+    if (titleEl) titleEl.textContent = 'New task';
+    assignCloudIconToCard(card);
     cardsContainer.appendChild(clone);
     var badge = column.querySelector('.board-column-add-card');
     if (badge) {
-      var n = cardsContainer.querySelectorAll('.board-card').length;
-      badge.textContent = n;
+      badge.textContent = cardsContainer.querySelectorAll('.board-card').length;
     }
+    var status = column.dataset.status || 'TODO';
+    setCardIcon(card, 'cloud-up');
+    var createUrl = '/tasks/create?' + new URLSearchParams({
+      project: project,
+      board: board,
+      title: 'New task',
+      description: '',
+      status: status,
+      priority: 'LOW',
+      assignee: '',
+      due_date: ''
+    }).toString();
+    fetch(createUrl, { method: 'POST' }).then(function(res) {
+      if (res.ok) {
+        return res.json().then(function(body) {
+          setCardIcon(card, 'cloud-filled');
+          if (body.ref) card.dataset.taskRef = body.ref;
+        });
+      }
+      if (res.status >= 400 && res.status < 600) {
+        setCardIcon(card, 'cloud-exclamation');
+      }
+    }).catch(function() {
+      setCardIcon(card, 'cloud-exclamation');
+    });
   }
 
   addCardButtons.forEach(function(btn) {
@@ -97,6 +219,35 @@
     var statusEl = popoutCard.querySelector('.board-card-status');
     var priorityEl = popoutCard.querySelector('.board-card-priority');
 
+    function pushTaskUpdate() {
+      var taskRef = popoutCard.dataset.taskRef;
+      if (!taskRef) return;
+      var ctx = getBoardContext();
+      if (!ctx.project) return;
+      var tEl = popoutCard.querySelector('.board-card-title');
+      var dEl = popoutCard.querySelector('.board-card-description-full');
+      var dueEl_ = popoutCard.querySelector('.board-card-due-date');
+      var sEl = popoutCard.querySelector('.board-card-status');
+      var pEl = popoutCard.querySelector('.board-card-priority');
+      var aEl = popoutCard.querySelector('.board-card-assignee');
+      var dueVal = (dueEl_ && dueEl_.textContent) ? dueEl_.textContent.trim() : '';
+      var statusDisplay = (sEl && sEl.textContent) ? sEl.textContent.trim() : 'TODO';
+      var priorityDisplay = (pEl && pEl.textContent) ? pEl.textContent.trim() : 'LOW';
+      var body = {
+        title: (tEl && tEl.textContent) ? tEl.textContent.trim() || 'Untitled' : 'Untitled',
+        description: (dEl && dEl.textContent) ? dEl.textContent.trim() : '',
+        status: statusDisplay ? statusDisplay.replace(/\s+/g, '_').toUpperCase() : 'TODO',
+        priority: priorityDisplay ? priorityDisplay.replace(/\s+/g, '_').toUpperCase() : 'LOW',
+        assignee: (aEl && aEl.textContent && aEl.textContent.trim() !== 'None') ? aEl.textContent.trim() : null,
+        due_date: (dueVal && dueVal !== 'None') ? dueVal : null
+      };
+      fetch('/tasks/update?' + new URLSearchParams({ project: ctx.project, board: ctx.board, task: taskRef }).toString(), {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify(body)
+      }).catch(function() {});
+    }
+
     if (titleEl) titleEl.classList.add('board-card-editable');
     function commitTitle() {
       var input = popoutCard.querySelector('.board-card-title-edit');
@@ -105,6 +256,7 @@
       titleEl.textContent = val;
       titleEl.classList.remove('d-none');
       input.remove();
+      pushTaskUpdate();
     }
     if (titleEl) {
       titleEl.addEventListener('click', function () {
@@ -129,6 +281,7 @@
       descEl.textContent = val || '';
       descEl.classList.remove('d-none');
       textarea.remove();
+      pushTaskUpdate();
     }
     if (descEl) {
       descEl.addEventListener('click', function () {
@@ -152,6 +305,7 @@
       dueEl.textContent = val || 'None';
       dueEl.classList.remove('d-none');
       input.remove();
+      pushTaskUpdate();
     }
     if (dueEl) {
       dueEl.addEventListener('click', function () {
@@ -175,6 +329,9 @@
       statusEl.textContent = display || 'TODO';
       statusEl.classList.remove('d-none');
       sel.remove();
+      var statusValue = display ? display.replace(/\s+/g, '_').toUpperCase() : 'TODO';
+      popoutCard.dataset.taskStatus = statusValue;
+      pushTaskUpdate();
     }
     if (statusEl) {
       statusEl.addEventListener('click', function () {
@@ -203,6 +360,7 @@
       priorityEl.textContent = display || 'LOW';
       priorityEl.classList.remove('d-none');
       sel.remove();
+      pushTaskUpdate();
     }
     if (priorityEl) {
       priorityEl.addEventListener('click', function () {
@@ -275,6 +433,18 @@
   function handleDragLeave(e) {
     e.currentTarget.classList.remove('border-primary');
   }
+  function pushTaskStatusUpdate(card, newStatus) {
+    var taskRef = card && card.dataset.taskRef;
+    if (!taskRef) return;
+    var ctx = getBoardContext();
+    if (!ctx.project) return;
+    fetch('/tasks/update?' + new URLSearchParams({ project: ctx.project, board: ctx.board, task: taskRef }).toString(), {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ status: newStatus || 'TODO' })
+    }).catch(function() {});
+  }
+
   function handleDrop(e) {
     e.preventDefault();
     e.currentTarget.classList.remove('border-primary');
@@ -284,11 +454,12 @@
     if (!targetCards) return;
     var sourceColumn = draggedCard.closest && draggedCard.closest('.board-column');
     if (sourceColumn === targetColumn) return;
-    draggedCard.dataset.taskStatus = targetColumn.dataset.status || '';
+    var newStatus = targetColumn.dataset.status || 'TODO';
+    draggedCard.dataset.taskStatus = newStatus;
     targetCards.appendChild(draggedCard);
     updateColumnBadge(sourceColumn);
     updateColumnBadge(targetColumn);
-    // TODO: POST to update task status on server (e.g. /project/.../tasks/update?task=... with body { status })
+    pushTaskStatusUpdate(draggedCard, newStatus);
   }
 
   if (boardColumns) {
diff --git a/pygitweb/static/icons/cloud-exclamation.svg b/pygitweb/static/icons/cloud-exclamation.svg
new file mode 100644
index 0000000..cdd3a92
--- /dev/null
+++ b/pygitweb/static/icons/cloud-exclamation.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-cloud-exclamation"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 18.004h-8.343c-2.572 -.004 -4.657 -2.011 -4.657 -4.487c0 -2.475 2.085 -4.482 4.657 -4.482c.393 -1.762 1.794 -3.2 3.675 -3.773c1.88 -.572 3.956 -.193 5.444 1c1.488 1.19 2.162 3.007 1.77 4.769h.99c1.374 0 2.562 .805 3.121 1.972" /><path d="M19 16v3" /><path d="M19 22v.01" /></svg>
\ No newline at end of file
diff --git a/pygitweb/static/main.css b/pygitweb/static/main.css
index d3864b3..fd20fd6 100644
--- a/pygitweb/static/main.css
+++ b/pygitweb/static/main.css
@@ -643,6 +643,18 @@ tbody tr:hover {
 	-webkit-mask-position: center;
 	mask-position: center;
 }
+.board-card-icon[data-icon="cloud-up"] {
+	-webkit-mask-image: url(/static/icons/cloud-up.svg);
+	mask-image: url(/static/icons/cloud-up.svg);
+}
+.board-card-icon[data-icon="cloud-filled"] {
+	-webkit-mask-image: url(/static/icons/cloud-filled.svg);
+	mask-image: url(/static/icons/cloud-filled.svg);
+}
+.board-card-icon[data-icon="cloud-exclamation"] {
+	-webkit-mask-image: url(/static/icons/cloud-exclamation.svg);
+	mask-image: url(/static/icons/cloud-exclamation.svg);
+}
 .board-card .board-card-title {
 	padding-right: 1.75rem;
 }
diff --git a/pygitweb/tasks.py b/pygitweb/tasks.py
index b7e226d..4310624 100644
--- a/pygitweb/tasks.py
+++ b/pygitweb/tasks.py
@@ -85,11 +85,14 @@ def create_board_for_project(
 
 # ---------- Board Routes ----------
 
-board_router = APIRouter(prefix="/boards", tags=["boards"])
+board_router = APIRouter(tags=["boards"])
 
 
 @board_router.get("/list", response_class=JSONResponse)
-def boards_list(request: Request, project: str) -> JSONResponse:
+def boards_list(
+    request: Request,
+    project: str = Query(..., description="Project path"),
+) -> JSONResponse:
     """List all boards for the project."""
     _require_auth_disabled()
     _validate_project(project)
@@ -113,7 +116,7 @@ def boards_list(request: Request, project: str) -> JSONResponse:
 @board_router.post("/create", response_class=JSONResponse)
 def boards_create(
     request: Request,
-    project: str,
+    project: str = Query(..., description="Project path"),
     name: str = Query(..., description="Board name"),
     description: str = Query("", description="Board description"),
 ) -> JSONResponse:
@@ -135,7 +138,7 @@ def boards_create(
 @board_router.post("/delete", response_class=JSONResponse)
 def boards_delete(
     request: Request,
-    project: str,
+    project: str = Query(..., description="Project path"),
     name: str = Query(..., description="Board name"),
 ) -> JSONResponse:
     """Delete a board (removes ref; tag object remains in ODB)."""
@@ -151,7 +154,7 @@ def boards_delete(
 
 # ---------- Task Routes ----------
 
-task_router = APIRouter(prefix="/tasks", tags=["tasks"])
+task_router = APIRouter(tags=["tasks"])
 
 
 def _task_to_json(t: Task) -> dict[str, Any]:
@@ -184,18 +187,7 @@ def get_board_tasks_grouped(
     """
     repo = open_repo(project)
     ref = _board_ref(board_name)
-    try:
-        b = get_board(repo, ref)
-    except (ValueError, KeyError):
-        return [
-            {"status": s, "label": s.replace("_", " ").title(), "tasks": []}
-            for s in BOARD_STATUS_ORDER
-        ]
-    if not b:
-        return [
-            {"status": s, "label": s.replace("_", " ").title(), "tasks": []}
-            for s in BOARD_STATUS_ORDER
-        ]
+    b = get_board(repo, ref)
     task_oids = getattr(b, "tasks", []) or []
     by_status: dict[str, list[dict[str, Any]]] = {s: [] for s in BOARD_STATUS_ORDER}
     for oid_hex in task_oids:
@@ -219,7 +211,7 @@ def get_board_tasks_grouped(
 @task_router.get("/list", response_class=JSONResponse)
 def task_list(
     request: Request,
-    project: str,
+    project: str = Query(..., description="Project path"),
     board: str = Query(..., description="Board name"),
 ) -> JSONResponse:
     """List all tasks on a board."""
@@ -246,7 +238,7 @@ def task_list(
 @task_router.post("/create", response_class=JSONResponse)
 def task_create(
     request: Request,
-    project: str,
+    project: str = Query(..., description="Project path"),
     board: str = Query(..., description="Board name"),
     title: str = Query(..., description="Task title"),
     description: str = Query("", description="Task description"),
@@ -295,7 +287,7 @@ def task_create(
 @task_router.post("/update", response_class=JSONResponse)
 async def task_update(
     request: Request,
-    project: str,
+    project: str = Query(..., description="Project path"),
     board: str = Query(..., description="Board name"),
     task_ref: str = Query(..., alias="task", description="Task ref (e.g. refs/tasks/task_123)"),
 ) -> JSONResponse:
@@ -342,7 +334,7 @@ async def task_update(
 @task_router.post("/delete", response_class=JSONResponse)
 def task_delete(
     request: Request,
-    project: str,
+    project: str = Query(..., description="Project path"),
     board: str = Query(..., description="Board name"),
     task_ref: str = Query(..., alias="task", description="Task ref"),
 ) -> JSONResponse:
@@ -369,7 +361,7 @@ def task_delete(
 
 # ---------- Comment Routes ----------
 
-comment_router = APIRouter(prefix="/comments", tags=["comments"])
+comment_router = APIRouter(tags=["comments"])
 
 
 def _comment_to_json(c: Comment) -> dict[str, Any]:
@@ -384,7 +376,7 @@ def _comment_to_json(c: Comment) -> dict[str, Any]:
 @comment_router.get("/list", response_class=JSONResponse)
 def comment_list(
     request: Request,
-    project: str,
+    project: str = Query(..., description="Project path"),
     board: str = Query(..., description="Board name"),
     task: str = Query(..., description="Task ref (e.g. refs/tasks/task_123)"),
 ) -> JSONResponse:
@@ -412,7 +404,7 @@ def comment_list(
 @comment_router.post("/create", response_class=JSONResponse)
 def comment_create(
     request: Request,
-    project: str,
+    project: str = Query(..., description="Project path"),
     board: str = Query(..., description="Board name"),
     task: str = Query(..., description="Task ref"),
     content: str = Query(..., description="Comment content"),
@@ -439,7 +431,7 @@ def comment_create(
 @comment_router.post("/update", response_class=JSONResponse)
 async def comment_update(
     request: Request,
-    project: str,
+    project: str = Query(..., description="Project path"),
     board: str = Query(..., description="Board name"),
     task: str = Query(..., description="Task ref (to update task.comments)"),
     comment_oid: str = Query(..., alias="comment", description="Comment OID hex"),
@@ -474,7 +466,7 @@ async def comment_update(
 @comment_router.post("/delete", response_class=JSONResponse)
 def comment_delete(
     request: Request,
-    project: str,
+    project: str = Query(..., description="Project path"),
     board: str = Query(..., description="Board name"),
     comment_oid: str = Query(..., alias="comment", description="Comment OID hex"),
 ) -> JSONResponse:
diff --git a/pygitweb/templates/board.html b/pygitweb/templates/board.html
index 9d3695d..97ebd89 100644
--- a/pygitweb/templates/board.html
+++ b/pygitweb/templates/board.html
@@ -10,7 +10,7 @@
   <span>{{ board_name }} board</span>
 </h1>
 
-<div class="board-columns d-flex gap-3 overflow-auto pb-2" style="min-height: 70vh;">
+<div class="board-columns d-flex gap-3 overflow-auto pb-2" style="min-height: 70vh;" data-project="{{ project }}" data-board="{{ board_name }}">
   {% for col in columns %}
   <div class="board-column flex-shrink-0 rounded border p-2 dropzone"
        data-status="{{ col.status }}"
