From e60734f3e71b052d51181fdaede72e10864f544a Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 12 Feb 2026 20:04:38 +0000 Subject: [PATCH 1/8] ci: add migration integrity gate for migration-relevant changes --- .github/workflows/ci.yml | 30 ++++++++++++++++++++++++++++++ Makefile | 22 ++++++++++++++++++++++ docs/03-development.md | 22 +++++++++++++++++++++- 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f17053e0..4dcf4890 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,6 +62,36 @@ jobs: nextjs-${{ runner.os }}-node-${{ steps.setup-node.outputs.node-version }}- + + - name: Run migration integrity gate + run: | + set -euo pipefail + + if [ "${{ github.event_name }}" = "pull_request" ]; then + BASE_SHA="${{ github.event.pull_request.base.sha }}" + HEAD_SHA="${{ github.sha }}" + git fetch --no-tags --depth=1 origin "$BASE_SHA" + else + BASE_SHA="${{ github.event.before }}" + HEAD_SHA="${{ github.sha }}" + fi + + CHANGED_FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA") + echo "Changed files:" + echo "$CHANGED_FILES" + + if ! echo "$CHANGED_FILES" | grep -Eq '^backend/(app/models|db|migrations|alembic\.ini)'; then + echo "No migration-relevant backend changes detected; skipping migration gate." + exit 0 + fi + + if echo "$CHANGED_FILES" | grep -Eq '^backend/app/models/' && ! echo "$CHANGED_FILES" | grep -Eq '^backend/migrations/versions/'; then + echo "Model changes detected without a migration under backend/migrations/versions/." + exit 1 + fi + + make backend-migration-check + - name: Run backend checks env: # Keep CI builds deterministic. diff --git a/Makefile b/Makefile index b64228ff..4ecffdc3 100644 --- a/Makefile +++ b/Makefile @@ -104,6 +104,28 @@ frontend-test: frontend-tooling ## Frontend tests (vitest) backend-migrate: ## Apply backend DB migrations (uses backend/migrations) cd $(BACKEND_DIR) && uv run alembic upgrade head +.PHONY: backend-migration-check +backend-migration-check: ## Validate Alembic migrations on clean Postgres (upgrade head + single-head sanity) + @set -euo pipefail; \ + CONTAINER_NAME="mc-migration-check-$$RANDOM"; \ + docker run -d --rm --name $$CONTAINER_NAME -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=migration_ci -p 55432:5432 postgres:16 >/dev/null; \ + cleanup() { docker rm -f $$CONTAINER_NAME >/dev/null 2>&1 || true; }; \ + trap cleanup EXIT; \ + for i in $$(seq 1 30); do \ + if docker exec $$CONTAINER_NAME pg_isready -U postgres -d migration_ci >/dev/null 2>&1; then break; fi; \ + sleep 1; \ + if [ $$i -eq 30 ]; then echo "Postgres did not become ready"; exit 1; fi; \ + done; \ + cd $(BACKEND_DIR) && \ + AUTH_MODE=local \ + LOCAL_AUTH_TOKEN=ci-local-token-ci-local-token-ci-local-token-ci-local-token \ + DATABASE_URL=postgresql+psycopg://postgres:postgres@localhost:55432/migration_ci \ + uv run alembic upgrade head && \ + AUTH_MODE=local \ + LOCAL_AUTH_TOKEN=ci-local-token-ci-local-token-ci-local-token-ci-local-token \ + DATABASE_URL=postgresql+psycopg://postgres:postgres@localhost:55432/migration_ci \ + uv run alembic heads | grep -q "(head)" + .PHONY: build build: frontend-build ## Build artifacts diff --git a/docs/03-development.md b/docs/03-development.md index 4949c0c3..b3e1417c 100644 --- a/docs/03-development.md +++ b/docs/03-development.md @@ -1,3 +1,23 @@ # Development workflow -Placeholder: see root `README.md` for current setup steps. +## Migration integrity gate (CI) + +CI enforces a migration integrity gate to prevent merge-time schema breakages. + +### What it validates + +- Alembic migrations can apply from a clean Postgres database (`upgrade head`) +- Alembic revision graph resolves to a head revision after migration apply +- On migration-relevant PRs, CI also checks that model changes are accompanied by migration updates + +If any of these checks fails, CI fails and the PR is blocked. + +### Local reproduction + +From repo root: + +```bash +make backend-migration-check +``` + +This command starts a temporary Postgres container, runs migration checks, and cleans up the container. From 426326e2af2580b008ea9ab075fd3740ebae8167 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 13 Feb 2026 09:05:21 +0000 Subject: [PATCH 2/8] ci(migrations): enforce graph + reversible cycle checks; fix FK downgrade naming --- Makefile | 9 ++- ...76359_sync_agent_gateway_linkage_schema.py | 4 +- backend/scripts/check_migration_graph.py | 77 +++++++++++++++++++ 3 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 backend/scripts/check_migration_graph.py diff --git a/Makefile b/Makefile index 4ecffdc3..b126da29 100644 --- a/Makefile +++ b/Makefile @@ -105,8 +105,9 @@ backend-migrate: ## Apply backend DB migrations (uses backend/migrations) cd $(BACKEND_DIR) && uv run alembic upgrade head .PHONY: backend-migration-check -backend-migration-check: ## Validate Alembic migrations on clean Postgres (upgrade head + single-head sanity) +backend-migration-check: ## Validate migration graph + reversible path on clean Postgres @set -euo pipefail; \ + (cd $(BACKEND_DIR) && uv run python scripts/check_migration_graph.py); \ CONTAINER_NAME="mc-migration-check-$$RANDOM"; \ docker run -d --rm --name $$CONTAINER_NAME -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=migration_ci -p 55432:5432 postgres:16 >/dev/null; \ cleanup() { docker rm -f $$CONTAINER_NAME >/dev/null 2>&1 || true; }; \ @@ -124,7 +125,11 @@ backend-migration-check: ## Validate Alembic migrations on clean Postgres (upgra AUTH_MODE=local \ LOCAL_AUTH_TOKEN=ci-local-token-ci-local-token-ci-local-token-ci-local-token \ DATABASE_URL=postgresql+psycopg://postgres:postgres@localhost:55432/migration_ci \ - uv run alembic heads | grep -q "(head)" + uv run alembic downgrade base && \ + AUTH_MODE=local \ + LOCAL_AUTH_TOKEN=ci-local-token-ci-local-token-ci-local-token-ci-local-token \ + DATABASE_URL=postgresql+psycopg://postgres:postgres@localhost:55432/migration_ci \ + uv run alembic upgrade head .PHONY: build build: frontend-build ## Build artifacts diff --git a/backend/migrations/versions/b308f2876359_sync_agent_gateway_linkage_schema.py b/backend/migrations/versions/b308f2876359_sync_agent_gateway_linkage_schema.py index 3863b4d8..3d1a07a4 100644 --- a/backend/migrations/versions/b308f2876359_sync_agent_gateway_linkage_schema.py +++ b/backend/migrations/versions/b308f2876359_sync_agent_gateway_linkage_schema.py @@ -22,7 +22,7 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.add_column('agents', sa.Column('gateway_id', sa.Uuid(), nullable=False)) op.create_index(op.f('ix_agents_gateway_id'), 'agents', ['gateway_id'], unique=False) - op.create_foreign_key(None, 'agents', 'gateways', ['gateway_id'], ['id']) + op.create_foreign_key('fk_agents_gateway_id_gateways', 'agents', 'gateways', ['gateway_id'], ['id']) op.drop_column('gateways', 'main_session_key') # ### end Alembic commands ### @@ -30,7 +30,7 @@ def upgrade() -> None: def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.add_column('gateways', sa.Column('main_session_key', sa.VARCHAR(), autoincrement=False, nullable=False)) - op.drop_constraint(None, 'agents', type_='foreignkey') + op.drop_constraint('fk_agents_gateway_id_gateways', 'agents', type_='foreignkey') op.drop_index(op.f('ix_agents_gateway_id'), table_name='agents') op.drop_column('agents', 'gateway_id') # ### end Alembic commands ### diff --git a/backend/scripts/check_migration_graph.py b/backend/scripts/check_migration_graph.py new file mode 100644 index 00000000..444583f3 --- /dev/null +++ b/backend/scripts/check_migration_graph.py @@ -0,0 +1,77 @@ +"""Migration graph integrity checks for CI. + +Checks: +- alembic script graph can be loaded (detects broken/missing links) +- single head by default (unless ALLOW_MULTIPLE_HEADS=true) +- no orphan revisions (all revisions reachable from heads) +""" + +from __future__ import annotations + +import os +import sys +from pathlib import Path + +from alembic.config import Config +from alembic.script import ScriptDirectory + + +def _truthy(value: str | None) -> bool: + return (value or "").strip().lower() in {"1", "true", "yes", "on"} + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + alembic_ini = root / "alembic.ini" + cfg = Config(str(alembic_ini)) + cfg.attributes["configure_logger"] = False + + try: + script = ScriptDirectory.from_config(cfg) + except Exception as exc: # pragma: no cover - CI path + print(f"ERROR: unable to load Alembic script directory: {exc}") + return 1 + + try: + heads = list(script.get_heads()) + except Exception as exc: # pragma: no cover - CI path + print(f"ERROR: unable to resolve Alembic heads: {exc}") + return 1 + + allow_multiple_heads = _truthy(os.getenv("ALLOW_MULTIPLE_HEADS")) + if not heads: + print("ERROR: no Alembic heads found") + return 1 + + if len(heads) > 1 and not allow_multiple_heads: + print("ERROR: multiple Alembic heads detected (set ALLOW_MULTIPLE_HEADS=true only for intentional merge windows)") + for h in heads: + print(f" - {h}") + return 1 + + try: + reachable = {rev.revision for rev in script.walk_revisions(base="base", head="heads") if rev.revision} + except Exception as exc: # pragma: no cover - CI path + print(f"ERROR: failed while walking Alembic revision graph: {exc}") + return 1 + + all_revisions = { + rev.revision + for rev in script.revision_map._revision_map.values() # type: ignore[attr-defined] + if getattr(rev, "revision", None) + } + orphans = sorted(all_revisions - reachable) + if orphans: + print("ERROR: orphan Alembic revisions detected (not reachable from heads):") + for rev in orphans: + print(f" - {rev}") + return 1 + + print("OK: migration graph integrity passed") + print(f"Heads: {', '.join(heads)}") + print(f"Reachable revisions: {len(reachable)}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From bfd7d5b992653364d709a22f775b30f3793eb496 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 13 Feb 2026 15:18:56 +0530 Subject: [PATCH 3/8] feat: add status_requested field to task updates and implement related logic in task approval process --- backend/app/api/tasks.py | 9 +++- .../tests/test_tasks_done_approval_gate.py | 47 +++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/backend/app/api/tasks.py b/backend/app/api/tasks.py index bcb0aada..83f68d18 100644 --- a/backend/app/api/tasks.py +++ b/backend/app/api/tasks.py @@ -1054,12 +1054,16 @@ async def update_task( updates.pop("comment", None) updates.pop("depends_on_task_ids", None) updates.pop("tag_ids", None) + requested_status = payload.status if "status" in payload.model_fields_set else None update = _TaskUpdateInput( task=task, actor=actor, board_id=board_id, previous_status=previous_status, previous_assigned=previous_assigned, + status_requested=( + requested_status is not None and requested_status != previous_status + ), updates=updates, comment=comment, depends_on_task_ids=depends_on_task_ids, @@ -1299,6 +1303,7 @@ class _TaskUpdateInput: board_id: UUID previous_status: str previous_assigned: UUID | None + status_requested: bool updates: dict[str, object] comment: str | None depends_on_task_ids: list[UUID] | None @@ -1597,7 +1602,7 @@ async def _apply_lead_task_update( task_id=update.task.id, previous_status=update.previous_status, target_status=update.task.status, - status_requested="status" in update.updates, + status_requested=update.status_requested, ) await _require_review_before_done_when_enabled( session, @@ -1878,7 +1883,7 @@ async def _finalize_updated_task( task_id=update.task.id, previous_status=update.previous_status, target_status=update.task.status, - status_requested="status" in update.updates, + status_requested=update.status_requested, ) await _require_review_before_done_when_enabled( session, diff --git a/backend/tests/test_tasks_done_approval_gate.py b/backend/tests/test_tasks_done_approval_gate.py index a3d6deea..308ac97e 100644 --- a/backend/tests/test_tasks_done_approval_gate.py +++ b/backend/tests/test_tasks_done_approval_gate.py @@ -356,6 +356,53 @@ async def test_update_task_allows_status_change_with_pending_approval_when_toggl await engine.dispose() +@pytest.mark.asyncio +async def test_update_task_allows_dependency_change_with_pending_approval() -> None: + engine = await _make_engine() + try: + async with await _make_session(engine) as session: + board, task, _agent = await _seed_board_task_and_agent( + session, + task_status="review", + require_approval_for_done=False, + block_status_changes_with_pending_approval=True, + ) + dependency = Task( + id=uuid4(), + board_id=board.id, + title="Dependency", + status="inbox", + ) + session.add(dependency) + session.add( + Approval( + id=uuid4(), + board_id=board.id, + task_id=task.id, + action_type="task.execute", + confidence=70, + status="pending", + ), + ) + await session.commit() + + updated = await tasks_api.update_task( + payload=TaskUpdate( + status="review", + depends_on_task_ids=[dependency.id], + ), + task=task, + session=session, + actor=ActorContext(actor_type="user"), + ) + + assert updated.depends_on_task_ids == [dependency.id] + assert updated.status == "inbox" + assert updated.blocked_by_task_ids == [dependency.id] + finally: + await engine.dispose() + + @pytest.mark.asyncio async def test_update_task_rejects_status_change_for_pending_multi_task_link_when_toggle_enabled() -> ( None From c84d79e08484f31e38f957d28017f636593f2159 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 13 Feb 2026 09:49:42 +0000 Subject: [PATCH 4/8] fix(ci): remove unused import in migration graph checker --- backend/scripts/check_migration_graph.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/scripts/check_migration_graph.py b/backend/scripts/check_migration_graph.py index 444583f3..30dc9f67 100644 --- a/backend/scripts/check_migration_graph.py +++ b/backend/scripts/check_migration_graph.py @@ -9,7 +9,6 @@ Checks: from __future__ import annotations import os -import sys from pathlib import Path from alembic.config import Config From efa3587e770dfaa599ed1c6cf47e70759102f732 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 13 Feb 2026 15:35:21 +0530 Subject: [PATCH 5/8] feat: add DependencyBanner component and refactor dependency display logic in task view --- frontend/src/app/boards/[boardId]/page.tsx | 93 +++++++------------ .../components/molecules/DependencyBanner.tsx | 92 ++++++++++++++++++ 2 files changed, 128 insertions(+), 57 deletions(-) create mode 100644 frontend/src/components/molecules/DependencyBanner.tsx diff --git a/frontend/src/app/boards/[boardId]/page.tsx b/frontend/src/app/boards/[boardId]/page.tsx index 6449ea63..266ce164 100644 --- a/frontend/src/app/boards/[boardId]/page.tsx +++ b/frontend/src/app/boards/[boardId]/page.tsx @@ -24,6 +24,10 @@ import { Markdown } from "@/components/atoms/Markdown"; import { StatusDot } from "@/components/atoms/StatusDot"; import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; import { TaskBoard } from "@/components/organisms/TaskBoard"; +import { + DependencyBanner, + type DependencyBannerDependency, +} from "@/components/molecules/DependencyBanner"; import { DashboardShell } from "@/components/templates/DashboardShell"; import { BoardChatComposer } from "@/components/BoardChatComposer"; import { Button } from "@/components/ui/button"; @@ -2211,6 +2215,30 @@ export default function BoardDetailPage() { [loadComments], ); + const selectedTaskDependencies = useMemo(() => { + if (!selectedTask) return []; + const blockedDependencyIds = new Set(selectedTask.blocked_by_task_ids ?? []); + return (selectedTask.depends_on_task_ids ?? []).map((dependencyId) => { + const dependencyTask = taskById.get(dependencyId); + const statusLabel = dependencyTask?.status + ? dependencyTask.status.replace(/_/g, " ") + : "unknown"; + return { + id: dependencyId, + title: dependencyTask?.title ?? dependencyId, + statusLabel, + isBlocking: blockedDependencyIds.has(dependencyId), + isDone: dependencyTask?.status === "done", + disabled: !dependencyTask, + onClick: dependencyTask + ? () => { + openComments({ id: dependencyId }); + } + : undefined, + }; + }); + }, [openComments, selectedTask, taskById]); + useEffect(() => { if (!taskIdFromUrl) return; if (openedTaskIdFromUrlRef.current === taskIdFromUrl) return; @@ -3382,63 +3410,14 @@ export default function BoardDetailPage() {

Dependencies

- {selectedTask?.depends_on_task_ids?.length ? ( -
- {selectedTask.depends_on_task_ids.map((depId) => { - const depTask = taskById.get(depId); - const title = depTask?.title ?? depId; - const statusLabel = depTask?.status - ? depTask.status.replace(/_/g, " ") - : "unknown"; - const isDone = depTask?.status === "done"; - const isBlocking = ( - selectedTask.blocked_by_task_ids ?? [] - ).includes(depId); - return ( - - ); - })} -
- ) : ( -

No dependencies.

- )} - {selectedTask?.is_blocked ? ( -
- Blocked by incomplete dependencies. -
- ) : null} + + {selectedTask?.is_blocked + ? "Blocked by incomplete dependencies." + : null} +
diff --git a/frontend/src/components/molecules/DependencyBanner.tsx b/frontend/src/components/molecules/DependencyBanner.tsx new file mode 100644 index 00000000..aad178d2 --- /dev/null +++ b/frontend/src/components/molecules/DependencyBanner.tsx @@ -0,0 +1,92 @@ +import { type ReactNode } from "react"; + +import { cn } from "@/lib/utils"; + +export interface DependencyBannerDependency { + id: string; + title: string; + statusLabel: string; + isBlocking?: boolean; + isDone?: boolean; + onClick?: () => void; + disabled?: boolean; +} + +interface DependencyBannerProps { + variant?: DependencyBannerVariant; + dependencies?: DependencyBannerDependency[]; + children?: ReactNode; + className?: string; + emptyMessage?: string; +} + +const toneClassByVariant: Record = { + blocked: "border-rose-200 bg-rose-50 text-rose-700", + resolved: "border-blue-200 bg-blue-50 text-blue-700", +}; + +export function DependencyBanner({ + variant = "blocked", + dependencies = [], + children, + className, + emptyMessage = "No dependencies.", +}: DependencyBannerProps) { + return ( +
+ {dependencies.length > 0 ? ( + dependencies.map((dependency) => { + const isBlocking = dependency.isBlocking === true; + const isDone = dependency.isDone === true; + return ( + + ); + }) + ) : ( +

{emptyMessage}

+ )} + {children ? ( +
+ {children} +
+ ) : null} +
+ ); +} From 99da4681249a415c0a9d77e5b1bdaaeeb2f5cf54 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 13 Feb 2026 10:06:24 +0000 Subject: [PATCH 6/8] fix(ci): make migration graph checker mypy-clean --- backend/scripts/check_migration_graph.py | 25 ++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/backend/scripts/check_migration_graph.py b/backend/scripts/check_migration_graph.py index 30dc9f67..1d159c86 100644 --- a/backend/scripts/check_migration_graph.py +++ b/backend/scripts/check_migration_graph.py @@ -49,21 +49,30 @@ def main() -> int: return 1 try: - reachable = {rev.revision for rev in script.walk_revisions(base="base", head="heads") if rev.revision} + reachable: set[str] = set() + for walk_rev in script.walk_revisions(base="base", head="heads"): + if walk_rev is None: + continue + if walk_rev.revision: + reachable.add(walk_rev.revision) except Exception as exc: # pragma: no cover - CI path print(f"ERROR: failed while walking Alembic revision graph: {exc}") return 1 - all_revisions = { - rev.revision - for rev in script.revision_map._revision_map.values() # type: ignore[attr-defined] - if getattr(rev, "revision", None) - } + all_revisions: set[str] = set() + # Alembic's revision_map is dynamically typed; guard None values. + for map_rev in script.revision_map._revision_map.values(): + if map_rev is None: + continue + revision = getattr(map_rev, "revision", None) + if revision: + all_revisions.add(revision) + orphans = sorted(all_revisions - reachable) if orphans: print("ERROR: orphan Alembic revisions detected (not reachable from heads):") - for rev in orphans: - print(f" - {rev}") + for orphan_rev in orphans: + print(f" - {orphan_rev}") return 1 print("OK: migration graph integrity passed") From 92c079410e9493d55542ad428c359c8db81afde4 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 13 Feb 2026 15:57:27 +0530 Subject: [PATCH 7/8] feat: enhance DependencyBanner to support resolved dependencies and update display logic --- frontend/src/app/boards/[boardId]/page.tsx | 69 ++++++++++++++++--- .../components/molecules/DependencyBanner.tsx | 2 + 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/boards/[boardId]/page.tsx b/frontend/src/app/boards/[boardId]/page.tsx index 266ce164..ce07ba22 100644 --- a/frontend/src/app/boards/[boardId]/page.tsx +++ b/frontend/src/app/boards/[boardId]/page.tsx @@ -2239,6 +2239,28 @@ export default function BoardDetailPage() { }); }, [openComments, selectedTask, taskById]); + const selectedTaskResolvedDependencies = useMemo< + DependencyBannerDependency[] + >(() => { + if (!selectedTask) return []; + return tasks + .filter((task) => task.depends_on_task_ids?.includes(selectedTask.id)) + .map((task) => { + const statusLabel = task.status ? task.status.replace(/_/g, " ") : "unknown"; + return { + id: task.id, + title: task.title, + statusLabel, + isBlocking: false, + isDone: task.status === "done", + onClick: () => { + openComments({ id: task.id }); + }, + disabled: false, + }; + }); + }, [openComments, selectedTask, tasks]); + useEffect(() => { if (!taskIdFromUrl) return; if (openedTaskIdFromUrlRef.current === taskIdFromUrl) return; @@ -3410,15 +3432,44 @@ export default function BoardDetailPage() {

Dependencies

- - {selectedTask?.is_blocked - ? "Blocked by incomplete dependencies." - : null} - -
+ {(() => { + const hasDependencies = + (selectedTask?.depends_on_task_ids?.length ?? 0) > 0; + const hasResolvedDependencies = + selectedTaskResolvedDependencies.length > 0; + const isDependencyModeBlocked = hasDependencies + ? selectedTask?.is_blocked === true + : false; + const bannerVariant = + hasDependencies || hasResolvedDependencies + ? isDependencyModeBlocked + ? "blocked" + : "resolved" + : "blocked"; + const displayedDependencies = + hasDependencies && selectedTask + ? selectedTaskDependencies + : selectedTaskResolvedDependencies; + const childrenMessage = + hasDependencies && selectedTask?.is_blocked + ? "Blocked by incomplete dependencies." + : hasDependencies + ? "Dependencies resolved." + : hasResolvedDependencies + ? "This task resolves these tasks." + : null; + + return ( + + {childrenMessage} + + ); + })()} +

diff --git a/frontend/src/components/molecules/DependencyBanner.tsx b/frontend/src/components/molecules/DependencyBanner.tsx index aad178d2..01bbea38 100644 --- a/frontend/src/components/molecules/DependencyBanner.tsx +++ b/frontend/src/components/molecules/DependencyBanner.tsx @@ -20,6 +20,8 @@ interface DependencyBannerProps { emptyMessage?: string; } +type DependencyBannerVariant = "blocked" | "resolved"; + const toneClassByVariant: Record = { blocked: "border-rose-200 bg-rose-50 text-rose-700", resolved: "border-blue-200 bg-blue-50 text-blue-700", From 59bdf03d050faab30302f67aa768d9586387f4d9 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 13 Feb 2026 16:05:04 +0530 Subject: [PATCH 8/8] refactor: improve code formatting and readability in page.tsx --- frontend/src/app/boards/[boardId]/page.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/boards/[boardId]/page.tsx b/frontend/src/app/boards/[boardId]/page.tsx index ce07ba22..f47975c9 100644 --- a/frontend/src/app/boards/[boardId]/page.tsx +++ b/frontend/src/app/boards/[boardId]/page.tsx @@ -2217,7 +2217,9 @@ export default function BoardDetailPage() { const selectedTaskDependencies = useMemo(() => { if (!selectedTask) return []; - const blockedDependencyIds = new Set(selectedTask.blocked_by_task_ids ?? []); + const blockedDependencyIds = new Set( + selectedTask.blocked_by_task_ids ?? [], + ); return (selectedTask.depends_on_task_ids ?? []).map((dependencyId) => { const dependencyTask = taskById.get(dependencyId); const statusLabel = dependencyTask?.status @@ -2246,7 +2248,9 @@ export default function BoardDetailPage() { return tasks .filter((task) => task.depends_on_task_ids?.includes(selectedTask.id)) .map((task) => { - const statusLabel = task.status ? task.status.replace(/_/g, " ") : "unknown"; + const statusLabel = task.status + ? task.status.replace(/_/g, " ") + : "unknown"; return { id: task.id, title: task.title, @@ -3469,7 +3473,7 @@ export default function BoardDetailPage() { ); })()} -

+