Index: dea/copilotDiffState.xml
===================================================================
--- .idea/copilotDiffState.xml	(revision 8449b9cc520ba82bb2945db031d4b9973a038e59)
+++ 	(revision )
@@ -1,18 +1,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="CopilotDiffPersistence">
-    <option name="pendingDiffs">
-      <map>
-        <entry key="$PROJECT_DIR$/frontend/src/pages/Dashboard/pages/Finance/components/FinanceSummaryCard.jsx">
-          <value>
-            <PendingDiffInfo>
-              <option name="filePath" value="$PROJECT_DIR$/frontend/src/pages/Dashboard/pages/Finance/components/FinanceSummaryCard.jsx" />
-              <option name="originalContent" value="import React from &quot;react&quot;;&#10;import FinanceLegend from &quot;./FinanceLegend.jsx&quot;;&#10;import FinancePieChart from &quot;./FinancePieChart.jsx&quot;;&#10;&#10;export default function FinanceSummaryCard({ segments }) {&#10;  return (&#10;    &lt;div className=&quot;card bg-base-200 border border-base-300&quot;&gt;&#10;      &lt;div className=&quot;card-body&quot;&gt;&#10;        &lt;div className=&quot;flex items-start justify-between gap-6 flex-wrap&quot;&gt;&#10;          &lt;div&gt;&#10;            &lt;h2 className=&quot;card-title&quot;&gt;Budget distribution&lt;/h2&gt;&#10;            &lt;p className=&quot;text-sm opacity-80&quot;&gt;&#10;              Your current percentages (stored in your finance profile).&#10;            &lt;/p&gt;&#10;          &lt;/div&gt;&#10;        &lt;/div&gt;&#10;&#10;        &lt;div className=&quot;mt-4 grid grid-cols-1 gap-6 md:grid-cols-[auto_1fr] items-center&quot;&gt;&#10;          &lt;div className=&quot;flex justify-center md:justify-start&quot;&gt;&#10;            &lt;FinancePieChart segments={segments} size={180} /&gt;&#10;          &lt;/div&gt;&#10;          &lt;FinanceLegend segments={segments} /&gt;&#10;        &lt;/div&gt;&#10;      &lt;/div&gt;&#10;    &lt;/div&gt;&#10;  );&#10;}&#10;&#10;" />
-              <option name="updatedContent" value="import React from &quot;react&quot;;&#10;import FinanceLegend from &quot;./FinanceLegend.jsx&quot;;&#10;import FinancePieChart from &quot;./FinancePieChart.jsx&quot;;&#10;&#10;function formatMoney(value) {&#10;  const n = Number(value);&#10;  if (!Number.isFinite(n)) return &quot;—&quot;;&#10;  try {&#10;    return new Intl.NumberFormat(undefined, {&#10;      minimumFractionDigits: 2,&#10;      maximumFractionDigits: 2,&#10;    }).format(n);&#10;  } catch {&#10;    return n.toFixed(2);&#10;  }&#10;}&#10;&#10;export default function FinanceSummaryCard({ segments, totalThisMonth, onEdit }) {&#10;  return (&#10;    &lt;div className=&quot;card bg-base-200 border border-base-300&quot;&gt;&#10;      &lt;div className=&quot;card-body&quot;&gt;&#10;        &lt;div className=&quot;flex items-start justify-between gap-6 flex-wrap&quot;&gt;&#10;          &lt;div&gt;&#10;            &lt;h2 className=&quot;card-title&quot;&gt;Budget distribution&lt;/h2&gt;&#10;            &lt;p className=&quot;text-sm opacity-80&quot;&gt;&#10;              Your current percentages (stored in your finance profile).&#10;            &lt;/p&gt;&#10;          &lt;/div&gt;&#10;&#10;          &lt;div className=&quot;flex items-center gap-2&quot;&gt;&#10;            &lt;button className=&quot;btn btn-outline btn-sm&quot; onClick={onEdit}&gt;&#10;              Edit&#10;            &lt;/button&gt;&#10;          &lt;/div&gt;&#10;        &lt;/div&gt;&#10;&#10;        &lt;div className=&quot;mt-4&quot;&gt;&#10;          &lt;div className=&quot;rounded-lg border border-base-300 bg-base-100 p-4&quot;&gt;&#10;            &lt;div className=&quot;text-sm opacity-70&quot;&gt;Total money earned this month&lt;/div&gt;&#10;            &lt;div className=&quot;text-2xl font-bold tabular-nums&quot;&gt;&#10;              {formatMoney(totalThisMonth)}&#10;            &lt;/div&gt;&#10;          &lt;/div&gt;&#10;        &lt;/div&gt;&#10;&#10;        &lt;div className=&quot;mt-6 grid grid-cols-1 gap-6 md:grid-cols-[auto_1fr] items-center&quot;&gt;&#10;          &lt;div className=&quot;flex justify-center md:justify-start&quot;&gt;&#10;            &lt;FinancePieChart segments={segments} size={180} /&gt;&#10;          &lt;/div&gt;&#10;&#10;          &lt;div className=&quot;space-y-4&quot;&gt;&#10;            &lt;FinanceLegend segments={segments} /&gt;&#10;&#10;            &lt;div className=&quot;divider my-0&quot; /&gt;&#10;&#10;            &lt;div className=&quot;space-y-2 text-sm&quot;&gt;&#10;              {segments.map((s) =&gt; {&#10;                const pct = Number(s.value) || 0;&#10;                const allocated = ((Number(totalThisMonth) || 0) * pct) / 100;&#10;                return (&#10;                  &lt;div&#10;                    key={s.key}&#10;                    className=&quot;flex items-center justify-between gap-3&quot;&#10;                  &gt;&#10;                    &lt;div className=&quot;flex items-center gap-2&quot;&gt;&#10;                      &lt;span&#10;                        className=&quot;inline-block h-3 w-3 rounded&quot;&#10;                        style={{ backgroundColor: s.color }}&#10;                      /&gt;&#10;                      &lt;span className=&quot;opacity-80&quot;&gt;&#10;                        {s.label} allocation&#10;                      &lt;/span&gt;&#10;                    &lt;/div&gt;&#10;                    &lt;span className=&quot;font-medium tabular-nums&quot;&gt;&#10;                      {formatMoney(allocated)}&#10;                    &lt;/span&gt;&#10;                  &lt;/div&gt;&#10;                );&#10;              })}&#10;            &lt;/div&gt;&#10;          &lt;/div&gt;&#10;        &lt;/div&gt;&#10;      &lt;/div&gt;&#10;    &lt;/div&gt;&#10;  );&#10;}&#10;&#10;" />
-            </PendingDiffInfo>
-          </value>
-        </entry>
-      </map>
-    </option>
-  </component>
-</project>
Index: backend/src/main/java/com/trekr/backend/config/SecurityConfig.java
===================================================================
--- backend/src/main/java/com/trekr/backend/config/SecurityConfig.java	(revision 8449b9cc520ba82bb2945db031d4b9973a038e59)
+++ backend/src/main/java/com/trekr/backend/config/SecurityConfig.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
@@ -50,6 +50,7 @@
                 "http://localhost:5173",
                 "http://127.0.0.1:5173"));
-        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
-        config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
+        config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
+        config.setAllowedHeaders(List.of("Authorization", "Content-Type", "Accept", "Origin", "X-Requested-With"));
+        config.setAllowCredentials(true);
 
         UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
Index: backend/src/main/java/com/trekr/backend/controller/DisciplineController.java
===================================================================
--- backend/src/main/java/com/trekr/backend/controller/DisciplineController.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ backend/src/main/java/com/trekr/backend/controller/DisciplineController.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
@@ -0,0 +1,122 @@
+package com.trekr.backend.controller;
+
+import com.trekr.backend.dto.discipline.CreateTaskRequest;
+import com.trekr.backend.dto.discipline.ComputeDailyCompletionResponse;
+import com.trekr.backend.dto.discipline.DailyCompletionsResponse;
+import com.trekr.backend.dto.discipline.TaskDto;
+import com.trekr.backend.dto.discipline.TasksResponse;
+import com.trekr.backend.dto.discipline.TrackingStatusResponse;
+import com.trekr.backend.dto.discipline.UpdateTaskFinishedRequest;
+import com.trekr.backend.dto.discipline.UpdateTaskRequest;
+import com.trekr.backend.security.UserPrincipal;
+import com.trekr.backend.service.DisciplineService;
+import jakarta.validation.Valid;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.core.Authentication;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDate;
+
+@RestController
+@RequestMapping("/api/discipline")
+public class DisciplineController {
+
+    private final DisciplineService disciplineService;
+
+    public DisciplineController(DisciplineService disciplineService) {
+        this.disciplineService = disciplineService;
+    }
+
+    @GetMapping("/status")
+    public TrackingStatusResponse status(Authentication authentication) {
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        boolean tracking = disciplineService.isTracking(principal.getUserId());
+        return new TrackingStatusResponse(tracking);
+    }
+
+    @PostMapping("/start")
+    @ResponseStatus(HttpStatus.CREATED)
+    public TrackingStatusResponse start(Authentication authentication) {
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        disciplineService.startTracking(principal.getUserId());
+        return new TrackingStatusResponse(true);
+    }
+
+    @GetMapping("/tasks")
+    public TasksResponse tasks(
+            @RequestParam(defaultValue = "0") int page,
+            @RequestParam(defaultValue = "20") int size,
+            Authentication authentication) {
+        if (size < 1)
+            size = 20;
+        if (size > 200)
+            size = 200;
+        if (page < 0)
+            page = 0;
+
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        return disciplineService.getTasks(principal.getUserId(), page, size);
+    }
+
+    @PostMapping("/tasks")
+    @ResponseStatus(HttpStatus.CREATED)
+    public TaskDto createTask(
+            @Valid @RequestBody CreateTaskRequest request,
+            Authentication authentication) {
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        return disciplineService.createTask(principal.getUserId(), request);
+    }
+
+    @PutMapping("/tasks/{taskId}")
+    public TaskDto updateTask(
+            @PathVariable Long taskId,
+            @Valid @RequestBody UpdateTaskRequest request,
+            Authentication authentication) {
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        return disciplineService.updateTask(principal.getUserId(), taskId, request);
+    }
+
+    @PatchMapping("/tasks/{taskId}/finished")
+    public TaskDto updateTaskFinished(
+            @PathVariable Long taskId,
+            @Valid @RequestBody UpdateTaskFinishedRequest request,
+            Authentication authentication) {
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        return disciplineService.updateTaskFinished(principal.getUserId(), taskId, request);
+    }
+
+    @DeleteMapping("/tasks/{taskId}")
+    @ResponseStatus(HttpStatus.NO_CONTENT)
+    public void deleteTask(
+            @PathVariable Long taskId,
+            Authentication authentication) {
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        disciplineService.deleteTask(principal.getUserId(), taskId);
+    }
+
+    @PostMapping("/daily-completions/compute")
+    public ComputeDailyCompletionResponse computeDailyCompletion(
+            @RequestParam(required = false) LocalDate date,
+            Authentication authentication) {
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        LocalDate target = (date != null) ? date : LocalDate.now();
+        return disciplineService.computeDailyCompletion(principal.getUserId(), target);
+    }
+
+    @GetMapping("/daily-completions")
+    public DailyCompletionsResponse dailyCompletions(
+            @RequestParam(defaultValue = "0") int page,
+            @RequestParam(defaultValue = "14") int size,
+            Authentication authentication) {
+        if (size < 1)
+            size = 14;
+        if (size > 365)
+            size = 365;
+        if (page < 0)
+            page = 0;
+
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        return disciplineService.getDailyCompletions(principal.getUserId(), page, size);
+    }
+}
+
Index: backend/src/main/java/com/trekr/backend/dto/discipline/ComputeDailyCompletionResponse.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/discipline/ComputeDailyCompletionResponse.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ backend/src/main/java/com/trekr/backend/dto/discipline/ComputeDailyCompletionResponse.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
@@ -0,0 +1,5 @@
+package com.trekr.backend.dto.discipline;
+
+public record ComputeDailyCompletionResponse(boolean created, DailyCompletionDto completion) {
+}
+
Index: backend/src/main/java/com/trekr/backend/dto/discipline/CreateTaskRequest.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/discipline/CreateTaskRequest.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ backend/src/main/java/com/trekr/backend/dto/discipline/CreateTaskRequest.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
@@ -0,0 +1,21 @@
+package com.trekr.backend.dto.discipline;
+
+import jakarta.validation.constraints.NotBlank;
+
+public class CreateTaskRequest {
+
+    @NotBlank
+    private String name;
+
+    public CreateTaskRequest() {
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+}
+
Index: backend/src/main/java/com/trekr/backend/dto/discipline/DailyCompletionDto.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/discipline/DailyCompletionDto.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ backend/src/main/java/com/trekr/backend/dto/discipline/DailyCompletionDto.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
@@ -0,0 +1,8 @@
+package com.trekr.backend.dto.discipline;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+public record DailyCompletionDto(Long dailyCompletionId, LocalDate date, BigDecimal procent) {
+}
+
Index: backend/src/main/java/com/trekr/backend/dto/discipline/DailyCompletionsResponse.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/discipline/DailyCompletionsResponse.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ backend/src/main/java/com/trekr/backend/dto/discipline/DailyCompletionsResponse.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
@@ -0,0 +1,7 @@
+package com.trekr.backend.dto.discipline;
+
+import java.util.List;
+
+public record DailyCompletionsResponse(List<DailyCompletionDto> completions, boolean hasMore) {
+}
+
Index: backend/src/main/java/com/trekr/backend/dto/discipline/TaskDto.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/discipline/TaskDto.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ backend/src/main/java/com/trekr/backend/dto/discipline/TaskDto.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
@@ -0,0 +1,5 @@
+package com.trekr.backend.dto.discipline;
+
+public record TaskDto(Long taskId, String name, boolean isFinished) {
+}
+
Index: backend/src/main/java/com/trekr/backend/dto/discipline/TasksResponse.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/discipline/TasksResponse.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ backend/src/main/java/com/trekr/backend/dto/discipline/TasksResponse.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
@@ -0,0 +1,7 @@
+package com.trekr.backend.dto.discipline;
+
+import java.util.List;
+
+public record TasksResponse(List<TaskDto> tasks, boolean hasMore) {
+}
+
Index: backend/src/main/java/com/trekr/backend/dto/discipline/TrackingStatusResponse.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/discipline/TrackingStatusResponse.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ backend/src/main/java/com/trekr/backend/dto/discipline/TrackingStatusResponse.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
@@ -0,0 +1,5 @@
+package com.trekr.backend.dto.discipline;
+
+public record TrackingStatusResponse(boolean tracking) {
+}
+
Index: backend/src/main/java/com/trekr/backend/dto/discipline/UpdateTaskFinishedRequest.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/discipline/UpdateTaskFinishedRequest.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ backend/src/main/java/com/trekr/backend/dto/discipline/UpdateTaskFinishedRequest.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
@@ -0,0 +1,21 @@
+package com.trekr.backend.dto.discipline;
+
+import jakarta.validation.constraints.NotNull;
+
+public class UpdateTaskFinishedRequest {
+
+    @NotNull
+    private Boolean isFinished;
+
+    public UpdateTaskFinishedRequest() {
+    }
+
+    public Boolean getIsFinished() {
+        return isFinished;
+    }
+
+    public void setIsFinished(Boolean isFinished) {
+        this.isFinished = isFinished;
+    }
+}
+
Index: backend/src/main/java/com/trekr/backend/dto/discipline/UpdateTaskRequest.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/discipline/UpdateTaskRequest.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ backend/src/main/java/com/trekr/backend/dto/discipline/UpdateTaskRequest.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
@@ -0,0 +1,21 @@
+package com.trekr.backend.dto.discipline;
+
+import jakarta.validation.constraints.NotBlank;
+
+public class UpdateTaskRequest {
+
+    @NotBlank
+    private String name;
+
+    public UpdateTaskRequest() {
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+}
+
Index: backend/src/main/java/com/trekr/backend/repository/DailyCompletionRepository.java
===================================================================
--- backend/src/main/java/com/trekr/backend/repository/DailyCompletionRepository.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ backend/src/main/java/com/trekr/backend/repository/DailyCompletionRepository.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
@@ -0,0 +1,17 @@
+package com.trekr.backend.repository;
+
+import com.trekr.backend.entity.discipline.DailyCompletion;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.time.LocalDate;
+import java.util.Optional;
+
+public interface DailyCompletionRepository extends JpaRepository<DailyCompletion, Long> {
+
+    Optional<DailyCompletion> findByUser_UserIdAndDate(Long userId, LocalDate date);
+
+    Page<DailyCompletion> findByUser_UserIdOrderByDateDescDailyCompletionIdDesc(Long userId, Pageable pageable);
+}
+
Index: backend/src/main/java/com/trekr/backend/repository/DisciplineUserRepository.java
===================================================================
--- backend/src/main/java/com/trekr/backend/repository/DisciplineUserRepository.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ backend/src/main/java/com/trekr/backend/repository/DisciplineUserRepository.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
@@ -0,0 +1,8 @@
+package com.trekr.backend.repository;
+
+import com.trekr.backend.entity.discipline.DisciplineUser;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface DisciplineUserRepository extends JpaRepository<DisciplineUser, Long> {
+}
+
Index: backend/src/main/java/com/trekr/backend/repository/TaskDailyCompletionRepository.java
===================================================================
--- backend/src/main/java/com/trekr/backend/repository/TaskDailyCompletionRepository.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ backend/src/main/java/com/trekr/backend/repository/TaskDailyCompletionRepository.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
@@ -0,0 +1,9 @@
+package com.trekr.backend.repository;
+
+import com.trekr.backend.entity.discipline.TaskDailyCompletion;
+import com.trekr.backend.entity.discipline.TaskDailyCompletionId;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface TaskDailyCompletionRepository extends JpaRepository<TaskDailyCompletion, TaskDailyCompletionId> {
+}
+
Index: backend/src/main/java/com/trekr/backend/repository/TaskRepository.java
===================================================================
--- backend/src/main/java/com/trekr/backend/repository/TaskRepository.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ backend/src/main/java/com/trekr/backend/repository/TaskRepository.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
@@ -0,0 +1,29 @@
+package com.trekr.backend.repository;
+
+import com.trekr.backend.entity.discipline.Task;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.util.List;
+
+public interface TaskRepository extends JpaRepository<Task, Long> {
+
+    Page<Task> findByDisciplineUser_UserIdOrderByTaskIdDesc(Long userId, Pageable pageable);
+
+    int deleteByTaskIdAndDisciplineUser_UserId(Long taskId, Long userId);
+
+    long countByDisciplineUser_UserId(Long userId);
+
+    long countByDisciplineUser_UserIdAndFinishedTrue(Long userId);
+
+    List<Task> findByDisciplineUser_UserIdAndFinishedTrue(Long userId);
+
+    @Modifying
+    @Query("update Task t set t.finished = false where t.disciplineUser.userId = :userId")
+    int resetFinishedForUser(@Param("userId") Long userId);
+}
+
Index: backend/src/main/java/com/trekr/backend/service/DisciplineService.java
===================================================================
--- backend/src/main/java/com/trekr/backend/service/DisciplineService.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ backend/src/main/java/com/trekr/backend/service/DisciplineService.java	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
@@ -0,0 +1,244 @@
+package com.trekr.backend.service;
+
+import com.trekr.backend.dto.discipline.CreateTaskRequest;
+import com.trekr.backend.dto.discipline.DailyCompletionDto;
+import com.trekr.backend.dto.discipline.DailyCompletionsResponse;
+import com.trekr.backend.dto.discipline.TaskDto;
+import com.trekr.backend.dto.discipline.TasksResponse;
+import com.trekr.backend.dto.discipline.ComputeDailyCompletionResponse;
+import com.trekr.backend.dto.discipline.UpdateTaskFinishedRequest;
+import com.trekr.backend.dto.discipline.UpdateTaskRequest;
+import com.trekr.backend.entity.User;
+import com.trekr.backend.entity.discipline.DisciplineUser;
+import com.trekr.backend.entity.discipline.DailyCompletion;
+import com.trekr.backend.entity.discipline.Task;
+import com.trekr.backend.entity.discipline.TaskDailyCompletion;
+import com.trekr.backend.entity.discipline.TaskDailyCompletionId;
+import com.trekr.backend.repository.DailyCompletionRepository;
+import com.trekr.backend.repository.DisciplineUserRepository;
+import com.trekr.backend.repository.TaskRepository;
+import com.trekr.backend.repository.TaskDailyCompletionRepository;
+import com.trekr.backend.repository.UserRepository;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.util.List;
+
+@Service
+public class DisciplineService {
+
+    private final DisciplineUserRepository disciplineUserRepository;
+    private final TaskRepository taskRepository;
+    private final DailyCompletionRepository dailyCompletionRepository;
+    private final TaskDailyCompletionRepository taskDailyCompletionRepository;
+    private final UserRepository userRepository;
+
+    public DisciplineService(
+            DisciplineUserRepository disciplineUserRepository,
+            TaskRepository taskRepository,
+            DailyCompletionRepository dailyCompletionRepository,
+            TaskDailyCompletionRepository taskDailyCompletionRepository,
+            UserRepository userRepository) {
+        this.disciplineUserRepository = disciplineUserRepository;
+        this.taskRepository = taskRepository;
+        this.dailyCompletionRepository = dailyCompletionRepository;
+        this.taskDailyCompletionRepository = taskDailyCompletionRepository;
+        this.userRepository = userRepository;
+    }
+
+    public boolean isTracking(Long userId) {
+        return disciplineUserRepository.existsById(userId);
+    }
+
+    @Transactional
+    public void startTracking(Long userId) {
+        if (disciplineUserRepository.existsById(userId)) {
+            return;
+        }
+
+        User user = userRepository.findById(userId)
+                .orElseThrow(() -> new RuntimeException("User not found"));
+
+        DisciplineUser disciplineUser = new DisciplineUser();
+        disciplineUser.setUser(user);
+        disciplineUserRepository.save(disciplineUser);
+    }
+
+    public TasksResponse getTasks(Long userId, int page, int size) {
+        if (!disciplineUserRepository.existsById(userId)) {
+            return new TasksResponse(List.of(), false);
+        }
+
+        Page<Task> result = taskRepository.findByDisciplineUser_UserIdOrderByTaskIdDesc(
+                userId, PageRequest.of(page, size));
+
+        List<TaskDto> tasks = result.getContent().stream()
+                .map(t -> new TaskDto(t.getTaskId(), t.getName(), t.isFinished()))
+                .toList();
+
+        return new TasksResponse(tasks, result.hasNext());
+    }
+
+    @Transactional
+    public TaskDto createTask(Long userId, CreateTaskRequest request) {
+        DisciplineUser disciplineUser = disciplineUserRepository.findById(userId)
+                .orElseThrow(() -> new RuntimeException("Discipline tracking is not enabled for this user"));
+
+        String name = request.getName();
+        if (name == null || name.isBlank()) {
+            throw new RuntimeException("Task name is required");
+        }
+
+        if (name.length() > 200) {
+            throw new RuntimeException("Task name is too long");
+        }
+
+        Task task = new Task();
+        task.setName(name.trim());
+        task.setFinished(false);
+        task.setDisciplineUser(disciplineUser);
+
+        Task saved = taskRepository.save(task);
+        return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished());
+    }
+
+    @Transactional
+    public TaskDto updateTask(Long userId, Long taskId, UpdateTaskRequest request) {
+        Task task = taskRepository.findById(taskId)
+                .orElseThrow(() -> new RuntimeException("Task not found"));
+
+        if (task.getDisciplineUser() == null || !userId.equals(task.getDisciplineUser().getUserId())) {
+            throw new RuntimeException("Task not found");
+        }
+
+        String name = request.getName();
+        if (name == null || name.isBlank()) {
+            throw new RuntimeException("Task name is required");
+        }
+
+        task.setName(name.trim());
+        Task saved = taskRepository.save(task);
+        return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished());
+    }
+
+    @Transactional
+    public TaskDto updateTaskFinished(Long userId, Long taskId, UpdateTaskFinishedRequest request) {
+        Task task = taskRepository.findById(taskId)
+                .orElseThrow(() -> new RuntimeException("Task not found"));
+
+        if (task.getDisciplineUser() == null || !userId.equals(task.getDisciplineUser().getUserId())) {
+            throw new RuntimeException("Task not found");
+        }
+
+        Boolean finished = request.getIsFinished();
+        if (finished == null) {
+            throw new RuntimeException("isFinished is required");
+        }
+
+        task.setFinished(finished);
+        Task saved = taskRepository.save(task);
+        return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished());
+    }
+
+    @Transactional
+    public void deleteTask(Long userId, Long taskId) {
+        if (taskId == null) {
+            throw new RuntimeException("Task not found");
+        }
+
+        int deleted = taskRepository.deleteByTaskIdAndDisciplineUser_UserId(taskId, userId);
+        if (deleted == 0) {
+            throw new RuntimeException("Task not found");
+        }
+    }
+
+    /**
+     * Computes the daily completion percentage for the given date:
+     * finished_tasks / total_tasks * 100
+     *
+     * - Persisted once per user+date. If already computed, it is not re-computed.
+     * - After computing, all tasks for that user are reset to unfinished.
+     */
+    @Transactional
+    public ComputeDailyCompletionResponse computeDailyCompletion(Long userId, LocalDate date) {
+        if (!disciplineUserRepository.existsById(userId)) {
+            throw new RuntimeException("Discipline tracking is not enabled for this user");
+        }
+
+        if (date == null) {
+            throw new RuntimeException("Date is required");
+        }
+
+        if (date.isAfter(LocalDate.now())) {
+            throw new RuntimeException("Date cannot be in the future");
+        }
+
+        DailyCompletion existing = dailyCompletionRepository.findByUser_UserIdAndDate(userId, date).orElse(null);
+        if (existing != null) {
+            return new ComputeDailyCompletionResponse(false,
+                    new DailyCompletionDto(existing.getDailyCompletionId(), existing.getDate(), existing.getProcent()));
+        }
+
+        long total = taskRepository.countByDisciplineUser_UserId(userId);
+        long finished = taskRepository.countByDisciplineUser_UserIdAndFinishedTrue(userId);
+
+        BigDecimal percent;
+        if (total <= 0) {
+            percent = BigDecimal.ZERO;
+        } else {
+            percent = BigDecimal.valueOf(finished)
+                    .multiply(new BigDecimal("100"))
+                    .divide(BigDecimal.valueOf(total), 2, RoundingMode.HALF_UP);
+        }
+
+        User user = userRepository.findById(userId)
+                .orElseThrow(() -> new RuntimeException("User not found"));
+
+        DailyCompletion completion = new DailyCompletion();
+        completion.setUser(user);
+        completion.setDate(date);
+        completion.setProcent(percent);
+        DailyCompletion savedCompletion = dailyCompletionRepository.save(completion);
+
+        // Link all tasks that were finished at calculation time
+        // (not strictly required for now, but matches schema and enables future analytics)
+        if (finished > 0) {
+            List<Task> finishedTasks = taskRepository.findByDisciplineUser_UserIdAndFinishedTrue(userId);
+            for (Task t : finishedTasks) {
+                TaskDailyCompletion link = new TaskDailyCompletion();
+                link.setTask(t);
+                link.setDailyCompletion(savedCompletion);
+                link.setId(new TaskDailyCompletionId(t.getTaskId(), savedCompletion.getDailyCompletionId()));
+                taskDailyCompletionRepository.save(link);
+            }
+        }
+
+        // Reset all tasks for next day
+        taskRepository.resetFinishedForUser(userId);
+
+        DailyCompletionDto dto = new DailyCompletionDto(savedCompletion.getDailyCompletionId(),
+                savedCompletion.getDate(), savedCompletion.getProcent());
+        return new ComputeDailyCompletionResponse(true, dto);
+    }
+
+    public DailyCompletionsResponse getDailyCompletions(Long userId, int page, int size) {
+        if (!disciplineUserRepository.existsById(userId)) {
+            return new DailyCompletionsResponse(List.of(), false);
+        }
+
+        Page<DailyCompletion> result = dailyCompletionRepository
+                .findByUser_UserIdOrderByDateDescDailyCompletionIdDesc(userId, PageRequest.of(page, size));
+
+        List<DailyCompletionDto> items = result.getContent().stream()
+                .map(dc -> new DailyCompletionDto(dc.getDailyCompletionId(), dc.getDate(), dc.getProcent()))
+                .toList();
+
+        return new DailyCompletionsResponse(items, result.hasNext());
+    }
+}
+
Index: frontend/src/App.jsx
===================================================================
--- frontend/src/App.jsx	(revision 8449b9cc520ba82bb2945db031d4b9973a038e59)
+++ frontend/src/App.jsx	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
@@ -19,4 +19,5 @@
 import NewInvestment from "./pages/Dashboard/pages/Investing/NewInvestment.jsx";
 import Discipline from "./pages/Dashboard/pages/Discipline/Discipline.jsx";
+import DisciplineTracking from "./pages/Dashboard/pages/Discipline/DisciplineTracking.jsx";
 
 function RequireAuth({ children }) {
@@ -59,4 +60,5 @@
           <Route path="investing/assets/new" element={<NewInvestment />} />
           <Route path="discipline" element={<Discipline />} />
+           <Route path="discipline/tracking" element={<DisciplineTracking />} />
         </Route>
         <Route path="*" element={<Navigate to="/" replace />} />
Index: frontend/src/api/dailyCompletion.js
===================================================================
--- frontend/src/api/dailyCompletion.js	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ frontend/src/api/dailyCompletion.js	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
@@ -0,0 +1,16 @@
+import api from "./axios";
+
+export async function computeDailyCompletion(date /* yyyy-mm-dd or undefined */) {
+  const res = await api.post("/discipline/daily-completions/compute", null, {
+    params: date ? { date } : {},
+  });
+  return res?.data;
+}
+
+export async function getDailyCompletions({ page = 0, size = 14 } = {}) {
+  const res = await api.get("/discipline/daily-completions", {
+    params: { page, size },
+  });
+  return res?.data;
+}
+
Index: frontend/src/api/discipline.js
===================================================================
--- frontend/src/api/discipline.js	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ frontend/src/api/discipline.js	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
@@ -0,0 +1,37 @@
+import api from "./axios";
+
+export async function getDisciplineStatus() {
+  const res = await api.get("/discipline/status");
+  return Boolean(res?.data?.tracking);
+}
+
+export async function startDisciplineTracking() {
+  return api.post("/discipline/start", {});
+}
+
+export async function getTasks({ page = 0, size = 50 } = {}) {
+  const res = await api.get("/discipline/tasks", { params: { page, size } });
+  return res?.data;
+}
+
+export async function createTask(payload) {
+  const res = await api.post("/discipline/tasks", payload);
+  return res?.data;
+}
+
+export async function updateTask(taskId, payload) {
+  const res = await api.put(`/discipline/tasks/${taskId}`, payload);
+  return res?.data;
+}
+
+export async function updateTaskFinished(taskId, isFinished) {
+  const res = await api.patch(`/discipline/tasks/${taskId}/finished`, {
+    isFinished,
+  });
+  return res?.data;
+}
+
+export async function deleteTask(taskId) {
+  return api.delete(`/discipline/tasks/${taskId}`);
+}
+
Index: frontend/src/pages/Dashboard/pages/Discipline/Discipline.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Discipline/Discipline.jsx	(revision 8449b9cc520ba82bb2945db031d4b9973a038e59)
+++ frontend/src/pages/Dashboard/pages/Discipline/Discipline.jsx	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
@@ -1,16 +1,85 @@
-import React from "react";
+import React, { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
 
-const Discipline = () => {
+import {
+  getDisciplineStatus,
+  startDisciplineTracking,
+} from "../../../../api/discipline";
+
+import DisciplineCenteredCard from "./components/DisciplineCenteredCard.jsx";
+import DisciplineStartCtaCard from "./components/DisciplineStartCtaCard.jsx";
+
+export default function Discipline() {
+  const navigate = useNavigate();
+
+  const [isLoading, setIsLoading] = useState(true);
+  const [isTracking, setIsTracking] = useState(false);
+  const [error, setError] = useState("");
+  const [isSubmitting, setIsSubmitting] = useState(false);
+
+  useEffect(() => {
+    let isMounted = true;
+    const run = async () => {
+      setIsLoading(true);
+      setError("");
+      try {
+        const tracking = await getDisciplineStatus();
+        if (!isMounted) return;
+        setIsTracking(tracking);
+        if (tracking) {
+          navigate("/dashboard/discipline/tracking", { replace: true });
+        }
+      } catch (err) {
+        if (!isMounted) return;
+        const message =
+          err?.response?.data?.message || "Failed to load discipline status";
+        setError(message);
+      } finally {
+        if (isMounted) setIsLoading(false);
+      }
+    };
+
+    run();
+    return () => {
+      isMounted = false;
+    };
+  }, [navigate]);
+
+  const onStart = async () => {
+    setError("");
+    setIsSubmitting(true);
+    try {
+      await startDisciplineTracking();
+      setIsTracking(true);
+      navigate("/dashboard/discipline/tracking", { replace: true });
+    } catch (err) {
+      const message =
+        err?.response?.data?.message ||
+        Object.values(err?.response?.data?.errors ?? {})[0] ||
+        "Failed to start tracking";
+      setError(message);
+    } finally {
+      setIsSubmitting(false);
+    }
+  };
+
+  if (isLoading) {
+    return <DisciplineCenteredCard title="Discipline" message="Loading…" />;
+  }
+
+  if (isTracking) {
+    return <DisciplineCenteredCard title="Discipline" message="Redirecting…" />;
+  }
+
   return (
-    <div className="space-y-4">
-      <h1 className="text-2xl font-bold">Discipline</h1>
-      <div className="card bg-base-200 border border-base-300">
-        <div className="card-body">
-          <p className="opacity-80">Discipline dashboard content goes here.</p>
-        </div>
+    <div className="min-h-[70vh] w-full flex items-center justify-center">
+      <div className="w-full max-w-3xl">
+        <DisciplineStartCtaCard
+          error={error}
+          onStart={onStart}
+          isSubmitting={isSubmitting}
+        />
       </div>
     </div>
   );
-};
-
-export default Discipline;
+}
Index: frontend/src/pages/Dashboard/pages/Discipline/DisciplineTracking.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Discipline/DisciplineTracking.jsx	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ frontend/src/pages/Dashboard/pages/Discipline/DisciplineTracking.jsx	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
@@ -0,0 +1,397 @@
+import React, { useCallback, useEffect, useMemo, useState } from "react";
+import graphPlaceholder from "../../../../assets/graph-placeholder.svg";
+
+import {
+  createTask,
+  deleteTask,
+  getTasks,
+  updateTask,
+  updateTaskFinished,
+} from "../../../../api/discipline";
+
+import {
+  computeDailyCompletion,
+  getDailyCompletions,
+} from "../../../../api/dailyCompletion";
+
+import TaskList from "./components/TaskList.jsx";
+
+export default function DisciplineTracking() {
+  const pageSize = 50;
+  const completionPageSize = 14;
+
+  const todayIso = useMemo(() => {
+    const d = new Date();
+    return d.toISOString().slice(0, 10);
+  }, []);
+
+  const [tasks, setTasks] = useState([]);
+  const [page, setPage] = useState(0);
+  const [hasMore, setHasMore] = useState(false);
+  const [isLoading, setIsLoading] = useState(true);
+  const [isLoadingMore, setIsLoadingMore] = useState(false);
+  const [error, setError] = useState("");
+  const [completionInfo, setCompletionInfo] = useState(null);
+  const [isComputing, setIsComputing] = useState(false);
+  const [workingTaskIds, setWorkingTaskIds] = useState([]);
+
+  const [dailyCompletions, setDailyCompletions] = useState([]);
+  const [completionPage, setCompletionPage] = useState(0);
+  const [completionHasMore, setCompletionHasMore] = useState(false);
+  const [isLoadingCompletions, setIsLoadingCompletions] = useState(true);
+  const [isLoadingMoreCompletions, setIsLoadingMoreCompletions] = useState(false);
+  const [completionError, setCompletionError] = useState("");
+
+  const workingSet = useMemo(
+    () => new Set(workingTaskIds),
+    [workingTaskIds],
+  );
+
+  const fetchPage = useCallback(async (nextPage) => {
+    const data = await getTasks({ page: nextPage, size: pageSize });
+    return {
+      nextTasks: Array.isArray(data?.tasks) ? data.tasks : [],
+      nextHasMore: Boolean(data?.hasMore),
+    };
+  }, []);
+
+  const fetchCompletionsPage = useCallback(async (nextPage) => {
+    const data = await getDailyCompletions({
+      page: nextPage,
+      size: completionPageSize,
+    });
+    return {
+      nextCompletions: Array.isArray(data?.completions) ? data.completions : [],
+      nextHasMore: Boolean(data?.hasMore),
+    };
+  }, []);
+
+  useEffect(() => {
+    let cancelled = false;
+    (async () => {
+      try {
+        setIsLoading(true);
+        setError("");
+        const { nextTasks, nextHasMore } = await fetchPage(0);
+        if (cancelled) return;
+        setTasks(nextTasks);
+        setHasMore(nextHasMore);
+        setPage(0);
+      } catch (e) {
+        if (cancelled) return;
+        setError(e?.response?.data?.message || "Failed to load tasks.");
+      } finally {
+        if (!cancelled) setIsLoading(false);
+      }
+    })();
+    return () => {
+      cancelled = true;
+    };
+  }, [fetchPage]);
+
+  useEffect(() => {
+    let cancelled = false;
+    (async () => {
+      try {
+        setIsLoadingCompletions(true);
+        setCompletionError("");
+        const { nextCompletions, nextHasMore } = await fetchCompletionsPage(0);
+        if (cancelled) return;
+        setDailyCompletions(nextCompletions);
+        setCompletionHasMore(nextHasMore);
+        setCompletionPage(0);
+      } catch (e) {
+        if (cancelled) return;
+        setCompletionError(
+          e?.response?.data?.message ||
+            "Failed to load daily completion history.",
+        );
+      } finally {
+        if (!cancelled) setIsLoadingCompletions(false);
+      }
+    })();
+
+    return () => {
+      cancelled = true;
+    };
+  }, [fetchCompletionsPage]);
+
+  const onLoadMoreCompletions = async () => {
+    if (isLoadingMoreCompletions || !completionHasMore) return;
+    const nextPage = completionPage + 1;
+    try {
+      setIsLoadingMoreCompletions(true);
+      setCompletionError("");
+      const { nextCompletions, nextHasMore } =
+        await fetchCompletionsPage(nextPage);
+      setDailyCompletions((prev) => [...prev, ...nextCompletions]);
+      setCompletionHasMore(nextHasMore);
+      setCompletionPage(nextPage);
+    } catch (e) {
+      setCompletionError(
+        e?.response?.data?.message || "Failed to load more completions.",
+      );
+    } finally {
+      setIsLoadingMoreCompletions(false);
+    }
+  };
+
+  const onLoadMore = async () => {
+    if (isLoadingMore || !hasMore) return;
+    const nextPage = page + 1;
+    try {
+      setIsLoadingMore(true);
+      setError("");
+      const { nextTasks, nextHasMore } = await fetchPage(nextPage);
+      setTasks((prev) => [...prev, ...nextTasks]);
+      setHasMore(nextHasMore);
+      setPage(nextPage);
+    } catch (e) {
+      setError(e?.response?.data?.message || "Failed to load more tasks.");
+    } finally {
+      setIsLoadingMore(false);
+    }
+  };
+
+  const withWorking = async (taskId, fn) => {
+    setWorkingTaskIds((prev) => [...prev, taskId]);
+    try {
+      await fn();
+    } finally {
+      setWorkingTaskIds((prev) => prev.filter((id) => id !== taskId));
+    }
+  };
+
+  const onAdd = async (name) => {
+    setError("");
+    try {
+      const created = await createTask({ name });
+      setTasks((prev) => [created, ...prev]);
+    } catch (e) {
+      setError(e?.response?.data?.message || "Failed to create task.");
+    }
+  };
+
+  const onDelete = async (taskId) => {
+    setError("");
+    await withWorking(taskId, async () => {
+      try {
+        await deleteTask(taskId);
+        setTasks((prev) => prev.filter((t) => t.taskId !== taskId));
+      } catch (e) {
+        setError(e?.response?.data?.message || "Failed to delete task.");
+      }
+    });
+  };
+
+  const onToggleFinished = async (taskId, isFinished) => {
+    if (workingSet.has(taskId)) return;
+    setError("");
+
+    // optimistic update
+    setTasks((prev) =>
+      prev.map((t) => (t.taskId === taskId ? { ...t, isFinished } : t)),
+    );
+
+    await withWorking(taskId, async () => {
+      try {
+        const updated = await updateTaskFinished(taskId, isFinished);
+        setTasks((prev) =>
+          prev.map((t) => (t.taskId === taskId ? updated : t)),
+        );
+      } catch (e) {
+        // revert
+        setTasks((prev) =>
+          prev.map((t) =>
+            t.taskId === taskId ? { ...t, isFinished: !isFinished } : t,
+          ),
+        );
+        setError(e?.response?.data?.message || "Failed to update task.");
+      }
+    });
+  };
+
+  const onRename = async (taskId, name) => {
+    setError("");
+    await withWorking(taskId, async () => {
+      try {
+        const updated = await updateTask(taskId, { name });
+        setTasks((prev) =>
+          prev.map((t) => (t.taskId === taskId ? updated : t)),
+        );
+      } catch (e) {
+        setError(e?.response?.data?.message || "Failed to update task.");
+      }
+    });
+  };
+
+  const onComputeToday = async () => {
+    setError("");
+    setCompletionInfo(null);
+    setIsComputing(true);
+    try {
+      const result = await computeDailyCompletion();
+      setCompletionInfo(result?.completion ?? null);
+
+      // refresh daily completion history
+      try {
+        const { nextCompletions, nextHasMore } = await fetchCompletionsPage(0);
+        setDailyCompletions(nextCompletions);
+        setCompletionHasMore(nextHasMore);
+        setCompletionPage(0);
+      } catch {
+        // ignore
+      }
+
+      // refresh tasks as backend will reset finished -> false
+      const { nextTasks, nextHasMore } = await fetchPage(0);
+      setTasks(nextTasks);
+      setHasMore(nextHasMore);
+      setPage(0);
+    } catch (e) {
+      setError(
+        e?.response?.data?.message || "Failed to compute daily completion.",
+      );
+    } finally {
+      setIsComputing(false);
+    }
+  };
+
+  const isTodayClosed = useMemo(() => {
+    return (dailyCompletions ?? []).some((dc) => dc?.date === todayIso);
+  }, [dailyCompletions, todayIso]);
+
+  return (
+    <div className="space-y-6">
+      <div className="flex items-center justify-between gap-3">
+        <h1 className="text-2xl font-bold">Discipline</h1>
+        <button
+          className="btn btn-primary btn-sm"
+          onClick={onComputeToday}
+          disabled={isComputing || isTodayClosed}
+          title="Compute today's completion and reset tasks for the next day"
+        >
+          {isTodayClosed ? "Day closed" : isComputing ? "Computing…" : "Close day"}
+        </button>
+      </div>
+
+      {completionInfo ? (
+        <div className="card bg-base-200 border border-base-300">
+          <div className="card-body">
+            <h2 className="card-title">Daily completion saved</h2>
+            <p className="text-sm opacity-80">
+              Date: {completionInfo.date} — Completion: {completionInfo.procent}%
+            </p>
+          </div>
+        </div>
+      ) : null}
+
+      {isLoading ? (
+        <div className="card bg-base-200 border border-base-300">
+          <div className="card-body">
+            <p className="opacity-80">Loading tasks…</p>
+          </div>
+        </div>
+      ) : (
+        <>
+          <TaskList
+            tasks={tasks}
+            onAdd={onAdd}
+            onDelete={onDelete}
+            onToggleFinished={onToggleFinished}
+            onRename={onRename}
+            isWorkingTaskIds={workingTaskIds}
+          />
+
+          {error ? <p className="text-error text-sm">{error}</p> : null}
+
+          {hasMore ? (
+            <button
+              className="btn btn-outline btn-sm"
+              disabled={isLoadingMore}
+              onClick={onLoadMore}
+            >
+              {isLoadingMore ? "Loading…" : "Load more"}
+            </button>
+          ) : null}
+
+          <div className="card bg-base-200 border border-base-300">
+            <div className="card-body">
+              <h2 className="card-title">Progress</h2>
+              <p className="text-sm opacity-80">(Placeholder graph for now)</p>
+              <div className="mt-4">
+                <img
+                  src={graphPlaceholder}
+                  alt="Discipline graph placeholder"
+                  className="w-full"
+                />
+              </div>
+            </div>
+          </div>
+
+          <div className="card bg-base-200 border border-base-300">
+            <div className="card-body">
+              <h2 className="card-title">Daily completion</h2>
+              <p className="text-sm opacity-80">
+                Previous days completion percentages.
+              </p>
+
+              {completionError ? (
+                <p className="text-error text-sm mt-2">{completionError}</p>
+              ) : null}
+
+              <div className="mt-4 overflow-x-auto">
+                <table className="table table-zebra">
+                  <thead>
+                    <tr>
+                      <th>Date</th>
+                      <th className="text-right">Completion</th>
+                    </tr>
+                  </thead>
+                  <tbody>
+                    {isLoadingCompletions ? (
+                      <tr>
+                        <td colSpan={2} className="opacity-70">
+                          Loading…
+                        </td>
+                      </tr>
+                    ) : (dailyCompletions ?? []).length === 0 ? (
+                      <tr>
+                        <td colSpan={2} className="opacity-70">
+                          No daily completion records yet. Click “Close day” to
+                          save today.
+                        </td>
+                      </tr>
+                    ) : (
+                      (dailyCompletions ?? []).map((dc) => (
+                        <tr key={dc.dailyCompletionId}>
+                          <td>{dc.date ?? "—"}</td>
+                          <td className="text-right tabular-nums">
+                            {dc.procent ?? 0}%
+                          </td>
+                        </tr>
+                      ))
+                    )}
+                  </tbody>
+                </table>
+              </div>
+
+              <div className="mt-4">
+                {completionHasMore ? (
+                  <button
+                    className="btn btn-outline btn-sm"
+                    disabled={isLoadingMoreCompletions}
+                    onClick={onLoadMoreCompletions}
+                  >
+                    {isLoadingMoreCompletions ? "Loading…" : "Load more"}
+                  </button>
+                ) : null}
+              </div>
+            </div>
+          </div>
+        </>
+      )}
+    </div>
+  );
+}
+
Index: frontend/src/pages/Dashboard/pages/Discipline/components/DisciplineCenteredCard.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Discipline/components/DisciplineCenteredCard.jsx	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ frontend/src/pages/Dashboard/pages/Discipline/components/DisciplineCenteredCard.jsx	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
@@ -0,0 +1,17 @@
+import React from "react";
+
+export default function DisciplineCenteredCard({ title, message }) {
+  return (
+    <div className="min-h-[70vh] w-full flex items-center justify-center">
+      <div className="w-full max-w-3xl">
+        <div className="card bg-base-200 border border-base-300">
+          <div className="card-body">
+            <h1 className="card-title text-2xl">{title}</h1>
+            <p className="opacity-80">{message}</p>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
+
Index: frontend/src/pages/Dashboard/pages/Discipline/components/DisciplineStartCtaCard.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Discipline/components/DisciplineStartCtaCard.jsx	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ frontend/src/pages/Dashboard/pages/Discipline/components/DisciplineStartCtaCard.jsx	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
@@ -0,0 +1,26 @@
+import React from "react";
+
+export default function DisciplineStartCtaCard({ error, onStart, isSubmitting }) {
+  return (
+    <div className="card bg-base-200 border border-base-300">
+      <div className="card-body">
+        <h1 className="card-title text-2xl">Discipline</h1>
+        <p className="opacity-80">
+          Start tracking your daily discipline tasks. You can add, edit, and mark
+          tasks as completed.
+        </p>
+        {error ? <p className="text-error text-sm mt-2">{error}</p> : null}
+        <div className="mt-4">
+          <button
+            className="btn btn-primary"
+            disabled={isSubmitting}
+            onClick={onStart}
+          >
+            {isSubmitting ? "Starting…" : "Start tracking"}
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+}
+
Index: frontend/src/pages/Dashboard/pages/Discipline/components/TaskList.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Discipline/components/TaskList.jsx	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
+++ frontend/src/pages/Dashboard/pages/Discipline/components/TaskList.jsx	(revision 4f85b41e4eaa6d668912791fb3cff6efbe7dc1a4)
@@ -0,0 +1,171 @@
+import React, { useMemo, useState } from "react";
+
+function normalizeName(value) {
+  return String(value ?? "").trim();
+}
+
+export default function TaskList({
+  tasks,
+  onAdd,
+  onDelete,
+  onToggleFinished,
+  onRename,
+  isWorkingTaskIds,
+}) {
+  const [newTaskName, setNewTaskName] = useState("");
+  const [editTaskId, setEditTaskId] = useState(null);
+  const [editName, setEditName] = useState("");
+
+  const workingSet = useMemo(
+    () => new Set(isWorkingTaskIds ?? []),
+    [isWorkingTaskIds],
+  );
+
+  const beginEdit = (t) => {
+    setEditTaskId(t.taskId);
+    setEditName(t.name ?? "");
+  };
+
+  const cancelEdit = () => {
+    setEditTaskId(null);
+    setEditName("");
+  };
+
+  const submitRename = async () => {
+    const name = normalizeName(editName);
+    if (!name) return;
+    await onRename(editTaskId, name);
+    cancelEdit();
+  };
+
+  return (
+    <div className="card bg-base-200 border border-base-300">
+      <div className="card-body">
+        <div className="flex items-center justify-between gap-3 flex-wrap">
+          <div>
+            <h2 className="card-title">Daily tasks</h2>
+            <p className="text-sm opacity-80">
+              Check a task to mark it finished. Changes are saved immediately.
+            </p>
+          </div>
+        </div>
+
+        <form
+          className="mt-4 flex gap-2"
+          onSubmit={(e) => {
+            e.preventDefault();
+            const name = normalizeName(newTaskName);
+            if (!name) return;
+            onAdd(name);
+            setNewTaskName("");
+          }}
+        >
+          <input
+            className="input input-bordered w-full"
+            placeholder="New task name…"
+            value={newTaskName}
+            onChange={(e) => setNewTaskName(e.target.value)}
+            maxLength={200}
+          />
+          <button className="btn btn-primary" type="submit">
+            Add
+          </button>
+        </form>
+
+        <div className="mt-4 space-y-2">
+          {(tasks ?? []).length === 0 ? (
+            <p className="opacity-70">No tasks yet.</p>
+          ) : (
+            (tasks ?? []).map((t) => {
+              const isWorking = workingSet.has(t.taskId);
+              const isEditing = editTaskId === t.taskId;
+
+              return (
+                <div
+                  key={t.taskId}
+                  className="flex items-center justify-between gap-3 rounded-lg border border-base-300 bg-base-100 px-3 py-2"
+                >
+                  <div className="flex items-center gap-3 w-full">
+                    <input
+                      type="checkbox"
+                      className="checkbox checkbox-success"
+                      checked={Boolean(t.isFinished)}
+                      disabled={isWorking}
+                      onChange={(e) =>
+                        onToggleFinished(t.taskId, e.target.checked)
+                      }
+                    />
+
+                    {isEditing ? (
+                      <input
+                        className="input input-bordered input-sm w-full"
+                        value={editName}
+                        onChange={(e) => setEditName(e.target.value)}
+                        maxLength={200}
+                      />
+                    ) : (
+                      <div className="w-full">
+                        <div
+                          className={
+                            t.isFinished
+                              ? "line-through opacity-70"
+                              : "font-medium"
+                          }
+                        >
+                          {t.name}
+                        </div>
+                      </div>
+                    )}
+                  </div>
+
+                  <div className="flex items-center gap-2">
+                    {isEditing ? (
+                      <>
+                        <button
+                          className="btn btn-primary btn-xs"
+                          type="button"
+                          disabled={isWorking}
+                          onClick={submitRename}
+                        >
+                          Save
+                        </button>
+                        <button
+                          className="btn btn-ghost btn-xs"
+                          type="button"
+                          disabled={isWorking}
+                          onClick={cancelEdit}
+                        >
+                          Cancel
+                        </button>
+                      </>
+                    ) : (
+                      <>
+                        <button
+                          className="btn btn-outline btn-xs"
+                          type="button"
+                          disabled={isWorking}
+                          onClick={() => beginEdit(t)}
+                        >
+                          Edit
+                        </button>
+                        <button
+                          className="btn btn-error btn-xs"
+                          type="button"
+                          disabled={isWorking}
+                          onClick={() => onDelete(t.taskId)}
+                        >
+                          Delete
+                        </button>
+                      </>
+                    )}
+                  </div>
+                </div>
+              );
+            })
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}
+
