Index: backend/Dockerfile
===================================================================
--- backend/Dockerfile	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
+++ backend/Dockerfile	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
@@ -0,0 +1,11 @@
+FROM maven:3.9-eclipse-temurin-21 AS build
+WORKDIR /app
+COPY pom.xml .
+RUN mvn dependency:go-offline -q
+COPY src ./src
+RUN mvn package -DskipTests -q
+
+FROM eclipse-temurin:21-jre
+WORKDIR /app
+COPY --from=build /app/target/backend-0.0.1-SNAPSHOT.jar app.jar
+ENTRYPOINT ["java", "-jar", "app.jar"]
Index: backend/src/main/java/com/trekr/backend/config/SecurityConfig.java
===================================================================
--- backend/src/main/java/com/trekr/backend/config/SecurityConfig.java	(revision 194c01621e96510fb6f238b2098a64b3663ec780)
+++ backend/src/main/java/com/trekr/backend/config/SecurityConfig.java	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
@@ -49,5 +49,7 @@
         config.setAllowedOrigins(List.of(
                 "http://localhost:5173",
-                "http://127.0.0.1:5173"));
+                "http://127.0.0.1:5173",
+                "http://localhost:3000",
+                "http://127.0.0.1:3000"));
         config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
         config.setAllowedHeaders(List.of("Authorization", "Content-Type", "Accept", "Origin", "X-Requested-With"));
Index: backend/src/main/java/com/trekr/backend/controller/CustomTrackingController.java
===================================================================
--- backend/src/main/java/com/trekr/backend/controller/CustomTrackingController.java	(revision 194c01621e96510fb6f238b2098a64b3663ec780)
+++ backend/src/main/java/com/trekr/backend/controller/CustomTrackingController.java	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
@@ -9,4 +9,5 @@
 import com.trekr.backend.dto.discipline.UpdateTaskFinishedRequest;
 import com.trekr.backend.dto.discipline.UpdateTaskRequest;
+import com.trekr.backend.dto.discipline.UpdateTaskStatusRequest;
 import com.trekr.backend.security.UserPrincipal;
 import com.trekr.backend.service.CustomTrackingService;
@@ -79,4 +80,14 @@
     }
 
+    @PatchMapping("/{customTrackingId}/tasks/{taskId}/status")
+    public TaskDto updateTaskStatus(
+            @PathVariable Long customTrackingId,
+            @PathVariable Long taskId,
+            @RequestBody UpdateTaskStatusRequest request,
+            Authentication authentication) {
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        return customTrackingService.updateCustomCategoryTaskStatus(principal.getUserId(), customTrackingId, taskId, request);
+    }
+
     @DeleteMapping("/{customTrackingId}/tasks/{taskId}")
     @ResponseStatus(HttpStatus.NO_CONTENT)
Index: backend/src/main/java/com/trekr/backend/dto/discipline/CreateTaskRequest.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/discipline/CreateTaskRequest.java	(revision 194c01621e96510fb6f238b2098a64b3663ec780)
+++ backend/src/main/java/com/trekr/backend/dto/discipline/CreateTaskRequest.java	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
@@ -2,4 +2,6 @@
 
 import jakarta.validation.constraints.NotBlank;
+
+import java.time.LocalDate;
 
 public class CreateTaskRequest {
@@ -8,14 +10,21 @@
     private String name;
 
+    private String description;
+    private LocalDate dueDate;
+    private String priority;
+
     public CreateTaskRequest() {
     }
 
-    public String getName() {
-        return name;
-    }
+    public String getName() { return name; }
+    public void setName(String name) { this.name = name; }
 
-    public void setName(String name) {
-        this.name = name;
-    }
+    public String getDescription() { return description; }
+    public void setDescription(String description) { this.description = description; }
+
+    public LocalDate getDueDate() { return dueDate; }
+    public void setDueDate(LocalDate dueDate) { this.dueDate = dueDate; }
+
+    public String getPriority() { return priority; }
+    public void setPriority(String priority) { this.priority = priority; }
 }
-
Index: backend/src/main/java/com/trekr/backend/dto/discipline/TaskDto.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/discipline/TaskDto.java	(revision 194c01621e96510fb6f238b2098a64b3663ec780)
+++ backend/src/main/java/com/trekr/backend/dto/discipline/TaskDto.java	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
@@ -1,5 +1,16 @@
 package com.trekr.backend.dto.discipline;
 
-public record TaskDto(Long taskId, String name, boolean isFinished) {
+import com.trekr.backend.entity.discipline.TaskStatus;
+
+import java.time.LocalDate;
+
+public record TaskDto(
+        Long taskId,
+        String name,
+        boolean isFinished,
+        TaskStatus status,
+        String description,
+        LocalDate dueDate,
+        String priority
+) {
 }
-
Index: backend/src/main/java/com/trekr/backend/dto/discipline/UpdateTaskRequest.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/discipline/UpdateTaskRequest.java	(revision 194c01621e96510fb6f238b2098a64b3663ec780)
+++ backend/src/main/java/com/trekr/backend/dto/discipline/UpdateTaskRequest.java	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
@@ -1,5 +1,8 @@
 package com.trekr.backend.dto.discipline;
 
+import com.trekr.backend.entity.discipline.TaskStatus;
 import jakarta.validation.constraints.NotBlank;
+
+import java.time.LocalDate;
 
 public class UpdateTaskRequest {
@@ -8,14 +11,25 @@
     private String name;
 
+    private String description;
+    private LocalDate dueDate;
+    private String priority;
+    private TaskStatus status;
+
     public UpdateTaskRequest() {
     }
 
-    public String getName() {
-        return name;
-    }
+    public String getName() { return name; }
+    public void setName(String name) { this.name = name; }
 
-    public void setName(String name) {
-        this.name = name;
-    }
+    public String getDescription() { return description; }
+    public void setDescription(String description) { this.description = description; }
+
+    public LocalDate getDueDate() { return dueDate; }
+    public void setDueDate(LocalDate dueDate) { this.dueDate = dueDate; }
+
+    public String getPriority() { return priority; }
+    public void setPriority(String priority) { this.priority = priority; }
+
+    public TaskStatus getStatus() { return status; }
+    public void setStatus(TaskStatus status) { this.status = status; }
 }
-
Index: backend/src/main/java/com/trekr/backend/entity/discipline/Task.java
===================================================================
--- backend/src/main/java/com/trekr/backend/entity/discipline/Task.java	(revision 194c01621e96510fb6f238b2098a64b3663ec780)
+++ backend/src/main/java/com/trekr/backend/entity/discipline/Task.java	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
@@ -3,4 +3,6 @@
 import jakarta.persistence.Column;
 import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
 import jakarta.persistence.FetchType;
 import jakarta.persistence.GeneratedValue;
@@ -10,4 +12,6 @@
 import jakarta.persistence.ManyToOne;
 import jakarta.persistence.Table;
+
+import java.time.LocalDate;
 
 @Entity
@@ -23,6 +27,16 @@
     private String name;
 
-    @Column(name = "is_finished", nullable = false)
-    private boolean finished;
+    @Enumerated(EnumType.STRING)
+    @Column(name = "status", nullable = false)
+    private TaskStatus status = TaskStatus.NOT_STARTED;
+
+    @Column(name = "description")
+    private String description;
+
+    @Column(name = "due_date")
+    private LocalDate dueDate;
+
+    @Column(name = "priority")
+    private String priority;
 
     @ManyToOne(fetch = FetchType.LAZY)
@@ -37,40 +51,31 @@
     }
 
-    public Long getTaskId() {
-        return taskId;
+    public Long getTaskId() { return taskId; }
+    public void setTaskId(Long taskId) { this.taskId = taskId; }
+
+    public String getName() { return name; }
+    public void setName(String name) { this.name = name; }
+
+    public TaskStatus getStatus() { return status; }
+    public void setStatus(TaskStatus status) { this.status = status; }
+
+    public boolean isFinished() { return TaskStatus.FINISHED == status; }
+    public void setFinished(boolean finished) {
+        this.status = finished ? TaskStatus.FINISHED : TaskStatus.NOT_STARTED;
     }
 
-    public void setTaskId(Long taskId) {
-        this.taskId = taskId;
-    }
+    public String getDescription() { return description; }
+    public void setDescription(String description) { this.description = description; }
 
-    public String getName() {
-        return name;
-    }
+    public LocalDate getDueDate() { return dueDate; }
+    public void setDueDate(LocalDate dueDate) { this.dueDate = dueDate; }
 
-    public void setName(String name) {
-        this.name = name;
-    }
+    public String getPriority() { return priority; }
+    public void setPriority(String priority) { this.priority = priority; }
 
-    public boolean isFinished() {
-        return finished;
-    }
+    public DisciplineUser getDisciplineUser() { return disciplineUser; }
+    public void setDisciplineUser(DisciplineUser disciplineUser) { this.disciplineUser = disciplineUser; }
 
-    public void setFinished(boolean finished) {
-        this.finished = finished;
-    }
-
-    public DisciplineUser getDisciplineUser() {
-        return disciplineUser;
-    }
-
-    public void setDisciplineUser(DisciplineUser disciplineUser) {
-        this.disciplineUser = disciplineUser;
-    }
-
-    public CustomTrackingCategory getCustomTrackingCategory() {
-        return customTrackingCategory;
-    }
-
+    public CustomTrackingCategory getCustomTrackingCategory() { return customTrackingCategory; }
     public void setCustomTrackingCategory(CustomTrackingCategory customTrackingCategory) {
         this.customTrackingCategory = customTrackingCategory;
Index: backend/src/main/java/com/trekr/backend/repository/TaskRepository.java
===================================================================
--- backend/src/main/java/com/trekr/backend/repository/TaskRepository.java	(revision 194c01621e96510fb6f238b2098a64b3663ec780)
+++ backend/src/main/java/com/trekr/backend/repository/TaskRepository.java	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
@@ -2,4 +2,5 @@
 
 import com.trekr.backend.entity.discipline.Task;
+import com.trekr.backend.entity.discipline.TaskStatus;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
@@ -19,7 +20,7 @@
     long countByDisciplineUser_UserId(Long userId);
 
-    long countByDisciplineUser_UserIdAndFinishedTrue(Long userId);
+    long countByDisciplineUser_UserIdAndStatus(Long userId, TaskStatus status);
 
-    List<Task> findByDisciplineUser_UserIdAndFinishedTrue(Long userId);
+    List<Task> findByDisciplineUser_UserIdAndStatus(Long userId, TaskStatus status);
 
     List<Task> findByCustomTrackingCategory_CustomTrackingIdAndCustomTrackingCategory_User_UserIdOrderByTaskIdDesc(
@@ -30,6 +31,5 @@
 
     @Modifying
-    @Query("update Task t set t.finished = false where t.disciplineUser.userId = :userId")
-    int resetFinishedForUser(@Param("userId") Long userId);
+    @Query("update Task t set t.status = :status where t.disciplineUser.userId = :userId")
+    int resetStatusForUser(@Param("userId") Long userId, @Param("status") TaskStatus status);
 }
-
Index: backend/src/main/java/com/trekr/backend/service/CustomTrackingService.java
===================================================================
--- backend/src/main/java/com/trekr/backend/service/CustomTrackingService.java	(revision 194c01621e96510fb6f238b2098a64b3663ec780)
+++ backend/src/main/java/com/trekr/backend/service/CustomTrackingService.java	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
@@ -8,4 +8,6 @@
 import com.trekr.backend.dto.discipline.TasksResponse;
 import com.trekr.backend.dto.discipline.UpdateTaskFinishedRequest;
+import com.trekr.backend.dto.discipline.UpdateTaskStatusRequest;
+import com.trekr.backend.entity.discipline.TaskStatus;
 import com.trekr.backend.dto.discipline.UpdateTaskRequest;
 import com.trekr.backend.entity.User;
@@ -81,5 +83,5 @@
                         customTrackingId, userId)
                 .stream()
-                .map(t -> new TaskDto(t.getTaskId(), t.getName(), t.isFinished()))
+                .map(t -> new TaskDto(t.getTaskId(), t.getName(), t.isFinished(), t.getStatus(), t.getDescription(), t.getDueDate(), t.getPriority()))
                 .toList();
 
@@ -101,10 +103,13 @@
         Task task = new Task();
         task.setName(name.trim());
-        task.setFinished(false);
+        task.setStatus(TaskStatus.NOT_STARTED);
+        task.setDescription(request.getDescription());
+        task.setDueDate(request.getDueDate());
+        task.setPriority(request.getPriority());
         task.setDisciplineUser(null);
         task.setCustomTrackingCategory(category);
 
         Task saved = taskRepository.save(task);
-        return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished());
+        return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished(), saved.getStatus(), saved.getDescription(), saved.getDueDate(), saved.getPriority());
     }
 
@@ -127,6 +132,12 @@
 
         task.setName(name.trim());
-        Task saved = taskRepository.save(task);
-        return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished());
+        task.setDescription(request.getDescription());
+        task.setDueDate(request.getDueDate());
+        task.setPriority(request.getPriority());
+        if (request.getStatus() != null) {
+            task.setStatus(request.getStatus());
+        }
+        Task saved = taskRepository.save(task);
+        return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished(), saved.getStatus(), saved.getDescription(), saved.getDueDate(), saved.getPriority());
     }
 
@@ -151,5 +162,27 @@
         task.setFinished(finished);
         Task saved = taskRepository.save(task);
-        return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished());
+        return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished(), saved.getStatus(), saved.getDescription(), saved.getDueDate(), saved.getPriority());
+    }
+
+    @Transactional
+    public TaskDto updateCustomCategoryTaskStatus(Long userId, Long customTrackingId, Long taskId,
+                                                  UpdateTaskStatusRequest request) {
+        Task task = taskRepository.findById(taskId)
+                .orElseThrow(() -> new RuntimeException("Task not found"));
+
+        if (task.getCustomTrackingCategory() == null
+                || !customTrackingId.equals(task.getCustomTrackingCategory().getCustomTrackingId())
+                || task.getCustomTrackingCategory().getUser() == null
+                || !userId.equals(task.getCustomTrackingCategory().getUser().getUserId())) {
+            throw new RuntimeException("Task not found");
+        }
+
+        if (request.getStatus() == null) {
+            throw new RuntimeException("status is required");
+        }
+
+        task.setStatus(request.getStatus());
+        Task saved = taskRepository.save(task);
+        return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished(), saved.getStatus(), saved.getDescription(), saved.getDueDate(), saved.getPriority());
     }
 
Index: backend/src/main/java/com/trekr/backend/service/DisciplineService.java
===================================================================
--- backend/src/main/java/com/trekr/backend/service/DisciplineService.java	(revision 194c01621e96510fb6f238b2098a64b3663ec780)
+++ backend/src/main/java/com/trekr/backend/service/DisciplineService.java	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
@@ -14,4 +14,5 @@
 import com.trekr.backend.entity.discipline.Task;
 import com.trekr.backend.entity.discipline.TaskDailyCompletion;
+import com.trekr.backend.entity.discipline.TaskStatus;
 import com.trekr.backend.entity.discipline.TaskDailyCompletionId;
 import com.trekr.backend.repository.DailyCompletionRepository;
@@ -81,5 +82,5 @@
 
         List<TaskDto> tasks = result.getContent().stream()
-                .map(t -> new TaskDto(t.getTaskId(), t.getName(), t.isFinished()))
+                .map(t -> new TaskDto(t.getTaskId(), t.getName(), t.isFinished(), t.getStatus(), t.getDescription(), t.getDueDate(), t.getPriority()))
                 .toList();
 
@@ -107,5 +108,5 @@
 
         Task saved = taskRepository.save(task);
-        return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished());
+        return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished(), saved.getStatus(), saved.getDescription(), saved.getDueDate(), saved.getPriority());
     }
 
@@ -126,5 +127,5 @@
         task.setName(name.trim());
         Task saved = taskRepository.save(task);
-        return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished());
+        return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished(), saved.getStatus(), saved.getDescription(), saved.getDueDate(), saved.getPriority());
     }
 
@@ -145,5 +146,5 @@
         task.setFinished(finished);
         Task saved = taskRepository.save(task);
-        return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished());
+        return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished(), saved.getStatus(), saved.getDescription(), saved.getDueDate(), saved.getPriority());
     }
 
@@ -187,5 +188,5 @@
 
         long total = taskRepository.countByDisciplineUser_UserId(userId);
-        long finished = taskRepository.countByDisciplineUser_UserIdAndFinishedTrue(userId);
+        long finished = taskRepository.countByDisciplineUser_UserIdAndStatus(userId, TaskStatus.FINISHED);
 
         BigDecimal percent;
@@ -210,5 +211,5 @@
         // (not strictly required for now, but matches schema and enables future analytics)
         if (finished > 0) {
-            List<TaskDailyCompletion> links = taskRepository.findByDisciplineUser_UserIdAndFinishedTrue(userId)
+            List<TaskDailyCompletion> links = taskRepository.findByDisciplineUser_UserIdAndStatus(userId, TaskStatus.FINISHED)
                     .stream()
                     .map(t -> {
@@ -224,5 +225,5 @@
 
         // Reset all tasks for next day
-        taskRepository.resetFinishedForUser(userId);
+        taskRepository.resetStatusForUser(userId, TaskStatus.NOT_STARTED);
 
         DailyCompletionDto dto = new DailyCompletionDto(savedCompletion.getDailyCompletionId(),
Index: db-scripts/ddl.sql
===================================================================
--- db-scripts/ddl.sql	(revision 194c01621e96510fb6f238b2098a64b3663ec780)
+++ db-scripts/ddl.sql	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
@@ -96,5 +96,8 @@
     task_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
     name TEXT NOT NULL,
-    is_finished BOOLEAN NOT NULL DEFAULT FALSE,
+    status VARCHAR(20) NOT NULL DEFAULT 'NOT_STARTED',
+    description TEXT,
+    due_date DATE,
+    priority VARCHAR(10),
 
     discipline_user_id BIGINT
Index: db-scripts/dml.sql
===================================================================
--- db-scripts/dml.sql	(revision 194c01621e96510fb6f238b2098a64b3663ec780)
+++ db-scripts/dml.sql	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
@@ -262,65 +262,65 @@
 );
 
-INSERT INTO TASKS (task_id, name, is_finished, discipline_user_id, custom_tracking_id) VALUES
-(1, 'Morning Run', FALSE, 1, NULL),
-(2, 'Read Book', FALSE, 1, NULL),
-(3, 'Meditation', FALSE, 2, NULL),
-(4, 'Code Review', FALSE, NULL, 1),
-(5, 'Push Updates', FALSE, NULL, 1),
-(6, 'Plan Day', FALSE, 3, NULL),
-(7, 'No Sugar', FALSE, 3, NULL),
-(8, 'Stretch', FALSE, 3, NULL),
-(9, 'Drink Water', FALSE, 4, NULL),
-(10, 'Walk 8k Steps', FALSE, 4, NULL),
-(11, 'Journal', FALSE, 5, NULL),
-(12, 'Gym', FALSE, 5, NULL),
-(13, 'Duolingo', FALSE, NULL, 2),
-(14, 'Watch Lesson', FALSE, NULL, 2),
-(15, 'Practice Problems', FALSE, NULL, 5),
-(16, 'Stretch 10m', FALSE, 7, NULL),
-(17, 'No Soda', FALSE, 7, NULL),
-(18, 'Walk 30m', FALSE, 8, NULL),
-(19, 'Sleep 7h', FALSE, 8, NULL),
-(20, 'Read 20p', FALSE, 9, NULL),
-(21, 'Meditate', FALSE, 9, NULL),
-(22, 'No Sugar', FALSE, 9, NULL),
-(23, 'Drink Water', FALSE, 10, NULL),
-(24, 'Morning Run', FALSE, 11, NULL),
-(25, 'Plan Day', FALSE, 11, NULL),
-(26, 'Study 1h', FALSE, 11, NULL),
-(27, 'Yoga', FALSE, 12, NULL),
-(28, 'Cook Healthy', FALSE, 12, NULL),
-(29, 'Gym', FALSE, 13, NULL),
-(30, 'Protein Target', FALSE, 13, NULL),
-(31, 'Journal', FALSE, 14, NULL),
-(32, 'Early Bed', FALSE, 14, NULL),
-(33, 'Duolingo', FALSE, 15, NULL),
-(34, 'No Alcohol', FALSE, 15, NULL),
-(35, 'Walk 8k Steps', FALSE, 15, NULL),
-(36, 'Attend Lecture', FALSE, NULL, 6),
-(37, 'Solve Tasks', FALSE, NULL, 6),
-(38, 'Review', FALSE, NULL, 6),
-(39, 'Steps', FALSE, NULL, 7),
-(40, 'Water', FALSE, NULL, 7),
-(41, 'Pitch', FALSE, NULL, 8),
-(42, 'Build', FALSE, NULL, 8),
-(43, 'Ship', FALSE, NULL, 8),
-(44, 'Meal Prep', FALSE, NULL, 9),
-(45, 'Groceries', FALSE, NULL, 9),
-(46, 'Read Fiction', FALSE, NULL, 10),
-(47, 'Read Nonfiction', FALSE, NULL, 10),
-(48, 'Standup', FALSE, NULL, 11),
-(49, 'Tickets', FALSE, NULL, 11),
-(50, 'PR Review', FALSE, NULL, 11),
-(51, 'Track Expenses', FALSE, NULL, 12),
-(52, 'Invest', FALSE, NULL, 12),
-(53, 'Duolingo Session', FALSE, NULL, 13),
-(54, 'Podcast', FALSE, NULL, 13),
-(55, 'Squat', FALSE, NULL, 14),
-(56, 'Bench', FALSE, NULL, 14),
-(57, 'Deadlift', FALSE, NULL, 14),
-(58, 'Practice', FALSE, NULL, 15),
-(59, 'Mock', FALSE, NULL, 15),
-(60, 'Review Exam', FALSE, NULL, 15);
+INSERT INTO TASKS (task_id, name, status, discipline_user_id, custom_tracking_id) VALUES
+(1, 'Morning Run', 'NOT_STARTED', 1, NULL),
+(2, 'Read Book', 'NOT_STARTED', 1, NULL),
+(3, 'Meditation', 'NOT_STARTED', 2, NULL),
+(4, 'Code Review', 'NOT_STARTED', NULL, 1),
+(5, 'Push Updates', 'NOT_STARTED', NULL, 1),
+(6, 'Plan Day', 'NOT_STARTED', 3, NULL),
+(7, 'No Sugar', 'NOT_STARTED', 3, NULL),
+(8, 'Stretch', 'NOT_STARTED', 3, NULL),
+(9, 'Drink Water', 'NOT_STARTED', 4, NULL),
+(10, 'Walk 8k Steps', 'NOT_STARTED', 4, NULL),
+(11, 'Journal', 'NOT_STARTED', 5, NULL),
+(12, 'Gym', 'NOT_STARTED', 5, NULL),
+(13, 'Duolingo', 'NOT_STARTED', NULL, 2),
+(14, 'Watch Lesson', 'NOT_STARTED', NULL, 2),
+(15, 'Practice Problems', 'NOT_STARTED', NULL, 5),
+(16, 'Stretch 10m', 'NOT_STARTED', 7, NULL),
+(17, 'No Soda', 'NOT_STARTED', 7, NULL),
+(18, 'Walk 30m', 'NOT_STARTED', 8, NULL),
+(19, 'Sleep 7h', 'NOT_STARTED', 8, NULL),
+(20, 'Read 20p', 'NOT_STARTED', 9, NULL),
+(21, 'Meditate', 'NOT_STARTED', 9, NULL),
+(22, 'No Sugar', 'NOT_STARTED', 9, NULL),
+(23, 'Drink Water', 'NOT_STARTED', 10, NULL),
+(24, 'Morning Run', 'NOT_STARTED', 11, NULL),
+(25, 'Plan Day', 'NOT_STARTED', 11, NULL),
+(26, 'Study 1h', 'NOT_STARTED', 11, NULL),
+(27, 'Yoga', 'NOT_STARTED', 12, NULL),
+(28, 'Cook Healthy', 'NOT_STARTED', 12, NULL),
+(29, 'Gym', 'NOT_STARTED', 13, NULL),
+(30, 'Protein Target', 'NOT_STARTED', 13, NULL),
+(31, 'Journal', 'NOT_STARTED', 14, NULL),
+(32, 'Early Bed', 'NOT_STARTED', 14, NULL),
+(33, 'Duolingo', 'NOT_STARTED', 15, NULL),
+(34, 'No Alcohol', 'NOT_STARTED', 15, NULL),
+(35, 'Walk 8k Steps', 'NOT_STARTED', 15, NULL),
+(36, 'Attend Lecture', 'NOT_STARTED', NULL, 6),
+(37, 'Solve Tasks', 'NOT_STARTED', NULL, 6),
+(38, 'Review', 'NOT_STARTED', NULL, 6),
+(39, 'Steps', 'NOT_STARTED', NULL, 7),
+(40, 'Water', 'NOT_STARTED', NULL, 7),
+(41, 'Pitch', 'NOT_STARTED', NULL, 8),
+(42, 'Build', 'NOT_STARTED', NULL, 8),
+(43, 'Ship', 'NOT_STARTED', NULL, 8),
+(44, 'Meal Prep', 'NOT_STARTED', NULL, 9),
+(45, 'Groceries', 'NOT_STARTED', NULL, 9),
+(46, 'Read Fiction', 'NOT_STARTED', NULL, 10),
+(47, 'Read Nonfiction', 'NOT_STARTED', NULL, 10),
+(48, 'Standup', 'NOT_STARTED', NULL, 11),
+(49, 'Tickets', 'NOT_STARTED', NULL, 11),
+(50, 'PR Review', 'NOT_STARTED', NULL, 11),
+(51, 'Track Expenses', 'NOT_STARTED', NULL, 12),
+(52, 'Invest', 'NOT_STARTED', NULL, 12),
+(53, 'Duolingo Session', 'NOT_STARTED', NULL, 13),
+(54, 'Podcast', 'NOT_STARTED', NULL, 13),
+(55, 'Squat', 'NOT_STARTED', NULL, 14),
+(56, 'Bench', 'NOT_STARTED', NULL, 14),
+(57, 'Deadlift', 'NOT_STARTED', NULL, 14),
+(58, 'Practice', 'NOT_STARTED', NULL, 15),
+(59, 'Mock', 'NOT_STARTED', NULL, 15),
+(60, 'Review Exam', 'NOT_STARTED', NULL, 15);
 
 SELECT setval(
Index: db-scripts/docker-init/01-init.sql
===================================================================
--- db-scripts/docker-init/01-init.sql	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
+++ db-scripts/docker-init/01-init.sql	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
@@ -0,0 +1,5 @@
+-- Combined init: schema (DDL) then seed data (DML)
+-- Postgres runs all *.sql files in /docker-entrypoint-initdb.d on first start.
+
+\i /docker-entrypoint-initdb.d/ddl.sql
+\i /docker-entrypoint-initdb.d/dml.sql
Index: db-scripts/migrations/002_task_enhancements.sql
===================================================================
--- db-scripts/migrations/002_task_enhancements.sql	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
+++ db-scripts/migrations/002_task_enhancements.sql	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
@@ -0,0 +1,17 @@
+-- Migration 002: Add description, due_date, priority, status to tasks
+-- Replace is_finished boolean with status enum string
+
+SET search_path TO trekr;
+
+ALTER TABLE tasks ADD COLUMN IF NOT EXISTS description TEXT;
+ALTER TABLE tasks ADD COLUMN IF NOT EXISTS due_date DATE;
+ALTER TABLE tasks ADD COLUMN IF NOT EXISTS priority VARCHAR(10);
+ALTER TABLE tasks ADD COLUMN IF NOT EXISTS status VARCHAR(20);
+
+-- Migrate existing data from is_finished to status
+UPDATE tasks SET status = CASE WHEN is_finished THEN 'FINISHED' ELSE 'NOT_STARTED' END WHERE status IS NULL;
+
+ALTER TABLE tasks ALTER COLUMN status SET NOT NULL;
+ALTER TABLE tasks ALTER COLUMN status SET DEFAULT 'NOT_STARTED';
+
+ALTER TABLE tasks DROP COLUMN IF EXISTS is_finished;
Index: docker-compose.yml
===================================================================
--- docker-compose.yml	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
+++ docker-compose.yml	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
@@ -0,0 +1,43 @@
+services:
+  db:
+    image: postgres:16-alpine
+    environment:
+      POSTGRES_DB: trekr
+      POSTGRES_USER: trekr
+      POSTGRES_PASSWORD: trekr
+    volumes:
+      - db_data:/var/lib/postgresql/data
+      - ./db-scripts/ddl.sql:/docker-entrypoint-initdb.d/ddl.sql:ro
+      - ./db-scripts/dml.sql:/docker-entrypoint-initdb.d/dml.sql:ro
+      - ./db-scripts/docker-init/01-init.sql:/docker-entrypoint-initdb.d/01-init.sql:ro
+    healthcheck:
+      test: ["CMD-SHELL", "pg_isready -U trekr -d trekr"]
+      interval: 5s
+      timeout: 5s
+      retries: 10
+    ports:
+      - "5432:5432"
+
+  backend:
+    build: ./backend
+    environment:
+      SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/trekr
+      SPRING_DATASOURCE_USERNAME: trekr
+      SPRING_DATASOURCE_PASSWORD: trekr
+      SPRING_JPA_HIBERNATE_DDL_AUTO: none
+      JWT_SECRET: ${JWT_SECRET:-change-me-in-production-min-32-chars}
+    depends_on:
+      db:
+        condition: service_healthy
+    ports:
+      - "8080:8080"
+
+  frontend:
+    build: ./frontend
+    ports:
+      - "3000:80"
+    depends_on:
+      - backend
+
+volumes:
+  db_data:
Index: frontend/Dockerfile
===================================================================
--- frontend/Dockerfile	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
+++ frontend/Dockerfile	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
@@ -0,0 +1,11 @@
+FROM node:22-alpine AS build
+WORKDIR /app
+COPY package*.json ./
+RUN npm ci --legacy-peer-deps
+COPY . .
+RUN VITE_API_BASE_URL=/api npm run build
+
+FROM nginx:alpine
+COPY --from=build /app/dist /usr/share/nginx/html
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+EXPOSE 80
Index: frontend/nginx.conf
===================================================================
--- frontend/nginx.conf	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
+++ frontend/nginx.conf	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
@@ -0,0 +1,31 @@
+server {
+    listen 80;
+
+    root /usr/share/nginx/html;
+    index index.html;
+
+    # Proxy API calls to the backend container
+    location /api/ {
+        proxy_pass http://backend:8080/api/;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+    }
+
+    # Yahoo Finance proxy (replaces the Vite dev proxy)
+    location /yahoo/ {
+        proxy_pass https://query1.finance.yahoo.com/;
+        proxy_ssl_server_name on;
+        proxy_set_header Host query1.finance.yahoo.com;
+    }
+
+    location /yahoo2/ {
+        proxy_pass https://query2.finance.yahoo.com/;
+        proxy_ssl_server_name on;
+        proxy_set_header Host query2.finance.yahoo.com;
+    }
+
+    # SPA fallback
+    location / {
+        try_files $uri $uri/ /index.html;
+    }
+}
Index: frontend/src/api/discipline.js
===================================================================
--- frontend/src/api/discipline.js	(revision 194c01621e96510fb6f238b2098a64b3663ec780)
+++ frontend/src/api/discipline.js	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
@@ -81,4 +81,12 @@
 }
 
+export async function updateCustomCategoryTaskStatus(customTrackingId, taskId, status) {
+  const res = await api.patch(
+    `/discipline/custom-tracking-categories/${customTrackingId}/tasks/${taskId}/status`,
+    { status }
+  );
+  return res?.data;
+}
+
 export async function deleteCustomCategoryTask(customTrackingId, taskId) {
   return api.delete(
Index: frontend/src/pages/Dashboard/pages/CustomCategory/CustomCategoryKanban.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/CustomCategory/CustomCategoryKanban.jsx	(revision 194c01621e96510fb6f238b2098a64b3663ec780)
+++ frontend/src/pages/Dashboard/pages/CustomCategory/CustomCategoryKanban.jsx	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
@@ -8,5 +8,5 @@
   getCustomTrackingCategories,
   updateCustomCategoryTask,
-  updateCustomCategoryTaskFinished,
+  updateCustomCategoryTaskStatus,
 } from "../../../../api/discipline.js";
 
@@ -23,7 +23,10 @@
 
   const lanes = useMemo(() => {
-    const notDone = (tasks ?? []).filter((t) => !t.isFinished);
-    const done = (tasks ?? []).filter((t) => t.isFinished);
-    return { notDone, done };
+    const all = tasks ?? [];
+    return {
+      notStarted: all.filter((t) => t.status === "NOT_STARTED"),
+      inProgress: all.filter((t) => t.status === "IN_PROGRESS"),
+      finished: all.filter((t) => t.status === "FINISHED"),
+    };
   }, [tasks]);
 
@@ -36,9 +39,7 @@
         getCustomCategoryTasks(customTrackingId),
       ]);
-
       const cats = catsRes?.items ?? [];
       const found = cats.find((c) => Number(c.customTrackingId) === customTrackingId);
       setCategoryName(found?.name ?? "Custom Category");
-
       setTasks(tasksRes?.tasks ?? []);
     } catch (e) {
@@ -59,11 +60,11 @@
   }, [customTrackingId]);
 
-  async function handleAddTask(name) {
-    const created = await createCustomCategoryTask(customTrackingId, { name });
+  async function handleAddTask(payload) {
+    const created = await createCustomCategoryTask(customTrackingId, payload);
     setTasks((prev) => [created, ...(prev ?? [])]);
   }
 
-  async function handleEditTask(taskId, name) {
-    const updated = await updateCustomCategoryTask(customTrackingId, taskId, { name });
+  async function handleEditTask(taskId, payload) {
+    const updated = await updateCustomCategoryTask(customTrackingId, taskId, payload);
     setTasks((prev) => (prev ?? []).map((t) => (t.taskId === taskId ? updated : t)));
   }
@@ -74,23 +75,13 @@
   }
 
-  async function handleMoveTask(taskId, toFinished) {
-    // optimistic update
+  async function handleMoveTask(taskId, toStatus) {
     setTasks((prev) =>
-      (prev ?? []).map((t) =>
-        t.taskId === taskId ? { ...t, isFinished: toFinished } : t
-      )
+      (prev ?? []).map((t) => (t.taskId === taskId ? { ...t, status: toStatus } : t))
     );
-
     try {
-      const saved = await updateCustomCategoryTaskFinished(
-        customTrackingId,
-        taskId,
-        toFinished
-      );
+      const saved = await updateCustomCategoryTaskStatus(customTrackingId, taskId, toStatus);
       setTasks((prev) => (prev ?? []).map((t) => (t.taskId === taskId ? saved : t)));
-    } catch (e) {
-      // revert by refetching (simple + correct)
+    } catch {
       await refresh();
-      throw e;
     }
   }
@@ -102,5 +93,5 @@
           <h1 className="text-2xl font-bold">{categoryName}</h1>
           <p className="text-sm opacity-80">
-            Drag tickets between columns to mark them done / not done.
+            Drag tickets between columns or use the edit form to update status.
           </p>
         </div>
@@ -115,6 +106,7 @@
       <CustomCategoryKanbanBoard
         loading={loading}
-        notDone={lanes.notDone}
-        done={lanes.done}
+        notStarted={lanes.notStarted}
+        inProgress={lanes.inProgress}
+        finished={lanes.finished}
         onAddTask={handleAddTask}
         onEditTask={handleEditTask}
@@ -125,4 +117,2 @@
   );
 }
-
-
Index: frontend/src/pages/Dashboard/pages/CustomCategory/components/CustomCategoryKanbanBoard.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/CustomCategory/components/CustomCategoryKanbanBoard.jsx	(revision 194c01621e96510fb6f238b2098a64b3663ec780)
+++ frontend/src/pages/Dashboard/pages/CustomCategory/components/CustomCategoryKanbanBoard.jsx	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
@@ -1,238 +1,194 @@
 import React, { useMemo, useState } from "react";
-
-import {
-  DndContext,
-  PointerSensor,
-  closestCenter,
-  useSensor,
-  useSensors,
-} from "@dnd-kit/core";
-import {
-  SortableContext,
-  arrayMove,
-  rectSortingStrategy,
-} from "@dnd-kit/sortable";
+import { DndContext, PointerSensor, closestCenter, useSensor, useSensors } from "@dnd-kit/core";
+import { SortableContext, rectSortingStrategy } from "@dnd-kit/sortable";
+import { MdAdd, MdSort } from "react-icons/md";
 
 import KanbanColumn from "./KanbanColumn";
 import TaskTicketCard from "./TaskTicketCard";
+import TaskFormModal from "./TaskFormModal";
 
-function laneIdForTask(task) {
-  return task?.isFinished ? "done" : "notDone";
+const LANES = [
+  { id: "NOT_STARTED", droppableId: "lane:NOT_STARTED", title: "Not Started" },
+  { id: "IN_PROGRESS", droppableId: "lane:IN_PROGRESS", title: "In Progress" },
+  { id: "FINISHED",    droppableId: "lane:FINISHED",    title: "Finished" },
+];
+
+const PRIORITY_ORDER = { HIGH: 0, MEDIUM: 1, LOW: 2, null: 3, undefined: 3 };
+
+function sortTasks(tasks, sortBy) {
+  if (!tasks || tasks.length === 0) return tasks;
+  const arr = [...tasks];
+  if (sortBy === "dueDate") {
+    arr.sort((a, b) => {
+      if (!a.dueDate && !b.dueDate) return 0;
+      if (!a.dueDate) return 1;
+      if (!b.dueDate) return -1;
+      return new Date(a.dueDate) - new Date(b.dueDate);
+    });
+  } else if (sortBy === "priority") {
+    arr.sort((a, b) => (PRIORITY_ORDER[a.priority] ?? 3) - (PRIORITY_ORDER[b.priority] ?? 3));
+  }
+  return arr;
+}
+
+function statusForDropId(dropId) {
+  return dropId.replace("lane:", "");
 }
 
 export default function CustomCategoryKanbanBoard({
-  loading,
-  notDone,
-  done,
-  onAddTask,
-  onEditTask,
-  onDeleteTask,
-  onMoveTask,
+  loading, notStarted, inProgress, finished,
+  onAddTask, onEditTask, onDeleteTask, onMoveTask,
 }) {
-  const [newName, setNewName] = useState("");
-  const [submitting, setSubmitting] = useState(false);
+  const [modalOpen, setModalOpen] = useState(false);
+  const [editingTask, setEditingTask] = useState(null);
+  const [modalLoading, setModalLoading] = useState(false);
+  const [sortBy, setSortBy] = useState("default");
 
   const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }));
 
-  // Keep a local order per lane for nicer UX, but still driven by server state.
-  const [notDoneOrder, setNotDoneOrder] = useState([]);
-  const [doneOrder, setDoneOrder] = useState([]);
+  const tasksByLane = useMemo(() => ({
+    NOT_STARTED: sortTasks(notStarted ?? [], sortBy),
+    IN_PROGRESS: sortTasks(inProgress ?? [], sortBy),
+    FINISHED:    sortTasks(finished ?? [], sortBy),
+  }), [notStarted, inProgress, finished, sortBy]);
 
-  const orderedNotDone = useMemo(() => {
-    const byId = new Map((notDone ?? []).map((t) => [String(t.taskId), t]));
-    const ids = [
-      ...notDoneOrder.filter((id) => byId.has(id)),
-      ...Array.from(byId.keys()).filter((id) => !notDoneOrder.includes(id)),
-    ];
-    return ids.map((id) => byId.get(id));
-  }, [notDone, notDoneOrder]);
+  const allTasks = useMemo(() => [
+    ...(notStarted ?? []),
+    ...(inProgress ?? []),
+    ...(finished ?? []),
+  ], [notStarted, inProgress, finished]);
 
-  const orderedDone = useMemo(() => {
-    const byId = new Map((done ?? []).map((t) => [String(t.taskId), t]));
-    const ids = [
-      ...doneOrder.filter((id) => byId.has(id)),
-      ...Array.from(byId.keys()).filter((id) => !doneOrder.includes(id)),
-    ];
-    return ids.map((id) => byId.get(id));
-  }, [done, doneOrder]);
+  function findTask(id) {
+    return allTasks.find((t) => String(t.taskId) === String(id));
+  }
 
-  async function submitNewTask(e) {
-    e.preventDefault();
-    const name = newName.trim();
-    if (!name || submitting) return;
-    setSubmitting(true);
-    try {
-      await onAddTask(name);
-      setNewName("");
-    } finally {
-      setSubmitting(false);
+  async function handleDragEnd({ active, over }) {
+    if (!over) return;
+    const activeId = String(active.id);
+    const overId = String(over.id);
+    if (activeId === overId) return;
+
+    const task = findTask(activeId);
+    if (!task) return;
+
+    if (overId.startsWith("lane:")) {
+      const toStatus = statusForDropId(overId);
+      if (task.status !== toStatus) await onMoveTask(task.taskId, toStatus);
+      return;
+    }
+
+    const overTask = findTask(overId);
+    if (!overTask) return;
+
+    if (task.status !== overTask.status) {
+      await onMoveTask(task.taskId, overTask.status);
     }
   }
 
-  function findTaskById(id) {
-    const all = [...(notDone ?? []), ...(done ?? [])];
-    return all.find((t) => String(t.taskId) === String(id));
-  }
-
-  async function handleDragEnd(event) {
-    const { active, over } = event;
-    if (!over) return;
-
-    const activeId = String(active.id);
-    const overId = String(over.id);
-
-    if (activeId === overId) return;
-
-    const task = findTaskById(activeId);
-    if (!task) return;
-
-    // Moving over a column dropzone
-    if (overId === "lane:notDone" || overId === "lane:done") {
-      const toFinished = overId === "lane:done";
-      if (Boolean(task.isFinished) === toFinished) return;
-      await onMoveTask(task.taskId, toFinished);
-      return;
-    }
-
-    // Sorting within a lane: keep it frontend-only.
-    const overTask = findTaskById(overId);
-    if (!overTask) return;
-
-    const fromLane = laneIdForTask(task);
-    const toLane = laneIdForTask(overTask);
-
-    if (fromLane !== toLane) {
-      // if dropped onto a card in the other lane => treat as lane move
-      await onMoveTask(task.taskId, toLane === "done");
-      return;
-    }
-
-    if (fromLane === "notDone") {
-      const ids = orderedNotDone.map((t) => String(t.taskId));
-      const oldIndex = ids.indexOf(activeId);
-      const newIndex = ids.indexOf(overId);
-      if (oldIndex >= 0 && newIndex >= 0 && oldIndex !== newIndex) {
-        setNotDoneOrder(arrayMove(ids, oldIndex, newIndex));
+  async function handleModalSubmit(payload) {
+    setModalLoading(true);
+    try {
+      if (editingTask) {
+        await onEditTask(editingTask.taskId, payload);
+      } else {
+        await onAddTask(payload);
       }
-    } else {
-      const ids = orderedDone.map((t) => String(t.taskId));
-      const oldIndex = ids.indexOf(activeId);
-      const newIndex = ids.indexOf(overId);
-      if (oldIndex >= 0 && newIndex >= 0 && oldIndex !== newIndex) {
-        setDoneOrder(arrayMove(ids, oldIndex, newIndex));
-      }
+      setModalOpen(false);
+      setEditingTask(null);
+    } finally {
+      setModalLoading(false);
     }
   }
 
+  const total = allTasks.length;
+
   return (
-    <div className="space-y-6">
-      <div className="rounded-xl border border-white/10 bg-neutral-900/40 p-4">
-        <div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
-          <div>
-            <div className="text-sm font-semibold text-white/90">
-              Tickets
-              <span className="ml-2 text-xs font-normal text-white/60">
-                Drag cards left/right to change status.
-              </span>
-            </div>
-            <div className="mt-1 text-xs text-white/55">
-              Tip: grab the <span className="font-semibold text-white/80">drag</span> handle.
-            </div>
+    <div className="space-y-4">
+      <div className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-white/10 bg-neutral-900/40 px-4 py-3">
+        <div className="flex flex-wrap items-center gap-3 text-xs text-white/70">
+          <span className="rounded-full bg-slate-500/20 px-2 py-1 text-slate-200">
+            Not started: {(notStarted ?? []).length}
+          </span>
+          <span className="rounded-full bg-amber-500/15 px-2 py-1 text-amber-200">
+            In progress: {(inProgress ?? []).length}
+          </span>
+          <span className="rounded-full bg-emerald-500/15 px-2 py-1 text-emerald-200">
+            Finished: {(finished ?? []).length}
+          </span>
+          <span className="text-white/40">/ {total} total</span>
+        </div>
+
+        <div className="flex items-center gap-2">
+          <div className="flex items-center gap-1.5 rounded-lg border border-white/10 bg-black/30 px-2 py-1.5">
+            <MdSort className="size-4 text-white/50" />
+            <span className="text-xs text-white/50">Sort:</span>
+            <select
+              value={sortBy}
+              onChange={(e) => setSortBy(e.target.value)}
+              className="bg-transparent text-xs text-white/80 outline-none cursor-pointer"
+            >
+              <option value="default">Default</option>
+              <option value="dueDate">Due date</option>
+              <option value="priority">Priority</option>
+            </select>
           </div>
 
-          <div className="flex items-center gap-3 text-xs text-white/70">
-            <span className="rounded-full bg-sky-500/15 px-2 py-1 text-sky-200">
-              Not done: {(notDone ?? []).length}
-            </span>
-            <span className="rounded-full bg-emerald-500/15 px-2 py-1 text-emerald-200">
-              Done: {(done ?? []).length}
-            </span>
-          </div>
+          <button
+            onClick={() => { setEditingTask(null); setModalOpen(true); }}
+            className="flex items-center gap-1.5 rounded-lg bg-green-400 px-3 py-1.5 text-sm font-medium text-black hover:bg-green-300"
+          >
+            <MdAdd className="size-4" />
+            New ticket
+          </button>
         </div>
-
-        <form onSubmit={submitNewTask} className="mt-4 flex flex-col gap-3 md:flex-row">
-          <input
-            value={newName}
-            onChange={(e) => setNewName(e.target.value)}
-            placeholder="Add a new ticket (e.g. Fix bug #123)"
-            maxLength={200}
-            className="w-full rounded-lg border border-white/10 bg-black/30 px-3 py-2 text-sm outline-none focus:border-green-400"
-          />
-          <button
-            className="rounded-lg bg-green-400 px-4 py-2 text-sm font-medium text-black disabled:opacity-60"
-            type="submit"
-            disabled={!newName.trim() || submitting}
-          >
-            {submitting ? "Adding..." : "Add"}
-          </button>
-        </form>
       </div>
 
-      <DndContext
-        sensors={sensors}
-        collisionDetection={closestCenter}
-        onDragEnd={handleDragEnd}
-      >
-        <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
-          <KanbanColumn
-            title="Not done"
-            droppableId="lane:notDone"
-            count={orderedNotDone.length}
-          >
-            <SortableContext
-              items={orderedNotDone.map((t) => String(t.taskId))}
-              strategy={rectSortingStrategy}
-            >
-              <div className="space-y-3">
-                {loading ? (
-                  <div className="text-sm opacity-70">Loading...</div>
-                ) : orderedNotDone.length === 0 ? (
-                  <div className="text-sm opacity-70">No tickets yet.</div>
-                ) : (
-                  orderedNotDone.map((t) => (
-                    <TaskTicketCard
-                      key={t.taskId}
-                      task={t}
-                      onEdit={(name) => onEditTask(t.taskId, name)}
-                      onDelete={() => onDeleteTask(t.taskId)}
-                    />
-                  ))
-                )}
-              </div>
-            </SortableContext>
-          </KanbanColumn>
-
-          <KanbanColumn
-            title="Done"
-            droppableId="lane:done"
-            count={orderedDone.length}
-          >
-            <SortableContext
-              items={orderedDone.map((t) => String(t.taskId))}
-              strategy={rectSortingStrategy}
-            >
-              <div className="space-y-3">
-                {loading ? (
-                  <div className="text-sm opacity-70">Loading...</div>
-                ) : orderedDone.length === 0 ? (
-                  <div className="text-sm opacity-70">Nothing done yet.</div>
-                ) : (
-                  orderedDone.map((t) => (
-                    <TaskTicketCard
-                      key={t.taskId}
-                      task={t}
-                      onEdit={(name) => onEditTask(t.taskId, name)}
-                      onDelete={() => onDeleteTask(t.taskId)}
-                    />
-                  ))
-                )}
-              </div>
-            </SortableContext>
-          </KanbanColumn>
+      <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
+        <div className="grid grid-cols-1 gap-4 md:grid-cols-3">
+          {LANES.map((lane) => {
+            const tasks = tasksByLane[lane.id] ?? [];
+            return (
+              <KanbanColumn
+                key={lane.id}
+                laneId={lane.id}
+                title={lane.title}
+                droppableId={lane.droppableId}
+                count={tasks.length}
+              >
+                <SortableContext
+                  items={tasks.map((t) => String(t.taskId))}
+                  strategy={rectSortingStrategy}
+                >
+                  <div className="space-y-3">
+                    {loading ? (
+                      <div className="text-sm opacity-70">Loading...</div>
+                    ) : tasks.length === 0 ? (
+                      <div className="text-sm opacity-40">No tickets.</div>
+                    ) : (
+                      tasks.map((t) => (
+                        <TaskTicketCard
+                          key={t.taskId}
+                          task={t}
+                          onEdit={() => { setEditingTask(t); setModalOpen(true); }}
+                          onDelete={() => onDeleteTask(t.taskId)}
+                        />
+                      ))
+                    )}
+                  </div>
+                </SortableContext>
+              </KanbanColumn>
+            );
+          })}
         </div>
       </DndContext>
+
+      <TaskFormModal
+        isOpen={modalOpen}
+        onClose={() => { setModalOpen(false); setEditingTask(null); }}
+        onSubmit={handleModalSubmit}
+        task={editingTask}
+        isLoading={modalLoading}
+      />
     </div>
   );
 }
-
-
Index: frontend/src/pages/Dashboard/pages/CustomCategory/components/KanbanColumn.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/CustomCategory/components/KanbanColumn.jsx	(revision 194c01621e96510fb6f238b2098a64b3663ec780)
+++ frontend/src/pages/Dashboard/pages/CustomCategory/components/KanbanColumn.jsx	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
@@ -1,30 +1,36 @@
 import React, { useMemo } from "react";
 import { useDroppable } from "@dnd-kit/core";
-import { MdCheckCircle, MdRadioButtonUnchecked } from "react-icons/md";
+import { MdCheckCircle, MdRadioButtonUnchecked, MdAutorenew } from "react-icons/md";
 
-export default function KanbanColumn({ title, droppableId, count, children }) {
+const VARIANTS = {
+  NOT_STARTED: {
+    Icon: MdRadioButtonUnchecked,
+    header: "from-slate-500/20 via-slate-500/8 to-transparent",
+    badge: "bg-slate-500/15 text-slate-200 border-slate-500/30",
+    ring: "group-hover:ring-slate-500/25",
+    drop: "border-slate-400/60 bg-slate-400/10",
+    border: "border-slate-500/20",
+  },
+  IN_PROGRESS: {
+    Icon: MdAutorenew,
+    header: "from-amber-500/20 via-amber-500/8 to-transparent",
+    badge: "bg-amber-500/15 text-amber-200 border-amber-500/30",
+    ring: "group-hover:ring-amber-500/25",
+    drop: "border-amber-400/60 bg-amber-400/10",
+    border: "border-amber-500/20",
+  },
+  FINISHED: {
+    Icon: MdCheckCircle,
+    header: "from-emerald-500/25 via-emerald-500/10 to-transparent",
+    badge: "bg-emerald-500/15 text-emerald-200 border-emerald-500/30",
+    ring: "group-hover:ring-emerald-500/25",
+    drop: "border-emerald-400/60 bg-emerald-400/10",
+    border: "border-emerald-500/20",
+  },
+};
+
+export default function KanbanColumn({ laneId, title, droppableId, count, children }) {
   const { setNodeRef, isOver } = useDroppable({ id: droppableId });
-
-  const variant = useMemo(() => {
-    if (droppableId === "lane:done") {
-      return {
-        Icon: MdCheckCircle,
-        header: "from-emerald-500/25 via-emerald-500/10 to-transparent",
-        badge: "bg-emerald-500/15 text-emerald-200 border-emerald-500/30",
-        ring: "group-hover:ring-emerald-500/25",
-        drop: "border-emerald-400/60 bg-emerald-400/10",
-        border: "border-emerald-500/20",
-      };
-    }
-
-    return {
-      Icon: MdRadioButtonUnchecked,
-      header: "from-sky-500/25 via-sky-500/10 to-transparent",
-      badge: "bg-sky-500/15 text-sky-200 border-sky-500/30",
-      ring: "group-hover:ring-sky-500/25",
-      drop: "border-sky-400/60 bg-sky-400/10",
-      border: "border-sky-500/20",
-    };
-  }, [droppableId]);
+  const v = VARIANTS[laneId] ?? VARIANTS.NOT_STARTED;
 
   return (
@@ -32,23 +38,14 @@
       className={
         "group overflow-hidden rounded-xl border bg-neutral-900/40 shadow-sm ring-1 ring-white/5 transition " +
-        variant.border +
-        " " +
-        variant.ring
+        v.border + " " + v.ring
       }
     >
-      <div className={"bg-gradient-to-b " + variant.header + " p-4"}>
+      <div className={"bg-gradient-to-b " + v.header + " p-4"}>
         <div className="flex items-center justify-between gap-3">
           <div className="flex items-center gap-2">
-            <variant.Icon className="size-5 text-white/80" />
-            <h2 className="text-sm font-semibold tracking-wide text-white/90">
-              {title}
-            </h2>
+            <v.Icon className="size-5 text-white/80" />
+            <h2 className="text-sm font-semibold tracking-wide text-white/90">{title}</h2>
           </div>
-          <span
-            className={
-              "rounded-full border px-2 py-0.5 text-xs font-medium " +
-              variant.badge
-            }
-          >
+          <span className={"rounded-full border px-2 py-0.5 text-xs font-medium " + v.badge}>
             {count}
           </span>
@@ -60,5 +57,5 @@
         className={
           "min-h-[220px] p-4 transition-colors " +
-          (isOver ? variant.drop : "border-t border-white/10")
+          (isOver ? v.drop : "border-t border-white/10")
         }
       >
@@ -68,3 +65,2 @@
   );
 }
-
Index: frontend/src/pages/Dashboard/pages/CustomCategory/components/TaskTicketCard.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/CustomCategory/components/TaskTicketCard.jsx	(revision 194c01621e96510fb6f238b2098a64b3663ec780)
+++ frontend/src/pages/Dashboard/pages/CustomCategory/components/TaskTicketCard.jsx	(revision 545738f221bcda7888ec3b35279f55d0f80c4faa)
@@ -1,36 +1,40 @@
-import React, { useMemo, useState } from "react";
+import React, { useMemo } from "react";
 import { useSortable } from "@dnd-kit/sortable";
 import { CSS } from "@dnd-kit/utilities";
-import { MdDelete, MdDragIndicator, MdEdit, MdSave } from "react-icons/md";
+import { MdDelete, MdDragIndicator, MdEdit, MdCalendarToday } from "react-icons/md";
+
+const PRIORITY_STYLES = {
+  HIGH:   "bg-red-500/15 text-red-300 border-red-500/30",
+  MEDIUM: "bg-amber-500/15 text-amber-300 border-amber-500/30",
+  LOW:    "bg-sky-500/15 text-sky-300 border-sky-500/30",
+};
+
+const STATUS_DOT = {
+  NOT_STARTED: "bg-slate-400",
+  IN_PROGRESS: "bg-amber-400",
+  FINISHED:    "bg-emerald-400",
+};
+
+function formatDate(dateStr) {
+  if (!dateStr) return null;
+  const d = new Date(dateStr);
+  return d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" });
+}
+
+function isOverdue(dateStr) {
+  if (!dateStr) return false;
+  return new Date(dateStr) < new Date(new Date().toDateString());
+}
 
 export default function TaskTicketCard({ task, onEdit, onDelete }) {
   const id = String(task.taskId);
-  const {
-    attributes,
-    listeners,
-    setNodeRef,
-    transform,
+  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
+
+  const style = useMemo(() => ({
+    transform: CSS.Transform.toString(transform),
     transition,
-    isDragging,
-  } = useSortable({ id });
+  }), [transform, transition]);
 
-  const style = useMemo(
-    () => ({
-      transform: CSS.Transform.toString(transform),
-      transition,
-    }),
-    [transform, transition]
-  );
-
-  const [editing, setEditing] = useState(false);
-  const [draft, setDraft] = useState(task.name ?? "");
-
-  async function submitEdit(e) {
-    e.preventDefault();
-    const next = draft.trim();
-    if (!next) return;
-    await onEdit(next);
-    setEditing(false);
-  }
+  const overdue = task.status !== "FINISHED" && isOverdue(task.dueDate);
 
   return (
@@ -40,81 +44,68 @@
       className={
         "group rounded-lg border border-white/10 bg-black/30 p-3 shadow-sm transition hover:border-white/20 hover:bg-black/35 " +
-        (task.isFinished ? "opacity-90" : "") +
-        (isDragging ? " opacity-60" : "")
+        (task.status === "FINISHED" ? "opacity-80" : "") +
+        (isDragging ? " opacity-50 scale-95" : "")
       }
     >
-      <div className="flex items-start justify-between gap-3">
+      {/* Header row */}
+      <div className="flex items-start justify-between gap-2">
         <div className="min-w-0 flex-1">
-          {editing ? (
-            <form onSubmit={submitEdit} className="flex gap-2">
-              <input
-                value={draft}
-                onChange={(e) => setDraft(e.target.value)}
-                maxLength={200}
-                className="w-full rounded-md border border-white/10 bg-black/40 px-2 py-1 text-sm outline-none focus:border-green-400"
-                autoFocus
-              />
-              <button
-                type="submit"
-                className="inline-flex items-center gap-1 rounded-md bg-green-400 px-2 py-1 text-sm font-medium text-black"
-              >
-                <MdSave className="size-4" />
-                Save
-              </button>
-            </form>
-          ) : (
-            <div className="flex items-center gap-2">
-              <span
-                className={
-                  "h-2 w-2 shrink-0 rounded-full " +
-                  (task.isFinished ? "bg-emerald-400" : "bg-sky-400")
-                }
-              />
-              <div className="min-w-0 text-sm font-medium text-white/90">
-                {task.name}
-              </div>
-            </div>
-          )}
+          <div className="flex items-center gap-2">
+            <span className={"h-2 w-2 shrink-0 rounded-full " + (STATUS_DOT[task.status] ?? "bg-slate-400")} />
+            <span className="text-sm font-medium text-white/90 leading-snug">{task.name}</span>
+          </div>
 
-          <div className="mt-1 text-xs text-white/50">Ticket #{task.taskId}</div>
+          {task.description ? (
+            <p className="mt-1.5 text-xs text-white/50 line-clamp-2">{task.description}</p>
+          ) : null}
         </div>
 
-        <div className="flex shrink-0 items-center gap-2">
-          {!editing ? (
-            <button
-              type="button"
-              onClick={() => {
-                setDraft(task.name ?? "");
-                setEditing(true);
-              }}
-              className="rounded-md p-1 text-white/70 hover:bg-white/10 hover:text-white"
-              title="Edit"
-            >
-              <MdEdit className="size-5" />
-            </button>
-          ) : null}
-
+        <div className="flex shrink-0 items-center gap-1">
+          <button
+            type="button"
+            onClick={onEdit}
+            className="rounded-md p-1 text-white/50 hover:bg-white/10 hover:text-white"
+            title="Edit"
+          >
+            <MdEdit className="size-4" />
+          </button>
           <button
             type="button"
             onClick={onDelete}
-            className="rounded-md p-1 text-white/70 hover:bg-white/10 hover:text-white"
+            className="rounded-md p-1 text-white/50 hover:bg-white/10 hover:text-white"
             title="Delete"
           >
-            <MdDelete className="size-5" />
+            <MdDelete className="size-4" />
           </button>
-
           <button
             type="button"
-            className="inline-flex cursor-grab items-center gap-1 rounded-md p-1 text-white/70 hover:bg-white/10 hover:text-white active:cursor-grabbing"
+            className="cursor-grab rounded-md p-1 text-white/50 hover:bg-white/10 hover:text-white active:cursor-grabbing"
             title="Drag"
             {...attributes}
             {...listeners}
           >
-            <MdDragIndicator className="size-5" />
+            <MdDragIndicator className="size-4" />
           </button>
         </div>
+      </div>
+
+      {/* Footer row: priority + due date */}
+      <div className="mt-2 flex flex-wrap items-center gap-2">
+        <span className="text-xs text-white/30">#{task.taskId}</span>
+
+        {task.priority ? (
+          <span className={"rounded-full border px-2 py-0.5 text-xs font-medium " + (PRIORITY_STYLES[task.priority] ?? PRIORITY_STYLES.LOW)}>
+            {task.priority.charAt(0) + task.priority.slice(1).toLowerCase()}
+          </span>
+        ) : null}
+
+        {task.dueDate ? (
+          <span className={"flex items-center gap-1 text-xs " + (overdue ? "text-red-400" : "text-white/40")}>
+            <MdCalendarToday className="size-3" />
+            {overdue ? "Overdue · " : ""}{formatDate(task.dueDate)}
+          </span>
+        ) : null}
       </div>
     </div>
   );
 }
-
