+
@foreach (var col in Model.Columns)
{
diff --git a/TaskTracker.Api/Pages/Partials/_UpcomingTasks.cshtml b/TaskTracker.Api/Pages/Partials/_UpcomingTasks.cshtml
new file mode 100644
index 0000000..4c58475
--- /dev/null
+++ b/TaskTracker.Api/Pages/Partials/_UpcomingTasks.cshtml
@@ -0,0 +1,37 @@
+@using TaskTracker.Api.Pages
+@using TaskTracker.Core.Enums
+@model BoardModel
+
+@if (Model.UpcomingTasks.Count > 0)
+{
+
+
+
+ @foreach (var task in Model.UpcomingTasks)
+ {
+ var categoryColor = BoardModel.GetCategoryColor(task.Category);
+ var dueAt = task.DueAt!.Value;
+ var dueAtUtc = dueAt.ToUniversalTime().ToString("O");
+ var localDueAt = dueAt.ToLocalTime().ToString("ddd, MMM d, h:mm tt");
+
+ @task.Title
+
+ @task.Status
+ @if (!string.IsNullOrEmpty(task.Category))
+ {
+
+ @task.Category
+ }
+
+
+
+ }
+
+
+}
diff --git a/TaskTracker.Api/wwwroot/css/site.css b/TaskTracker.Api/wwwroot/css/site.css
index 2fd3bf1..1007d7f 100644
--- a/TaskTracker.Api/wwwroot/css/site.css
+++ b/TaskTracker.Api/wwwroot/css/site.css
@@ -474,6 +474,99 @@ body::before {
}
+/* ============================================================
+ UPCOMING TASKS
+ ============================================================ */
+
+.upcoming-tasks {
+ margin-bottom: 20px;
+ padding: 14px;
+ border: 1px solid var(--color-border);
+ border-radius: 12px;
+ background: var(--color-surface);
+}
+
+.upcoming-tasks-header {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 12px;
+}
+
+.upcoming-tasks-header h2 {
+ margin: 0;
+ font-size: 12px;
+ font-weight: 600;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+}
+
+.upcoming-tasks-header span {
+ color: var(--color-text-secondary);
+ font-size: 11px;
+}
+
+.upcoming-task-list {
+ display: flex;
+ gap: 10px;
+ overflow-x: auto;
+ padding-bottom: 2px;
+}
+
+.upcoming-task {
+ display: grid;
+ flex: 0 0 210px;
+ gap: 6px;
+ padding: 10px;
+ border: 1px solid var(--color-border);
+ border-radius: 8px;
+ background: var(--color-elevated);
+ cursor: pointer;
+ transition: border-color 0.15s, transform 0.15s;
+}
+
+.upcoming-task:hover {
+ border-color: var(--color-border-hover);
+ transform: translateY(-1px);
+}
+
+.upcoming-task-title {
+ overflow: hidden;
+ color: var(--color-text-primary);
+ font-size: 13px;
+ font-weight: 500;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.upcoming-task-meta {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ min-width: 0;
+ color: var(--color-text-secondary);
+ font-size: 11px;
+}
+
+.upcoming-task-status {
+ color: var(--color-status-pending);
+}
+
+.upcoming-task-status--active { color: var(--color-status-active); }
+.upcoming-task-status--paused { color: var(--color-status-paused); }
+.upcoming-task-status--completed { color: var(--color-status-completed); }
+.upcoming-task-status--abandoned { color: var(--color-status-abandoned); }
+
+.task-countdown {
+ color: var(--color-text-secondary);
+ font-size: 11px;
+}
+
+.task-countdown--overdue {
+ color: var(--color-status-abandoned);
+}
+
/* ============================================================
KANBAN LAYOUT
============================================================ */
diff --git a/TaskTracker.Tests/BoardUpcomingTasksTests.cs b/TaskTracker.Tests/BoardUpcomingTasksTests.cs
new file mode 100644
index 0000000..6fbfb7f
--- /dev/null
+++ b/TaskTracker.Tests/BoardUpcomingTasksTests.cs
@@ -0,0 +1,82 @@
+using TaskTracker.Api.Pages;
+using TaskTracker.Core.Entities;
+using TaskTracker.Core.Enums;
+using Xunit;
+
+namespace TaskTracker.Tests;
+
+public class BoardUpcomingTasksTests
+{
+ [Fact]
+ public void SelectUpcomingTasks_excludes_completed_abandoned_and_tasks_without_a_deadline()
+ {
+ var dueAt = DateTimeOffset.Parse("2026-08-14T09:00:00Z");
+ var tasks = new[]
+ {
+ Task("pending", WorkTaskStatus.Pending, dueAt),
+ Task("completed", WorkTaskStatus.Completed, dueAt.AddHours(-1)),
+ Task("abandoned", WorkTaskStatus.Abandoned, dueAt.AddHours(1)),
+ Task("no deadline", WorkTaskStatus.Active),
+ };
+
+ var upcoming = BoardModel.SelectUpcomingTasks(tasks, category: null, hasSubtasks: false);
+
+ var task = Assert.Single(upcoming);
+ Assert.Equal("pending", task.Title);
+ }
+
+ [Fact]
+ public void SelectUpcomingTasks_includes_top_level_tasks_and_subtasks_in_due_date_order()
+ {
+ var parent = Task("top level", WorkTaskStatus.Pending, DateTimeOffset.Parse("2026-08-14T12:00:00Z"));
+ var subtask = Task("subtask", WorkTaskStatus.Active, DateTimeOffset.Parse("2026-08-14T10:00:00Z"), parentTaskId: 1);
+ var later = Task("later", WorkTaskStatus.Paused, DateTimeOffset.Parse("2026-08-14T14:00:00Z"));
+
+ var upcoming = BoardModel.SelectUpcomingTasks([parent, subtask, later], category: null, hasSubtasks: false);
+
+ Assert.Equal(["subtask", "top level", "later"], upcoming.Select(task => task.Title));
+ }
+
+ [Fact]
+ public void SelectUpcomingTasks_limits_results_to_eight_earliest_deadlines()
+ {
+ var start = DateTimeOffset.Parse("2026-08-14T09:00:00Z");
+ var tasks = Enumerable.Range(0, 9)
+ .Select(index => Task($"task {index}", WorkTaskStatus.Pending, start.AddHours(index)))
+ .ToList();
+
+ var upcoming = BoardModel.SelectUpcomingTasks(tasks, category: null, hasSubtasks: false);
+
+ Assert.Equal(8, upcoming.Count);
+ Assert.DoesNotContain(upcoming, task => task.Title == "task 8");
+ }
+
+ [Fact]
+ public void SelectUpcomingTasks_honors_active_category_and_has_subtasks_filters()
+ {
+ var dueAt = DateTimeOffset.Parse("2026-08-14T09:00:00Z");
+ var developmentParent = Task("development parent", WorkTaskStatus.Pending, dueAt, category: "Development");
+ developmentParent.SubTasks.Add(Task("development child", WorkTaskStatus.Pending, dueAt.AddHours(1), parentTaskId: 1, category: "Development"));
+ var developmentLeaf = Task("development leaf", WorkTaskStatus.Pending, dueAt.AddHours(2), category: "Development");
+ var researchParent = Task("research parent", WorkTaskStatus.Pending, dueAt.AddHours(-1), category: "Research");
+ researchParent.SubTasks.Add(Task("research child", WorkTaskStatus.Pending, dueAt, parentTaskId: 2, category: "Research"));
+
+ var upcoming = BoardModel.SelectUpcomingTasks(
+ [developmentParent, developmentParent.SubTasks[0], developmentLeaf, researchParent, researchParent.SubTasks[0]],
+ category: "development",
+ hasSubtasks: true);
+
+ var task = Assert.Single(upcoming);
+ Assert.Equal("development parent", task.Title);
+ }
+
+ private static WorkTask Task(string title, WorkTaskStatus status, DateTimeOffset? dueAt = null, int? parentTaskId = null, string? category = null) =>
+ new()
+ {
+ Title = title,
+ Status = status,
+ DueAt = dueAt,
+ ParentTaskId = parentTaskId,
+ Category = category,
+ };
+}