Index: backend/pom.xml
===================================================================
--- backend/pom.xml	(revision ff01f66118570f9849d473fe7df4fe26b5e3a361)
+++ backend/pom.xml	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -51,4 +51,9 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-webmvc</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
         </dependency>
         <dependency>
Index: backend/src/main/java/com/trekr/backend/config/SecurityConfig.java
===================================================================
--- backend/src/main/java/com/trekr/backend/config/SecurityConfig.java	(revision ff01f66118570f9849d473fe7df4fe26b5e3a361)
+++ backend/src/main/java/com/trekr/backend/config/SecurityConfig.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -1,4 +1,5 @@
 package com.trekr.backend.config;
 
+import com.trekr.backend.security.JwtAuthenticationFilter;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -7,6 +8,8 @@
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 import org.springframework.web.cors.CorsConfiguration;
 import org.springframework.web.cors.CorsConfigurationSource;
@@ -19,4 +22,10 @@
 public class SecurityConfig {
 
+    private final JwtAuthenticationFilter jwtAuthenticationFilter;
+
+    public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
+        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
+    }
+
     @Bean
     public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
@@ -24,7 +33,12 @@
                 .csrf(AbstractHttpConfigurer::disable)
                 .cors(Customizer.withDefaults())
+                .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+                .httpBasic(AbstractHttpConfigurer::disable)
+                .formLogin(AbstractHttpConfigurer::disable)
                 .authorizeHttpRequests(auth -> auth
                         .requestMatchers("/api/auth/**").permitAll()
                         .anyRequest().authenticated());
+
+        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
         return http.build();
     }
Index: backend/src/main/java/com/trekr/backend/controller/InvestingController.java
===================================================================
--- backend/src/main/java/com/trekr/backend/controller/InvestingController.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ backend/src/main/java/com/trekr/backend/controller/InvestingController.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,74 @@
+package com.trekr.backend.controller;
+
+import com.trekr.backend.dto.invest.AssetDto;
+import com.trekr.backend.dto.invest.CreateAssetRequest;
+import com.trekr.backend.dto.invest.InvestingAssetsResponse;
+import com.trekr.backend.dto.invest.TrackingStatusResponse;
+import com.trekr.backend.security.UserPrincipal;
+import com.trekr.backend.service.InvestingService;
+import jakarta.validation.Valid;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.core.Authentication;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/investing")
+public class InvestingController {
+
+    private final InvestingService investingService;
+
+    public InvestingController(InvestingService investingService) {
+        this.investingService = investingService;
+    }
+
+    @GetMapping("/status")
+    public TrackingStatusResponse status(Authentication authentication) {
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        boolean tracking = investingService.isTracking(principal.getUserId());
+        return new TrackingStatusResponse(tracking);
+    }
+
+    @PostMapping("/start")
+    @ResponseStatus(HttpStatus.CREATED)
+    public TrackingStatusResponse start(Authentication authentication) {
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        investingService.startTracking(principal.getUserId());
+        return new TrackingStatusResponse(true);
+    }
+
+    @GetMapping("/assets")
+    public InvestingAssetsResponse assets(
+            @RequestParam(defaultValue = "0") int page,
+            @RequestParam(defaultValue = "5") int size,
+            Authentication authentication) {
+        if (size < 1)
+            size = 5;
+        if (size > 50)
+            size = 50;
+        if (page < 0)
+            page = 0;
+
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        return investingService.getAssets(principal.getUserId(), page, size);
+    }
+
+    @PostMapping("/assets")
+    @ResponseStatus(HttpStatus.CREATED)
+    public AssetDto createAsset(
+            @Valid @RequestBody CreateAssetRequest request,
+            Authentication authentication) {
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        return investingService.createAsset(principal.getUserId(), request);
+    }
+
+    @DeleteMapping("/assets/{assetId}")
+    @ResponseStatus(HttpStatus.NO_CONTENT)
+    public void deleteAsset(
+            @PathVariable Long assetId,
+            Authentication authentication) {
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        investingService.deleteAsset(principal.getUserId(), assetId);
+    }
+}
Index: backend/src/main/java/com/trekr/backend/controller/TrainingController.java
===================================================================
--- backend/src/main/java/com/trekr/backend/controller/TrainingController.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ backend/src/main/java/com/trekr/backend/controller/TrainingController.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,80 @@
+package com.trekr.backend.controller;
+
+import com.trekr.backend.dto.training.CreateTrainingSessionRequest;
+import com.trekr.backend.dto.training.TrainingSessionDto;
+import com.trekr.backend.dto.training.TrainingStartRequest;
+import com.trekr.backend.dto.training.TrainingSessionsResponse;
+import com.trekr.backend.dto.training.TrainingProfileResponse;
+import com.trekr.backend.dto.training.TrackingStatusResponse;
+import com.trekr.backend.dto.training.WorkoutTypeDto;
+import com.trekr.backend.security.UserPrincipal;
+import com.trekr.backend.service.TrainingService;
+import jakarta.validation.Valid;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.core.Authentication;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/training")
+public class TrainingController {
+
+    private final TrainingService trainingService;
+
+    public TrainingController(TrainingService trainingService) {
+        this.trainingService = trainingService;
+    }
+
+    @GetMapping("/status")
+    public TrackingStatusResponse status(Authentication authentication) {
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        boolean tracking = trainingService.isTracking(principal.getUserId());
+        return new TrackingStatusResponse(tracking);
+    }
+
+    @PostMapping("/start")
+    @ResponseStatus(HttpStatus.CREATED)
+    public TrackingStatusResponse start(@Valid @RequestBody TrainingStartRequest request,
+            Authentication authentication) {
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        trainingService.startTracking(principal.getUserId(), request);
+        return new TrackingStatusResponse(true);
+    }
+
+    @GetMapping("/sessions")
+    public TrainingSessionsResponse sessions(
+            @RequestParam(defaultValue = "0") int page,
+            @RequestParam(defaultValue = "5") int size,
+            Authentication authentication) {
+        if (size < 1)
+            size = 5;
+        if (size > 50)
+            size = 50;
+        if (page < 0)
+            page = 0;
+
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        return trainingService.getSessions(principal.getUserId(), page, size);
+    }
+
+    @GetMapping("/workout-types")
+    public List<WorkoutTypeDto> workoutTypes() {
+        return trainingService.getWorkoutTypes();
+    }
+
+    @GetMapping("/profile")
+    public TrainingProfileResponse profile(Authentication authentication) {
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        return trainingService.getProfile(principal.getUserId());
+    }
+
+    @PostMapping("/sessions")
+    @ResponseStatus(HttpStatus.CREATED)
+    public TrainingSessionDto createSession(
+            @Valid @RequestBody CreateTrainingSessionRequest request,
+            Authentication authentication) {
+        UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+        return trainingService.createSession(principal.getUserId(), request);
+    }
+}
Index: backend/src/main/java/com/trekr/backend/dto/invest/AssetDto.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/invest/AssetDto.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ backend/src/main/java/com/trekr/backend/dto/invest/AssetDto.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,64 @@
+package com.trekr.backend.dto.invest;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+public class AssetDto {
+
+    private Long assetId;
+    private String tickerSymbol;
+    private BigDecimal buyPrice;
+    private LocalDate buyDate;
+    private BigDecimal quantity;
+
+    public AssetDto() {
+    }
+
+    public AssetDto(Long assetId, String tickerSymbol, BigDecimal buyPrice, LocalDate buyDate, BigDecimal quantity) {
+        this.assetId = assetId;
+        this.tickerSymbol = tickerSymbol;
+        this.buyPrice = buyPrice;
+        this.buyDate = buyDate;
+        this.quantity = quantity;
+    }
+
+    public Long getAssetId() {
+        return assetId;
+    }
+
+    public void setAssetId(Long assetId) {
+        this.assetId = assetId;
+    }
+
+    public String getTickerSymbol() {
+        return tickerSymbol;
+    }
+
+    public void setTickerSymbol(String tickerSymbol) {
+        this.tickerSymbol = tickerSymbol;
+    }
+
+    public BigDecimal getBuyPrice() {
+        return buyPrice;
+    }
+
+    public void setBuyPrice(BigDecimal buyPrice) {
+        this.buyPrice = buyPrice;
+    }
+
+    public LocalDate getBuyDate() {
+        return buyDate;
+    }
+
+    public void setBuyDate(LocalDate buyDate) {
+        this.buyDate = buyDate;
+    }
+
+    public BigDecimal getQuantity() {
+        return quantity;
+    }
+
+    public void setQuantity(BigDecimal quantity) {
+        this.quantity = quantity;
+    }
+}
Index: backend/src/main/java/com/trekr/backend/dto/invest/CreateAssetRequest.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/invest/CreateAssetRequest.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ backend/src/main/java/com/trekr/backend/dto/invest/CreateAssetRequest.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,60 @@
+package com.trekr.backend.dto.invest;
+
+import jakarta.validation.constraints.DecimalMin;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.PastOrPresent;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+public class CreateAssetRequest {
+
+    @NotBlank
+    private String tickerSymbol;
+
+    @NotNull
+    @DecimalMin(value = "0", inclusive = false)
+    private BigDecimal quantity;
+
+    @DecimalMin(value = "0", inclusive = true)
+    private BigDecimal buyPrice;
+
+    @PastOrPresent
+    private LocalDate buyDate;
+
+    public CreateAssetRequest() {
+    }
+
+    public String getTickerSymbol() {
+        return tickerSymbol;
+    }
+
+    public void setTickerSymbol(String tickerSymbol) {
+        this.tickerSymbol = tickerSymbol;
+    }
+
+    public BigDecimal getQuantity() {
+        return quantity;
+    }
+
+    public void setQuantity(BigDecimal quantity) {
+        this.quantity = quantity;
+    }
+
+    public BigDecimal getBuyPrice() {
+        return buyPrice;
+    }
+
+    public void setBuyPrice(BigDecimal buyPrice) {
+        this.buyPrice = buyPrice;
+    }
+
+    public LocalDate getBuyDate() {
+        return buyDate;
+    }
+
+    public void setBuyDate(LocalDate buyDate) {
+        this.buyDate = buyDate;
+    }
+}
Index: backend/src/main/java/com/trekr/backend/dto/invest/InvestingAssetsResponse.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/invest/InvestingAssetsResponse.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ backend/src/main/java/com/trekr/backend/dto/invest/InvestingAssetsResponse.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,33 @@
+package com.trekr.backend.dto.invest;
+
+import java.util.List;
+
+public class InvestingAssetsResponse {
+
+    private List<AssetDto> assets;
+    private boolean hasMore;
+
+    public InvestingAssetsResponse() {
+    }
+
+    public InvestingAssetsResponse(List<AssetDto> assets, boolean hasMore) {
+        this.assets = assets;
+        this.hasMore = hasMore;
+    }
+
+    public List<AssetDto> getAssets() {
+        return assets;
+    }
+
+    public void setAssets(List<AssetDto> assets) {
+        this.assets = assets;
+    }
+
+    public boolean isHasMore() {
+        return hasMore;
+    }
+
+    public void setHasMore(boolean hasMore) {
+        this.hasMore = hasMore;
+    }
+}
Index: backend/src/main/java/com/trekr/backend/dto/invest/TrackingStatusResponse.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/invest/TrackingStatusResponse.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ backend/src/main/java/com/trekr/backend/dto/invest/TrackingStatusResponse.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,21 @@
+package com.trekr.backend.dto.invest;
+
+public class TrackingStatusResponse {
+
+    private boolean tracking;
+
+    public TrackingStatusResponse() {
+    }
+
+    public TrackingStatusResponse(boolean tracking) {
+        this.tracking = tracking;
+    }
+
+    public boolean isTracking() {
+        return tracking;
+    }
+
+    public void setTracking(boolean tracking) {
+        this.tracking = tracking;
+    }
+}
Index: backend/src/main/java/com/trekr/backend/dto/training/CreateTrainingSessionRequest.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/training/CreateTrainingSessionRequest.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ backend/src/main/java/com/trekr/backend/dto/training/CreateTrainingSessionRequest.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,58 @@
+package com.trekr.backend.dto.training;
+
+import jakarta.validation.constraints.DecimalMin;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+
+import java.math.BigDecimal;
+
+public class CreateTrainingSessionRequest {
+
+    @NotBlank
+    private String type;
+
+    @NotNull
+    @DecimalMin(value = "1", inclusive = true)
+    private BigDecimal durationMinutes;
+
+    @NotNull
+    private Boolean autoCalculateCalories;
+
+    @DecimalMin(value = "0", inclusive = true)
+    private BigDecimal calories;
+
+    public CreateTrainingSessionRequest() {
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public BigDecimal getDurationMinutes() {
+        return durationMinutes;
+    }
+
+    public void setDurationMinutes(BigDecimal durationMinutes) {
+        this.durationMinutes = durationMinutes;
+    }
+
+    public Boolean getAutoCalculateCalories() {
+        return autoCalculateCalories;
+    }
+
+    public void setAutoCalculateCalories(Boolean autoCalculateCalories) {
+        this.autoCalculateCalories = autoCalculateCalories;
+    }
+
+    public BigDecimal getCalories() {
+        return calories;
+    }
+
+    public void setCalories(BigDecimal calories) {
+        this.calories = calories;
+    }
+}
Index: backend/src/main/java/com/trekr/backend/dto/training/TrackingStatusResponse.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/training/TrackingStatusResponse.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ backend/src/main/java/com/trekr/backend/dto/training/TrackingStatusResponse.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,21 @@
+package com.trekr.backend.dto.training;
+
+public class TrackingStatusResponse {
+
+    private boolean tracking;
+
+    public TrackingStatusResponse() {
+    }
+
+    public TrackingStatusResponse(boolean tracking) {
+        this.tracking = tracking;
+    }
+
+    public boolean isTracking() {
+        return tracking;
+    }
+
+    public void setTracking(boolean tracking) {
+        this.tracking = tracking;
+    }
+}
Index: backend/src/main/java/com/trekr/backend/dto/training/TrainingProfileResponse.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/training/TrainingProfileResponse.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ backend/src/main/java/com/trekr/backend/dto/training/TrainingProfileResponse.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,43 @@
+package com.trekr.backend.dto.training;
+
+import java.math.BigDecimal;
+
+public class TrainingProfileResponse {
+
+    private String gender;
+    private Integer age;
+    private BigDecimal weight;
+
+    public TrainingProfileResponse() {
+    }
+
+    public TrainingProfileResponse(String gender, Integer age, BigDecimal weight) {
+        this.gender = gender;
+        this.age = age;
+        this.weight = weight;
+    }
+
+    public String getGender() {
+        return gender;
+    }
+
+    public void setGender(String gender) {
+        this.gender = gender;
+    }
+
+    public Integer getAge() {
+        return age;
+    }
+
+    public void setAge(Integer age) {
+        this.age = age;
+    }
+
+    public BigDecimal getWeight() {
+        return weight;
+    }
+
+    public void setWeight(BigDecimal weight) {
+        this.weight = weight;
+    }
+}
Index: backend/src/main/java/com/trekr/backend/dto/training/TrainingSessionDto.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/training/TrainingSessionDto.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ backend/src/main/java/com/trekr/backend/dto/training/TrainingSessionDto.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,64 @@
+package com.trekr.backend.dto.training;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+public class TrainingSessionDto {
+
+    private Long trainingId;
+    private LocalDate date;
+    private String type;
+    private BigDecimal duration;
+    private BigDecimal calories;
+
+    public TrainingSessionDto() {
+    }
+
+    public TrainingSessionDto(Long trainingId, LocalDate date, String type, BigDecimal duration, BigDecimal calories) {
+        this.trainingId = trainingId;
+        this.date = date;
+        this.type = type;
+        this.duration = duration;
+        this.calories = calories;
+    }
+
+    public Long getTrainingId() {
+        return trainingId;
+    }
+
+    public void setTrainingId(Long trainingId) {
+        this.trainingId = trainingId;
+    }
+
+    public LocalDate getDate() {
+        return date;
+    }
+
+    public void setDate(LocalDate date) {
+        this.date = date;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public BigDecimal getDuration() {
+        return duration;
+    }
+
+    public void setDuration(BigDecimal duration) {
+        this.duration = duration;
+    }
+
+    public BigDecimal getCalories() {
+        return calories;
+    }
+
+    public void setCalories(BigDecimal calories) {
+        this.calories = calories;
+    }
+}
Index: backend/src/main/java/com/trekr/backend/dto/training/TrainingSessionsResponse.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/training/TrainingSessionsResponse.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ backend/src/main/java/com/trekr/backend/dto/training/TrainingSessionsResponse.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,33 @@
+package com.trekr.backend.dto.training;
+
+import java.util.List;
+
+public class TrainingSessionsResponse {
+
+    private List<TrainingSessionDto> sessions;
+    private boolean hasMore;
+
+    public TrainingSessionsResponse() {
+    }
+
+    public TrainingSessionsResponse(List<TrainingSessionDto> sessions, boolean hasMore) {
+        this.sessions = sessions;
+        this.hasMore = hasMore;
+    }
+
+    public List<TrainingSessionDto> getSessions() {
+        return sessions;
+    }
+
+    public void setSessions(List<TrainingSessionDto> sessions) {
+        this.sessions = sessions;
+    }
+
+    public boolean isHasMore() {
+        return hasMore;
+    }
+
+    public void setHasMore(boolean hasMore) {
+        this.hasMore = hasMore;
+    }
+}
Index: backend/src/main/java/com/trekr/backend/dto/training/TrainingStartRequest.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/training/TrainingStartRequest.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ backend/src/main/java/com/trekr/backend/dto/training/TrainingStartRequest.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,50 @@
+package com.trekr.backend.dto.training;
+
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+
+import java.math.BigDecimal;
+
+public class TrainingStartRequest {
+
+    @NotBlank(message = "Gender is required")
+    private String gender;
+
+    @NotNull(message = "Age is required")
+    @Min(value = 1, message = "Age must be greater than 0")
+    @Max(value = 120, message = "Age must be realistic")
+    private Integer age;
+
+    @NotNull(message = "Weight is required")
+    @Min(value = 1, message = "Weight must be greater than 0")
+    private BigDecimal weight;
+
+    public TrainingStartRequest() {
+    }
+
+    public String getGender() {
+        return gender;
+    }
+
+    public void setGender(String gender) {
+        this.gender = gender;
+    }
+
+    public Integer getAge() {
+        return age;
+    }
+
+    public void setAge(Integer age) {
+        this.age = age;
+    }
+
+    public BigDecimal getWeight() {
+        return weight;
+    }
+
+    public void setWeight(BigDecimal weight) {
+        this.weight = weight;
+    }
+}
Index: backend/src/main/java/com/trekr/backend/dto/training/WorkoutTypeDto.java
===================================================================
--- backend/src/main/java/com/trekr/backend/dto/training/WorkoutTypeDto.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ backend/src/main/java/com/trekr/backend/dto/training/WorkoutTypeDto.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,43 @@
+package com.trekr.backend.dto.training;
+
+import java.math.BigDecimal;
+
+public class WorkoutTypeDto {
+
+    private String type;
+    private String label;
+    private BigDecimal met;
+
+    public WorkoutTypeDto() {
+    }
+
+    public WorkoutTypeDto(String type, String label, BigDecimal met) {
+        this.type = type;
+        this.label = label;
+        this.met = met;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    public void setLabel(String label) {
+        this.label = label;
+    }
+
+    public BigDecimal getMet() {
+        return met;
+    }
+
+    public void setMet(BigDecimal met) {
+        this.met = met;
+    }
+}
Index: backend/src/main/java/com/trekr/backend/repository/AssetRepository.java
===================================================================
--- backend/src/main/java/com/trekr/backend/repository/AssetRepository.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ backend/src/main/java/com/trekr/backend/repository/AssetRepository.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,14 @@
+package com.trekr.backend.repository;
+
+import com.trekr.backend.entity.invest.Asset;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface AssetRepository extends JpaRepository<Asset, Long> {
+    Page<Asset> findByInvestorUser_UserIdOrderByAssetIdDesc(Long userId, Pageable pageable);
+
+    int deleteByAssetIdAndInvestorUser_UserId(Long assetId, Long userId);
+}
Index: backend/src/main/java/com/trekr/backend/repository/InvestorUserRepository.java
===================================================================
--- backend/src/main/java/com/trekr/backend/repository/InvestorUserRepository.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ backend/src/main/java/com/trekr/backend/repository/InvestorUserRepository.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,9 @@
+package com.trekr.backend.repository;
+
+import com.trekr.backend.entity.invest.InvestorUser;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface InvestorUserRepository extends JpaRepository<InvestorUser, Long> {
+}
Index: backend/src/main/java/com/trekr/backend/repository/TrainingSessionRepository.java
===================================================================
--- backend/src/main/java/com/trekr/backend/repository/TrainingSessionRepository.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ backend/src/main/java/com/trekr/backend/repository/TrainingSessionRepository.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,12 @@
+package com.trekr.backend.repository;
+
+import com.trekr.backend.entity.training.TrainingSession;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface TrainingSessionRepository extends JpaRepository<TrainingSession, Long> {
+    Page<TrainingSession> findByTrainingUser_UserIdOrderByDateDesc(Long userId, Pageable pageable);
+}
Index: backend/src/main/java/com/trekr/backend/repository/TrainingUserRepository.java
===================================================================
--- backend/src/main/java/com/trekr/backend/repository/TrainingUserRepository.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ backend/src/main/java/com/trekr/backend/repository/TrainingUserRepository.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,9 @@
+package com.trekr.backend.repository;
+
+import com.trekr.backend.entity.training.TrainingUser;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface TrainingUserRepository extends JpaRepository<TrainingUser, Long> {
+}
Index: backend/src/main/java/com/trekr/backend/security/JwtAuthenticationFilter.java
===================================================================
--- backend/src/main/java/com/trekr/backend/security/JwtAuthenticationFilter.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ backend/src/main/java/com/trekr/backend/security/JwtAuthenticationFilter.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,54 @@
+package com.trekr.backend.security;
+
+import com.trekr.backend.service.JwtService;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.util.List;
+
+@Component
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+    private final JwtService jwtService;
+
+    public JwtAuthenticationFilter(JwtService jwtService) {
+        this.jwtService = jwtService;
+    }
+
+    @Override
+    protected void doFilterInternal(
+            HttpServletRequest request,
+            HttpServletResponse response,
+            FilterChain filterChain) throws ServletException, IOException {
+
+        String authHeader = request.getHeader("Authorization");
+        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        String token = authHeader.substring("Bearer ".length()).trim();
+        if (token.isEmpty() || !jwtService.isTokenValid(token)) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        if (SecurityContextHolder.getContext().getAuthentication() == null) {
+            UserPrincipal principal = jwtService.parseUserPrincipal(token);
+            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(principal,
+                    null, List.of());
+            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+            SecurityContextHolder.getContext().setAuthentication(authentication);
+        }
+
+        filterChain.doFilter(request, response);
+    }
+}
Index: backend/src/main/java/com/trekr/backend/security/UserPrincipal.java
===================================================================
--- backend/src/main/java/com/trekr/backend/security/UserPrincipal.java	(revision ff01f66118570f9849d473fe7df4fe26b5e3a361)
+++ backend/src/main/java/com/trekr/backend/security/UserPrincipal.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -1,5 +1,3 @@
 package com.trekr.backend.security;
-
-import lombok.Getter;
 
 import java.io.Serializable;
@@ -9,5 +7,4 @@
  * Controllers can inject this or get it from SecurityContextHolder.
  */
-@Getter
 public class UserPrincipal implements Serializable {
     private static final long serialVersionUID = 1L;
@@ -22,3 +19,15 @@
         this.email = email;
     }
+
+    public Long getUserId() {
+        return userId;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public String getEmail() {
+        return email;
+    }
 }
Index: backend/src/main/java/com/trekr/backend/service/InvestingService.java
===================================================================
--- backend/src/main/java/com/trekr/backend/service/InvestingService.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ backend/src/main/java/com/trekr/backend/service/InvestingService.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,135 @@
+package com.trekr.backend.service;
+
+import com.trekr.backend.dto.invest.AssetDto;
+import com.trekr.backend.dto.invest.CreateAssetRequest;
+import com.trekr.backend.dto.invest.InvestingAssetsResponse;
+import com.trekr.backend.entity.User;
+import com.trekr.backend.entity.invest.Asset;
+import com.trekr.backend.entity.invest.InvestorUser;
+import com.trekr.backend.repository.AssetRepository;
+import com.trekr.backend.repository.InvestorUserRepository;
+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.time.LocalDate;
+import java.util.List;
+import java.util.Locale;
+
+@Service
+public class InvestingService {
+
+    private final InvestorUserRepository investorUserRepository;
+    private final AssetRepository assetRepository;
+    private final UserRepository userRepository;
+
+    public InvestingService(
+            InvestorUserRepository investorUserRepository,
+            AssetRepository assetRepository,
+            UserRepository userRepository) {
+        this.investorUserRepository = investorUserRepository;
+        this.assetRepository = assetRepository;
+        this.userRepository = userRepository;
+    }
+
+    public boolean isTracking(Long userId) {
+        return investorUserRepository.existsById(userId);
+    }
+
+    @Transactional
+    public void startTracking(Long userId) {
+        if (investorUserRepository.existsById(userId)) {
+            return;
+        }
+
+        User user = userRepository.findById(userId)
+                .orElseThrow(() -> new RuntimeException("User not found"));
+
+        InvestorUser investorUser = new InvestorUser();
+        investorUser.setUser(user);
+        investorUserRepository.save(investorUser);
+    }
+
+    public InvestingAssetsResponse getAssets(Long userId, int page, int size) {
+        if (!investorUserRepository.existsById(userId)) {
+            return new InvestingAssetsResponse(List.of(), false);
+        }
+
+        Page<Asset> result = assetRepository
+                .findByInvestorUser_UserIdOrderByAssetIdDesc(userId, PageRequest.of(page, size));
+
+        List<AssetDto> assets = result.getContent().stream()
+                .map(a -> new AssetDto(
+                        a.getAssetId(),
+                        a.getTickerSymbol(),
+                        a.getBuyPrice(),
+                        a.getBuyDate(),
+                        a.getQuantity()))
+                .toList();
+
+        return new InvestingAssetsResponse(assets, result.hasNext());
+    }
+
+    @Transactional
+    public AssetDto createAsset(Long userId, CreateAssetRequest request) {
+        InvestorUser investorUser = investorUserRepository.findById(userId)
+                .orElseThrow(() -> new RuntimeException("Investing tracking is not enabled for this user"));
+
+        String ticker = normalizeTicker(request.getTickerSymbol());
+        if (ticker == null || ticker.isBlank()) {
+            throw new RuntimeException("Ticker symbol is required");
+        }
+
+        if (ticker.length() > 30) {
+            throw new RuntimeException("Ticker symbol is too long");
+        }
+
+        // Allow equities (AAPL), crypto pairs (BTC/USD), indices (^GSPC), FX pairs
+        // (EURUSD=X), etc.
+        if (!ticker.matches("^[A-Z0-9.^=\\-/]{1,30}$")) {
+            throw new RuntimeException("Ticker symbol contains invalid characters");
+        }
+
+        LocalDate buyDate = request.getBuyDate();
+        if (buyDate != null && buyDate.isAfter(LocalDate.now())) {
+            throw new RuntimeException("Buy date cannot be in the future");
+        }
+
+        Asset asset = new Asset();
+        asset.setInvestorUser(investorUser);
+        asset.setTickerSymbol(ticker);
+        asset.setQuantity(request.getQuantity());
+        asset.setBuyPrice(request.getBuyPrice());
+        asset.setBuyDate(buyDate);
+
+        Asset saved = assetRepository.save(asset);
+
+        return new AssetDto(
+                saved.getAssetId(),
+                saved.getTickerSymbol(),
+                saved.getBuyPrice(),
+                saved.getBuyDate(),
+                saved.getQuantity());
+    }
+
+    @Transactional
+    public void deleteAsset(Long userId, Long assetId) {
+        if (assetId == null) {
+            throw new RuntimeException("Asset not found");
+        }
+
+        int deleted = assetRepository.deleteByAssetIdAndInvestorUser_UserId(assetId, userId);
+        if (deleted == 0) {
+            throw new RuntimeException("Asset not found");
+        }
+    }
+
+    private static String normalizeTicker(String tickerSymbol) {
+        if (tickerSymbol == null) {
+            return null;
+        }
+        return tickerSymbol.trim().toUpperCase(Locale.ROOT);
+    }
+}
Index: backend/src/main/java/com/trekr/backend/service/JwtService.java
===================================================================
--- backend/src/main/java/com/trekr/backend/service/JwtService.java	(revision ff01f66118570f9849d473fe7df4fe26b5e3a361)
+++ backend/src/main/java/com/trekr/backend/service/JwtService.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -1,5 +1,8 @@
 package com.trekr.backend.service;
 
+import com.trekr.backend.security.UserPrincipal;
 import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.JwtException;
+import io.jsonwebtoken.Claims;
 import io.jsonwebtoken.security.Keys;
 import org.springframework.beans.factory.annotation.Value;
@@ -34,3 +37,28 @@
                 .compact();
     }
+
+    public boolean isTokenValid(String token) {
+        try {
+            parseClaims(token);
+            return true;
+        } catch (JwtException | IllegalArgumentException ex) {
+            return false;
+        }
+    }
+
+    public UserPrincipal parseUserPrincipal(String token) {
+        Claims claims = parseClaims(token);
+        Long userId = Long.valueOf(claims.getSubject());
+        String username = (String) claims.get("username");
+        String email = (String) claims.get("email");
+        return new UserPrincipal(userId, username, email);
+    }
+
+    private Claims parseClaims(String token) {
+        return Jwts.parser()
+                .verifyWith(signingKey)
+                .build()
+                .parseSignedClaims(token)
+                .getPayload();
+    }
 }
Index: backend/src/main/java/com/trekr/backend/service/TrainingService.java
===================================================================
--- backend/src/main/java/com/trekr/backend/service/TrainingService.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ backend/src/main/java/com/trekr/backend/service/TrainingService.java	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,222 @@
+package com.trekr.backend.service;
+
+import com.trekr.backend.dto.training.CreateTrainingSessionRequest;
+import com.trekr.backend.dto.training.TrainingSessionDto;
+import com.trekr.backend.dto.training.TrainingStartRequest;
+import com.trekr.backend.dto.training.TrainingSessionsResponse;
+import com.trekr.backend.dto.training.TrainingProfileResponse;
+import com.trekr.backend.dto.training.WorkoutTypeDto;
+import com.trekr.backend.entity.User;
+import com.trekr.backend.entity.training.TrainingSession;
+import com.trekr.backend.entity.training.TrainingUser;
+import com.trekr.backend.repository.TrainingSessionRepository;
+import com.trekr.backend.repository.TrainingUserRepository;
+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;
+import java.util.Locale;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Service
+public class TrainingService {
+
+    private static final Map<String, BigDecimal> MET_BY_TYPE = Map.ofEntries(
+            Map.entry("running", new BigDecimal("9.8")),
+            Map.entry("cycling", new BigDecimal("7.5")),
+            Map.entry("walking", new BigDecimal("3.5")),
+            Map.entry("strength", new BigDecimal("6.0")),
+            Map.entry("hiit", new BigDecimal("9.0")),
+            Map.entry("yoga", new BigDecimal("2.5")),
+            Map.entry("swimming", new BigDecimal("8.0")),
+            Map.entry("rowing", new BigDecimal("7.0")),
+            Map.entry("elliptical", new BigDecimal("5.0")),
+
+            // Added types (same calories formula; MET is the only extra input)
+            Map.entry("hiking", new BigDecimal("6.0")),
+            Map.entry("stair-climbing", new BigDecimal("8.8")),
+            Map.entry("jump-rope", new BigDecimal("12.3")),
+            Map.entry("boxing", new BigDecimal("7.8")),
+            Map.entry("basketball", new BigDecimal("8.0")),
+            Map.entry("soccer", new BigDecimal("10.0")),
+            Map.entry("tennis", new BigDecimal("7.3")),
+            Map.entry("dance", new BigDecimal("5.5")),
+            Map.entry("pilates", new BigDecimal("3.0")),
+            Map.entry("stretching", new BigDecimal("2.3")));
+
+    private static final List<String> WORKOUT_TYPE_ORDER = List.of(
+            "running",
+            "cycling",
+            "walking",
+            "strength",
+            "hiit",
+            "yoga",
+            "swimming",
+            "rowing",
+            "elliptical",
+            "hiking",
+            "stair-climbing",
+            "jump-rope",
+            "boxing",
+            "basketball",
+            "soccer",
+            "tennis",
+            "dance",
+            "pilates",
+            "stretching");
+
+    private final TrainingUserRepository trainingUserRepository;
+    private final TrainingSessionRepository trainingSessionRepository;
+    private final UserRepository userRepository;
+
+    public TrainingService(
+            TrainingUserRepository trainingUserRepository,
+            TrainingSessionRepository trainingSessionRepository,
+            UserRepository userRepository) {
+        this.trainingUserRepository = trainingUserRepository;
+        this.trainingSessionRepository = trainingSessionRepository;
+        this.userRepository = userRepository;
+    }
+
+    public boolean isTracking(Long userId) {
+        return trainingUserRepository.existsById(userId);
+    }
+
+    @Transactional
+    public void startTracking(Long userId, TrainingStartRequest request) {
+        if (trainingUserRepository.existsById(userId)) {
+            return;
+        }
+
+        User user = userRepository.findById(userId)
+                .orElseThrow(() -> new RuntimeException("User not found"));
+
+        TrainingUser trainingUser = new TrainingUser();
+        trainingUser.setUser(user);
+        trainingUser.setGender(request.getGender());
+        trainingUser.setAge(request.getAge());
+        trainingUser.setWeight(request.getWeight());
+
+        trainingUserRepository.save(trainingUser);
+    }
+
+    public TrainingSessionsResponse getSessions(Long userId, int page, int size) {
+        if (!trainingUserRepository.existsById(userId)) {
+            return new TrainingSessionsResponse(List.of(), false);
+        }
+
+        Page<TrainingSession> result = trainingSessionRepository
+                .findByTrainingUser_UserIdOrderByDateDesc(userId, PageRequest.of(page, size));
+
+        List<TrainingSessionDto> sessions = result.getContent().stream()
+                .map(s -> new TrainingSessionDto(
+                        s.getTrainingId(),
+                        s.getDate(),
+                        s.getType(),
+                        s.getDuration(),
+                        s.getCalories()))
+                .toList();
+
+        return new TrainingSessionsResponse(sessions, result.hasNext());
+    }
+
+    public List<WorkoutTypeDto> getWorkoutTypes() {
+        return WORKOUT_TYPE_ORDER.stream()
+                .map(type -> new WorkoutTypeDto(type, labelForType(type), MET_BY_TYPE.get(type)))
+                .toList();
+    }
+
+    public TrainingProfileResponse getProfile(Long userId) {
+        TrainingUser trainingUser = trainingUserRepository.findById(userId)
+                .orElseThrow(() -> new RuntimeException("Training tracking is not enabled for this user"));
+        return new TrainingProfileResponse(trainingUser.getGender(), trainingUser.getAge(), trainingUser.getWeight());
+    }
+
+    @Transactional
+    public TrainingSessionDto createSession(Long userId, CreateTrainingSessionRequest request) {
+        TrainingUser trainingUser = trainingUserRepository.findById(userId)
+                .orElseThrow(() -> new RuntimeException("Training tracking is not enabled for this user"));
+
+        String type = normalizeType(request.getType());
+        BigDecimal met = MET_BY_TYPE.get(type);
+        if (met == null) {
+            throw new RuntimeException("Unsupported workout type");
+        }
+
+        BigDecimal durationMinutes = request.getDurationMinutes();
+        if (durationMinutes == null || durationMinutes.compareTo(BigDecimal.ONE) < 0) {
+            throw new RuntimeException("Duration must be at least 1 minute");
+        }
+
+        BigDecimal calories;
+        if (Boolean.TRUE.equals(request.getAutoCalculateCalories())) {
+            calories = calculateCalories(trainingUser, met, durationMinutes);
+        } else {
+            calories = request.getCalories();
+            if (calories == null) {
+                throw new RuntimeException("Calories are required when auto-calculation is disabled");
+            }
+            if (calories.compareTo(BigDecimal.ZERO) < 0) {
+                throw new RuntimeException("Calories must be 0 or greater");
+            }
+        }
+
+        TrainingSession session = new TrainingSession();
+        session.setTrainingUser(trainingUser);
+        session.setType(type);
+        session.setDuration(durationMinutes);
+        session.setCalories(calories);
+        session.setDate(LocalDate.now());
+
+        TrainingSession saved = trainingSessionRepository.save(session);
+
+        return new TrainingSessionDto(
+                saved.getTrainingId(),
+                saved.getDate(),
+                saved.getType(),
+                saved.getDuration(),
+                saved.getCalories());
+    }
+
+    private static String normalizeType(String type) {
+        if (type == null) {
+            return null;
+        }
+        return type.trim().toLowerCase(Locale.ROOT);
+    }
+
+    private static String labelForType(String type) {
+        if (type == null || type.isBlank()) {
+            return "";
+        }
+        if ("hiit".equals(type)) {
+            return "HIIT";
+        }
+
+        String normalized = type.replace('_', '-');
+        return List.of(normalized.split("-"))
+                .stream()
+                .filter(part -> part != null && !part.isBlank())
+                .map(part -> Character.toUpperCase(part.charAt(0)) + part.substring(1))
+                .collect(Collectors.joining(" "));
+    }
+
+    private static BigDecimal calculateCalories(TrainingUser trainingUser, BigDecimal met, BigDecimal durationMinutes) {
+        BigDecimal weightKg = trainingUser.getWeight();
+        if (weightKg == null) {
+            throw new RuntimeException("Weight is required to auto-calculate calories");
+        }
+
+        BigDecimal durationHours = durationMinutes.divide(new BigDecimal("60"), 10, RoundingMode.HALF_UP);
+        return met.multiply(weightKg)
+                .multiply(durationHours)
+                .setScale(2, RoundingMode.HALF_UP);
+    }
+}
Index: frontend/.gitignore
===================================================================
--- frontend/.gitignore	(revision ff01f66118570f9849d473fe7df4fe26b5e3a361)
+++ frontend/.gitignore	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -12,4 +12,5 @@
 dist-ssr
 *.local
+.env.local
 
 # Editor directories and files
Index: frontend/package-lock.json
===================================================================
--- frontend/package-lock.json	(revision ff01f66118570f9849d473fe7df4fe26b5e3a361)
+++ frontend/package-lock.json	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -16,4 +16,5 @@
         "react": "^19.2.0",
         "react-dom": "^19.2.0",
+        "react-icons": "^5.5.0",
         "react-router-dom": "^7.13.0"
       },
@@ -343,4 +344,5 @@
         "ppc64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -359,4 +361,5 @@
         "arm"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -375,4 +378,5 @@
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -391,4 +395,5 @@
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -407,4 +412,5 @@
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -423,4 +429,5 @@
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -439,4 +446,5 @@
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -455,4 +463,5 @@
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -471,4 +480,5 @@
         "arm"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -487,4 +497,5 @@
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -503,4 +514,5 @@
         "ia32"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -519,4 +531,5 @@
         "loong64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -535,4 +548,5 @@
         "mips64el"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -551,4 +565,5 @@
         "ppc64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -567,4 +582,5 @@
         "riscv64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -583,4 +599,5 @@
         "s390x"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -599,4 +616,5 @@
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -615,4 +633,5 @@
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -631,4 +650,5 @@
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -647,4 +667,5 @@
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -663,4 +684,5 @@
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -679,4 +701,5 @@
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -695,4 +718,5 @@
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -711,4 +735,5 @@
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -727,4 +752,5 @@
         "ia32"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -743,4 +769,5 @@
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -4805,4 +4832,13 @@
       }
     },
+    "node_modules/@relume_io/relume-ui/node_modules/react-icons": {
+      "version": "5.4.0",
+      "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz",
+      "integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": "*"
+      }
+    },
     "node_modules/@relume_io/relume-ui/node_modules/zod": {
       "version": "3.25.76",
@@ -4828,4 +4864,5 @@
         "arm"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -4841,4 +4878,5 @@
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -4854,4 +4892,5 @@
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -4867,4 +4906,5 @@
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -4880,4 +4920,5 @@
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -4893,4 +4934,5 @@
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -4906,4 +4948,5 @@
         "arm"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -4919,4 +4962,5 @@
         "arm"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -4932,4 +4976,5 @@
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -4945,4 +4990,5 @@
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -4958,4 +5004,5 @@
         "loong64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -4971,4 +5018,5 @@
         "loong64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -4984,4 +5032,5 @@
         "ppc64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -4997,4 +5046,5 @@
         "ppc64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -5010,4 +5060,5 @@
         "riscv64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -5023,4 +5074,5 @@
         "riscv64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -5036,4 +5088,5 @@
         "s390x"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -5049,4 +5102,5 @@
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -5062,4 +5116,5 @@
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -5075,4 +5130,5 @@
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -5088,4 +5144,5 @@
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -5101,4 +5158,5 @@
         "arm64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -5114,4 +5172,5 @@
         "ia32"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -5127,4 +5186,5 @@
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -5140,4 +5200,5 @@
         "x64"
       ],
+      "dev": true,
       "license": "MIT",
       "optional": true,
@@ -5500,4 +5561,5 @@
       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
       "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+      "dev": true,
       "license": "MIT"
     },
@@ -5513,5 +5575,5 @@
       "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz",
       "integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==",
-      "devOptional": true,
+      "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -5523,5 +5585,5 @@
       "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
       "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
-      "devOptional": true,
+      "dev": true,
       "license": "MIT",
       "peerDependencies": {
@@ -5594,4 +5656,5 @@
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
       "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -5682,4 +5745,5 @@
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
       "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true,
       "license": "MIT"
     },
@@ -5698,4 +5762,5 @@
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
       "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -5786,4 +5851,5 @@
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
       "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -5823,4 +5889,5 @@
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
       "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -5835,4 +5902,5 @@
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
       "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true,
       "license": "MIT"
     },
@@ -5853,4 +5921,5 @@
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
       "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+      "dev": true,
       "license": "MIT"
     },
@@ -5906,5 +5975,5 @@
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
       "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
-      "devOptional": true,
+      "dev": true,
       "license": "MIT"
     },
@@ -6094,4 +6163,5 @@
       "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
       "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
+      "dev": true,
       "hasInstallScript": true,
       "license": "MIT",
@@ -6363,4 +6433,5 @@
       "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
       "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "dev": true,
       "license": "MIT",
       "engines": {
@@ -6508,4 +6579,5 @@
       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
       "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
       "hasInstallScript": true,
       "license": "MIT",
@@ -6631,4 +6703,5 @@
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
       "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
       "license": "MIT",
       "engines": {
@@ -6765,4 +6838,5 @@
       "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
       "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "dev": true,
       "license": "ISC"
     },
@@ -7212,4 +7286,5 @@
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
       "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
       "license": "ISC",
       "dependencies": {
@@ -7249,4 +7324,5 @@
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
       "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "dev": true,
       "license": "MIT"
     },
@@ -7255,4 +7331,5 @@
       "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
       "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "dev": true,
       "funding": [
         {
@@ -9306,4 +9383,5 @@
       "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
       "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "dev": true,
       "license": "ISC"
     },
@@ -9312,4 +9390,5 @@
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
       "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+      "dev": true,
       "license": "MIT",
       "engines": {
@@ -9324,4 +9403,5 @@
       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
       "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+      "dev": true,
       "funding": [
         {
@@ -9453,7 +9533,7 @@
     },
     "node_modules/react-icons": {
-      "version": "5.4.0",
-      "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz",
-      "integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==",
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
+      "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
       "license": "MIT",
       "peerDependencies": {
@@ -9602,4 +9682,5 @@
       "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
       "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -9652,4 +9733,5 @@
       "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
       "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
       "license": "ISC",
       "bin": {
@@ -9712,4 +9794,5 @@
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
       "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -9762,4 +9845,5 @@
       "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
       "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -9887,4 +9971,5 @@
       "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
       "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -9961,4 +10046,5 @@
       "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
       "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
       "license": "ISC",
       "dependencies": {
Index: frontend/package.json
===================================================================
--- frontend/package.json	(revision ff01f66118570f9849d473fe7df4fe26b5e3a361)
+++ frontend/package.json	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -18,4 +18,5 @@
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
+    "react-icons": "^5.5.0",
     "react-router-dom": "^7.13.0"
   },
Index: frontend/src/App.jsx
===================================================================
--- frontend/src/App.jsx	(revision ff01f66118570f9849d473fe7df4fe26b5e3a361)
+++ frontend/src/App.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -4,5 +4,15 @@
 import Login from "./pages/Login/Login.jsx";
 import Register from "./pages/Register/Register.jsx";
-import Dashboard from "./pages/Dashboard.jsx";
+import DashboardLayout from "./pages/Dashboard/DashboardLayout.jsx";
+import ControlCenter from "./pages/Dashboard/pages/ControlCenter/ControlCenter.jsx";
+import Training from "./pages/Dashboard/pages/Training/Training.jsx";
+import TrainingTracking from "./pages/Dashboard/pages/Training/TrainingTracking.jsx";
+import NewTrainingSession from "./pages/Dashboard/pages/Training/NewTrainingSession.jsx";
+import Weight from "./pages/Dashboard/pages/Weight/Weight.jsx";
+import Finance from "./pages/Dashboard/pages/Finance/Finance.jsx";
+import Investing from "./pages/Dashboard/pages/Investing/Investing.jsx";
+import InvestingTracking from "./pages/Dashboard/pages/Investing/InvestingTracking.jsx";
+import NewInvestment from "./pages/Dashboard/pages/Investing/NewInvestment.jsx";
+import Discipline from "./pages/Dashboard/pages/Discipline/Discipline.jsx";
 
 function RequireAuth({ children }) {
@@ -23,8 +33,23 @@
           element={
             <RequireAuth>
-              <Dashboard />
+              <DashboardLayout />
             </RequireAuth>
           }
-        />
+        >
+          <Route index element={<Navigate to="control-center" replace />} />
+          <Route path="control-center" element={<ControlCenter />} />
+          <Route path="training" element={<Training />} />
+          <Route path="training/tracking" element={<TrainingTracking />} />
+          <Route
+            path="training/sessions/new"
+            element={<NewTrainingSession />}
+          />
+          <Route path="weight" element={<Weight />} />
+          <Route path="finance" element={<Finance />} />
+          <Route path="investing" element={<Investing />} />
+          <Route path="investing/tracking" element={<InvestingTracking />} />
+          <Route path="investing/assets/new" element={<NewInvestment />} />
+          <Route path="discipline" element={<Discipline />} />
+        </Route>
         <Route path="*" element={<Navigate to="/" replace />} />
       </Routes>
Index: frontend/src/api/twelveData.js
===================================================================
--- frontend/src/api/twelveData.js	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/api/twelveData.js	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,284 @@
+const BASE_URL =
+  import.meta.env.VITE_TWELVE_DATA_BASE_URL ?? "https://api.twelvedata.com";
+
+const API_KEY = import.meta.env.VITE_TWELVE_DATA_API_KEY ?? "";
+
+const CACHE_TTL_MS = 5 * 60_000;
+const MAX_SYMBOLS_PER_REQUEST = 20;
+
+// Cache by Twelve Data symbol (normalized)
+const priceCache = new Map();
+const inFlight = new Map();
+
+function getCachedPrice(twelveSymbol) {
+  const item = priceCache.get(twelveSymbol);
+  if (!item) return null;
+  if (Date.now() - item.ts > CACHE_TTL_MS) return null;
+  return item.price;
+}
+
+function setCachedPrice(twelveSymbol, price) {
+  if (!twelveSymbol || typeof price !== "number" || !Number.isFinite(price)) return;
+  priceCache.set(twelveSymbol, { price, ts: Date.now() });
+}
+
+function parsePriceJson(json, requestedSymbols) {
+  const out = {};
+  if (!json || typeof json !== "object" || Array.isArray(json)) return out;
+
+  // Single symbol response
+  if (json.price !== undefined && requestedSymbols.length === 1) {
+    const n = Number(json.price);
+    if (Number.isFinite(n)) out[requestedSymbols[0]] = n;
+    return out;
+  }
+
+  // Multi symbol response: object keyed by symbol
+  for (const sym of requestedSymbols) {
+    const n = Number(json?.[sym]?.price);
+    if (Number.isFinite(n)) out[sym] = n;
+  }
+
+  return out;
+}
+
+async function fetchBatchPrices(twelveSymbols) {
+  const params = new URLSearchParams();
+  params.set("symbol", twelveSymbols.join(","));
+  params.set("apikey", API_KEY);
+
+  const url = `${BASE_URL}/price?${params.toString()}`;
+  const resp = await fetch(url, { method: "GET" });
+
+  if (!resp.ok) {
+    const text = await resp.text().catch(() => "");
+    throw new Error(`Twelve Data request failed (HTTP ${resp.status}) ${text}`);
+  }
+
+  const json = await resp.json();
+  if (json?.status === "error") {
+    throw new Error(String(json?.message ?? "Twelve Data error"));
+  }
+
+  return parsePriceJson(json, twelveSymbols);
+}
+
+async function fetchSinglePrice(twelveSymbol) {
+  const cached = getCachedPrice(twelveSymbol);
+  if (typeof cached === "number") return cached;
+
+  if (inFlight.has(twelveSymbol)) return inFlight.get(twelveSymbol);
+
+  const p = (async () => {
+    const params = new URLSearchParams();
+    params.set("symbol", twelveSymbol);
+    params.set("apikey", API_KEY);
+    const url = `${BASE_URL}/price?${params.toString()}`;
+    const resp = await fetch(url, { method: "GET" });
+    if (!resp.ok) {
+      const text = await resp.text().catch(() => "");
+      throw new Error(`Twelve Data request failed (HTTP ${resp.status}) ${text}`);
+    }
+    const json = await resp.json();
+    if (json?.status === "error") {
+      throw new Error(String(json?.message ?? "Twelve Data error"));
+    }
+    const n = Number(json?.price);
+    if (!Number.isFinite(n)) throw new Error("Twelve Data returned no price");
+    setCachedPrice(twelveSymbol, n);
+    return n;
+  })();
+
+  inFlight.set(twelveSymbol, p);
+  try {
+    return await p;
+  } finally {
+    inFlight.delete(twelveSymbol);
+  }
+}
+
+async function fetchSingleExchangeRate(pairSymbol) {
+  const cached = getCachedPrice(pairSymbol);
+  if (typeof cached === "number") return cached;
+
+  if (inFlight.has(pairSymbol)) return inFlight.get(pairSymbol);
+
+  const p = (async () => {
+    const params = new URLSearchParams();
+    params.set("symbol", pairSymbol);
+    params.set("apikey", API_KEY);
+    const url = `${BASE_URL}/exchange_rate?${params.toString()}`;
+    const resp = await fetch(url, { method: "GET" });
+    if (!resp.ok) {
+      const text = await resp.text().catch(() => "");
+      throw new Error(`Twelve Data request failed (HTTP ${resp.status}) ${text}`);
+    }
+    const json = await resp.json();
+    if (json?.status === "error") {
+      throw new Error(String(json?.message ?? "Twelve Data error"));
+    }
+
+    const n = Number(json?.rate);
+    if (!Number.isFinite(n)) throw new Error("Twelve Data returned no rate");
+    setCachedPrice(pairSymbol, n);
+    return n;
+  })();
+
+  inFlight.set(pairSymbol, p);
+  try {
+    return await p;
+  } finally {
+    inFlight.delete(pairSymbol);
+  }
+}
+
+export function toTwelveSymbol(symbol) {
+  if (!symbol) return "";
+  const s = String(symbol).trim().toUpperCase();
+  if (!s) return "";
+
+  // Already Twelve format (common for FX/crypto)
+  if (s.includes("/")) return s;
+
+  // Common Yahoo-style crypto tickers
+  const cryptoMap = {
+    "BTC-USD": "BTC/USD",
+    "ETH-USD": "ETH/USD",
+    "SOL-USD": "SOL/USD",
+    "BNB-USD": "BNB/USD",
+    "XRP-USD": "XRP/USD",
+    "ADA-USD": "ADA/USD",
+    "DOGE-USD": "DOGE/USD",
+    "AVAX-USD": "AVAX/USD",
+    "DOT-USD": "DOT/USD",
+    "LINK-USD": "LINK/USD",
+    "MATIC-USD": "MATIC/USD",
+    "LTC-USD": "LTC/USD",
+  };
+
+  if (cryptoMap[s]) return cryptoMap[s];
+
+  // Default: assume it's a stock/ETF symbol (AAPL, SPY, BRK-B, ...)
+  return s;
+}
+
+/**
+ * Fetch current prices for one or more symbols.
+ * Returns an object keyed by *original input symbols* (uppercased), with numeric prices.
+ */
+export async function fetchCurrentPricesBySymbol(symbols) {
+  const input = Array.isArray(symbols) ? symbols : [];
+  const uniqueOriginal = Array.from(
+    new Set(input.map((s) => String(s ?? "").trim()).filter(Boolean)),
+  );
+
+  if (uniqueOriginal.length === 0) return {};
+
+  if (!API_KEY) {
+    throw new Error(
+      "Missing Twelve Data API key (set VITE_TWELVE_DATA_API_KEY in frontend/.env.local)",
+    );
+  }
+
+  // Map original -> twelve
+  const originalToTwelve = new Map();
+  for (const orig of uniqueOriginal) {
+    originalToTwelve.set(orig, toTwelveSymbol(orig));
+  }
+
+  const allTwelveSymbols = Array.from(
+    new Set(Array.from(originalToTwelve.values()).filter(Boolean)),
+  );
+
+  const pairSymbols = allTwelveSymbols.filter((s) => String(s).includes("/"));
+  const priceSymbols = allTwelveSymbols.filter((s) => !String(s).includes("/"));
+
+  // Gather prices with cache + resilient fetch (batch first, fall back to per-symbol)
+  const twelvePrices = {};
+  const missing = [];
+  let hadAnyError = false;
+  let lastErrorMessage = "";
+
+  for (const sym of allTwelveSymbols) {
+    const cached = getCachedPrice(sym);
+    if (typeof cached === "number") {
+      twelvePrices[sym] = cached;
+    } else {
+      missing.push(sym);
+    }
+  }
+
+  // Fetch pair symbols via exchange_rate (generally the most reliable for FX/crypto pairs)
+  for (const sym of pairSymbols) {
+    if (twelvePrices[sym] !== undefined) continue;
+    try {
+      const n = await fetchSingleExchangeRate(sym);
+      if (Number.isFinite(n)) twelvePrices[sym] = n;
+    } catch (e) {
+      hadAnyError = true;
+      lastErrorMessage = e?.message || lastErrorMessage;
+    }
+  }
+
+  // Chunk batch requests to avoid overly long URLs; if a batch fails, we continue
+  // with per-symbol fetches so one invalid symbol doesn't break everything.
+  const missingPrices = missing.filter((s) => priceSymbols.includes(s));
+
+  for (let i = 0; i < missingPrices.length; i += MAX_SYMBOLS_PER_REQUEST) {
+    const chunk = missingPrices.slice(i, i + MAX_SYMBOLS_PER_REQUEST);
+    try {
+      const got = await fetchBatchPrices(chunk);
+      for (const [sym, price] of Object.entries(got)) {
+        const n = Number(price);
+        if (Number.isFinite(n)) {
+          twelvePrices[sym] = n;
+          setCachedPrice(sym, n);
+        }
+      }
+
+      // Some symbols might not come back in a multi-symbol response; fetch them individually.
+      for (const sym of chunk) {
+        if (twelvePrices[sym] !== undefined) continue;
+        try {
+          const n = await fetchSinglePrice(sym);
+          if (Number.isFinite(n)) twelvePrices[sym] = n;
+        } catch (e) {
+          // ignore single-symbol failures
+          hadAnyError = true;
+          lastErrorMessage = e?.message || lastErrorMessage;
+        }
+      }
+    } catch (e) {
+      // Fall back to per-symbol if the whole batch failed.
+      hadAnyError = true;
+      lastErrorMessage = e?.message || lastErrorMessage;
+      await Promise.all(
+        chunk.map(async (sym) => {
+          try {
+            const n = await fetchSinglePrice(sym);
+            if (Number.isFinite(n)) twelvePrices[sym] = n;
+          } catch (e2) {
+            // ignore
+            hadAnyError = true;
+            lastErrorMessage = e2?.message || lastErrorMessage;
+          }
+        }),
+      );
+    }
+  }
+
+  // Convert back to original symbols
+  const out = {};
+  for (const [orig, twelve] of originalToTwelve.entries()) {
+    const p = twelvePrices[twelve];
+    if (typeof p === "number") {
+      out[String(orig).toUpperCase()] = p;
+    }
+  }
+
+  if (Object.keys(out).length === 0 && hadAnyError) {
+    throw new Error(lastErrorMessage || "Could not fetch any current prices");
+  }
+
+  return out;
+}
Index: frontend/src/api/yahooFinance.js
===================================================================
--- frontend/src/api/yahooFinance.js	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/api/yahooFinance.js	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,148 @@
+import { LOCAL_TICKER_SYMBOLS } from "../data/tickers";
+
+const tickerSearchCache = new Map();
+const TICKER_CACHE_TTL_MS = 15 * 60 * 1000;
+
+const quoteCache = new Map();
+const QUOTE_CACHE_TTL_MS = 5 * 60 * 1000;
+let quoteBackoffUntilMs = 0;
+
+async function tryFetchJson(url) {
+  // Note: We intentionally do NOT set custom headers here.
+  // Custom headers usually trigger a CORS preflight which Yahoo won't allow.
+  const resp = await fetch(url, { method: "GET" });
+  if (!resp.ok) {
+    throw new Error(`Yahoo Finance HTTP ${resp.status}`);
+  }
+  return resp.json();
+}
+
+function toTickerOption(raw) {
+  const symbol = raw?.symbol ? String(raw.symbol) : "";
+  if (!symbol) return null;
+
+  const name = raw?.shortname || raw?.longname || raw?.name || "";
+  const exchange = raw?.exchDisp || raw?.exchange || "";
+
+  return {
+    symbol,
+    name: name ? String(name) : "",
+    exchange: exchange ? String(exchange) : "",
+    quoteType: raw?.quoteType ? String(raw.quoteType) : "",
+  };
+}
+
+/**
+ * Direct Yahoo Finance search (yfinance data source).
+ * Falls back to backend proxy if the browser blocks CORS.
+ */
+export async function searchTickers(query, limit = 20) {
+  const q = String(query ?? "").trim();
+  if (!q) return [];
+  const safeLimit = Math.max(1, Math.min(Number(limit) || 20, 25));
+
+  const cacheKey = `${q.toUpperCase()}:${safeLimit}`;
+  const cached = tickerSearchCache.get(cacheKey);
+  const now = Date.now();
+  if (cached && cached.expiresAt > now) {
+    return cached.value;
+  }
+
+  // Offline/local ticker search: avoids Yahoo 429s and keeps the dropdown reliable.
+  const upperQ = q.toUpperCase();
+  const symbols = Array.isArray(LOCAL_TICKER_SYMBOLS) ? LOCAL_TICKER_SYMBOLS : [];
+  const out = symbols
+    .map((s) => String(s ?? "").trim())
+    .filter(Boolean)
+    .filter((s) => s.toUpperCase().includes(upperQ))
+    .slice(0, safeLimit)
+    .map((symbol) => ({ symbol, name: "", exchange: "" }));
+
+  tickerSearchCache.set(cacheKey, {
+    value: out,
+    expiresAt: now + TICKER_CACHE_TTL_MS,
+  });
+  return out;
+}
+
+/**
+ * Direct Yahoo Finance quotes (yfinance data source).
+ * Returns a map { SYMBOL: priceNumber }.
+ * Falls back to backend proxy if the browser blocks CORS.
+ */
+export async function getQuotesBySymbol(symbols) {
+  const list = Array.isArray(symbols) ? symbols : [];
+  const unique = Array.from(
+    new Set(
+      list
+        .map((s) => String(s ?? "").trim())
+        .filter(Boolean)
+        .map((s) => s.toUpperCase()),
+    ),
+  );
+
+  if (unique.length === 0) return {};
+
+  const parseYahoo = (json) => {
+    const results = Array.isArray(json?.quoteResponse?.result)
+      ? json.quoteResponse.result
+      : [];
+    const map = {};
+    for (const r of results) {
+      const sym = r?.symbol ? String(r.symbol).toUpperCase() : "";
+      const price = r?.regularMarketPrice;
+      if (!sym) continue;
+      if (typeof price !== "number") continue;
+      map[sym] = price;
+    }
+    return map;
+  };
+
+  const now = Date.now();
+  const out = {};
+
+  // Serve cache first.
+  const toFetch = [];
+  for (const sym of unique) {
+    const cached = quoteCache.get(sym);
+    if (cached && cached.expiresAt > now && typeof cached.price === "number") {
+      out[sym] = cached.price;
+    } else {
+      toFetch.push(sym);
+    }
+  }
+
+  // Production builds: browser CORS blocks Yahoo, so quotes are unavailable without a proxy.
+  if (!import.meta.env.DEV) {
+    return out;
+  }
+
+  // Backoff after 429s.
+  if (now < quoteBackoffUntilMs) {
+    return out;
+  }
+
+  if (toFetch.length === 0) {
+    return out;
+  }
+
+  const yahooUrl = `/yahoo/v7/finance/quote?symbols=${encodeURIComponent(
+    toFetch.join(","),
+  )}`;
+
+  try {
+    const json = await tryFetchJson(yahooUrl);
+    const fresh = parseYahoo(json);
+    for (const [sym, price] of Object.entries(fresh)) {
+      quoteCache.set(sym, { price, expiresAt: Date.now() + QUOTE_CACHE_TTL_MS });
+      out[sym] = price;
+    }
+    return out;
+  } catch (e) {
+    const msg = String(e?.message ?? "");
+    if (msg.includes("HTTP 429")) {
+      quoteBackoffUntilMs = Date.now() + 60_000;
+    }
+    return out;
+  }
+}
Index: frontend/src/assets/graph-placeholder.svg
===================================================================
--- frontend/src/assets/graph-placeholder.svg	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/assets/graph-placeholder.svg	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,52 @@
+<svg width="1200" height="500" viewBox="0 0 1200 500" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
+      <stop offset="0" stop-color="#0b1f14"/>
+      <stop offset="1" stop-color="#0f2a1c"/>
+    </linearGradient>
+    <linearGradient id="line" x1="0" y1="0" x2="1" y2="0">
+      <stop offset="0" stop-color="#34d399"/>
+      <stop offset="1" stop-color="#22c55e"/>
+    </linearGradient>
+  </defs>
+
+  <rect x="0" y="0" width="1200" height="500" rx="24" fill="url(#bg)"/>
+
+  <!-- grid -->
+  <g opacity="0.15" stroke="#FFFFFF">
+    <path d="M96 80H1120"/>
+    <path d="M96 160H1120"/>
+    <path d="M96 240H1120"/>
+    <path d="M96 320H1120"/>
+    <path d="M96 400H1120"/>
+
+    <path d="M192 64V432"/>
+    <path d="M320 64V432"/>
+    <path d="M448 64V432"/>
+    <path d="M576 64V432"/>
+    <path d="M704 64V432"/>
+    <path d="M832 64V432"/>
+    <path d="M960 64V432"/>
+    <path d="M1088 64V432"/>
+  </g>
+
+  <!-- axes -->
+  <path d="M96 432H1120" stroke="#FFFFFF" opacity="0.35"/>
+  <path d="M96 64V432" stroke="#FFFFFF" opacity="0.35"/>
+
+  <!-- line -->
+  <path d="M96 360 C 220 310, 280 240, 384 260 C 480 280, 560 180, 640 190 C 740 205, 820 120, 928 140 C 1010 155, 1060 120, 1120 110" stroke="url(#line)" stroke-width="6" fill="none"/>
+
+  <!-- points -->
+  <g fill="#34d399">
+    <circle cx="96" cy="360" r="7"/>
+    <circle cx="384" cy="260" r="7"/>
+    <circle cx="640" cy="190" r="7"/>
+    <circle cx="928" cy="140" r="7"/>
+    <circle cx="1120" cy="110" r="7"/>
+  </g>
+
+  <text x="96" y="44" fill="#E5E7EB" font-family="ui-sans-serif, system-ui" font-size="18" opacity="0.9">
+    Graph placeholder
+  </text>
+</svg>
Index: frontend/src/data/tickers.js
===================================================================
--- frontend/src/data/tickers.js	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/data/tickers.js	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,173 @@
+// A pragmatic, hardcoded list to keep ticker selection reliable.
+// Add/remove symbols as needed.
+// Symbols use Yahoo Finance format (e.g., BTC-USD, BRK-B).
+
+export const LOCAL_TICKER_SYMBOLS = [
+  // Crypto (USD pairs)
+  "BTC/USD",
+  "ETH/USD",
+  "SOL/USD",
+  "BNB/USD",
+  "XRP/USD",
+  "ADA/USD",
+  "DOGE/USD",
+  "AVAX/USD",
+  "DOT/USD",
+  "LINK/USD",
+  "MATIC/USD",
+  "LTC/USD",
+
+  // Major ETFs / indices proxies
+  "SPY",
+  "VOO",
+  "IVV",
+  "QQQ",
+  "VTI",
+  "IWM",
+  "DIA",
+  "VEA",
+  "VWO",
+  "EFA",
+  "EEM",
+  "SCHD",
+  "VIG",
+  "VNQ",
+  "TLT",
+  "IEF",
+  "SHY",
+  "HYG",
+  "LQD",
+  "GLD",
+  "SLV",
+  "USO",
+
+  // Mega-cap / large-cap US
+  "AAPL",
+  "MSFT",
+  "NVDA",
+  "AMZN",
+  "GOOGL",
+  "GOOG",
+  "META",
+  "TSLA",
+  "BRK-B",
+  "JPM",
+  "V",
+  "MA",
+  "LLY",
+  "AVGO",
+  "UNH",
+  "XOM",
+  "COST",
+  "HD",
+  "PG",
+  "JNJ",
+  "ORCL",
+  "KO",
+  "PEP",
+  "BAC",
+  "WMT",
+  "ABBV",
+  "MRK",
+  "CVX",
+  "CRM",
+  "ADBE",
+  "NFLX",
+  "AMD",
+  "INTC",
+  "CSCO",
+  "QCOM",
+  "TXN",
+  "IBM",
+  "INTU",
+  "AMAT",
+  "NOW",
+  "ISRG",
+  "GE",
+  "CAT",
+  "BA",
+  "DE",
+  "MMM",
+  "HON",
+  "LOW",
+  "SBUX",
+  "NKE",
+  "MCD",
+  "DIS",
+  "CMCSA",
+  "T",
+  "VZ",
+  "PFE",
+  "TMO",
+  "ABT",
+  "DHR",
+  "LIN",
+  "NEE",
+  "DUK",
+  "SO",
+  "PLD",
+  "AMT",
+  "CCI",
+
+  // Popular growth / tech
+  "SHOP",
+  "SNOW",
+  "PLTR",
+  "UBER",
+  "ABNB",
+  "RBLX",
+  "SQ",
+  "PYPL",
+  "ROKU",
+  "ZM",
+  "DOCU",
+  "CRWD",
+  "PANW",
+  "ZS",
+  "NET",
+  "DDOG",
+  "MDB",
+
+  // Finance
+  "GS",
+  "MS",
+  "C",
+  "WFC",
+  "AXP",
+  "BLK",
+
+  // Energy
+  "COP",
+  "SLB",
+  "OXY",
+
+  // Consumer / retail
+  "TGT",
+  "TJX",
+  "C",
+  "CROX",
+  "LULU",
+  "COST",
+  "WMT",
+  "AMZN",
+
+  // Autos
+  "F",
+  "GM",
+
+  // Semis
+  "ASML",
+  "TSM",
+  "MU",
+  "LRCX",
+  "KLAC",
+
+  // Misc
+  "SPOT",
+  "BABA",
+  "JD",
+  "PDD",
+  "NVO",
+  "TM",
+  "SONY",
+];
Index: frontend/src/pages/Dashboard/DashboardLayout.jsx
===================================================================
--- frontend/src/pages/Dashboard/DashboardLayout.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/DashboardLayout.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,44 @@
+import React, { useMemo } from "react";
+import { Outlet, useNavigate } from "react-router-dom";
+
+import { SidebarInset, SidebarProvider } from "@relume_io/relume-ui";
+
+import DashboardSidebar from "./components/DashboardSidebar.jsx";
+import DashboardTopbar from "./components/DashboardTopbar.jsx";
+
+const DashboardLayout = () => {
+  const navigate = useNavigate();
+
+  const user = useMemo(() => {
+    try {
+      const raw = localStorage.getItem("authUser");
+      return raw ? JSON.parse(raw) : null;
+    } catch {
+      return null;
+    }
+  }, []);
+
+  const username = user?.username ?? "";
+
+  const onLogout = () => {
+    localStorage.removeItem("authToken");
+    localStorage.removeItem("authUser");
+    navigate("/", { replace: true });
+  };
+
+  return (
+    <SidebarProvider>
+      <DashboardSidebar />
+      <SidebarInset className="pt-16 lg:pt-0">
+        <DashboardTopbar username={username} onLogout={onLogout} />
+        <div className="h-[calc(100vh-4rem)] overflow-auto">
+          <div className="container mx-auto px-6 py-8 md:px-8 md:py-10 lg:py-12">
+            <Outlet />
+          </div>
+        </div>
+      </SidebarInset>
+    </SidebarProvider>
+  );
+};
+
+export default DashboardLayout;
Index: frontend/src/pages/Dashboard/components/DashboardSidebar.jsx
===================================================================
--- frontend/src/pages/Dashboard/components/DashboardSidebar.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/components/DashboardSidebar.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,106 @@
+import React from "react";
+import { NavLink, useLocation } from "react-router-dom";
+
+import {
+  Button,
+  Sidebar,
+  SidebarContent,
+  SidebarHeader,
+  SidebarMenu,
+  SidebarMenuButton,
+  SidebarMenuItem,
+} from "@relume_io/relume-ui";
+
+import {
+  MdTune,
+  MdFitnessCenter,
+  MdMonitorWeight,
+  MdAccountBalanceWallet,
+  MdTrendingUp,
+  MdChecklist,
+  MdAdd,
+} from "react-icons/md";
+
+import logo from "../../../assets/logo.png";
+
+const navItems = [
+  { title: "Control Center", to: "/dashboard/control-center", icon: MdTune },
+  { title: "Training", to: "/dashboard/training", icon: MdFitnessCenter },
+  { title: "Weight", to: "/dashboard/weight", icon: MdMonitorWeight },
+  { title: "Finance", to: "/dashboard/finance", icon: MdAccountBalanceWallet },
+  { title: "Investing", to: "/dashboard/investing", icon: MdTrendingUp },
+  { title: "Discipline", to: "/dashboard/discipline", icon: MdChecklist },
+];
+
+const DashboardSidebar = () => {
+  const location = useLocation();
+
+  return (
+    <Sidebar
+      className="py-6"
+      closeButtonClassName="fixed top-4 right-4"
+      collapsible="none"
+    >
+      <SidebarHeader className="hidden lg:block">
+        <NavLink to="/" className="flex items-center gap-3 px-2">
+          <img src={logo} alt="Trekr" className="h-10 w-auto rounded-2xl" />
+        </NavLink>
+      </SidebarHeader>
+
+      <SidebarContent className="mt-6">
+        <SidebarMenu>
+          {navItems.map((item) => (
+            <SidebarMenuItem key={item.title}>
+              {(() => {
+                const isActive =
+                  location.pathname === item.to ||
+                  location.pathname.startsWith(`${item.to}/`);
+
+                return (
+                  <SidebarMenuButton
+                    asChild
+                    isActive={isActive}
+                    className={
+                      isActive
+                        ? "!bg-green-400 !text-black hover:!bg-green-400 active:!bg-green-400"
+                        : ""
+                    }
+                  >
+                    <NavLink
+                      to={item.to}
+                      className="flex w-full items-center gap-3"
+                    >
+                      <item.icon
+                        className={
+                          isActive
+                            ? "size-6 shrink-0 text-black"
+                            : "size-6 shrink-0"
+                        }
+                      />
+                      <span className={isActive ? "text-black" : ""}>
+                        {item.title}
+                      </span>
+                    </NavLink>
+                  </SidebarMenuButton>
+                );
+              })()}
+            </SidebarMenuItem>
+          ))}
+        </SidebarMenu>
+
+        <div className="mt-6 px-2">
+          <Button
+            variant="secondary"
+            className="w-full justify-start gap-2 !bg-green-400 !text-black hover:!bg-green-500"
+            type="button"
+          >
+            <MdAdd className="size-5" />
+            <span>Add Custom</span>
+          </Button>
+        </div>
+      </SidebarContent>
+    </Sidebar>
+  );
+};
+
+export default DashboardSidebar;
Index: frontend/src/pages/Dashboard/components/DashboardTopbar.jsx
===================================================================
--- frontend/src/pages/Dashboard/components/DashboardTopbar.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/components/DashboardTopbar.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,59 @@
+import React from "react";
+import { Link } from "react-router-dom";
+
+import { MdLogout } from "react-icons/md";
+
+import logo from "../../../assets/logo.png";
+
+const DashboardTopbar = ({ username, onLogout }) => {
+  return (
+    <>
+      {/* Desktop */}
+      <div className="sticky top-0 z-30 hidden min-h-16 w-full items-center border-b border-base-300 bg-base-100 px-6 lg:flex lg:px-8">
+        <div className="mx-auto grid size-full grid-cols-1 items-center justify-end justify-items-end gap-4 lg:grid-cols-[1fr_max-content] lg:justify-between lg:justify-items-stretch">
+          <div className="flex items-center gap-3"></div>
+
+          <div className="flex items-center gap-3">
+            <span className="text-sm opacity-80">{username || "—"}</span>
+            <button
+              type="button"
+              className="btn btn-ghost btn-sm"
+              onClick={onLogout}
+              aria-label="Log out"
+              title="Log out"
+            >
+              <MdLogout className="size-5" />
+            </button>
+          </div>
+        </div>
+      </div>
+
+      {/* Mobile */}
+      <div className="fixed left-0 right-0 top-0 z-30 flex min-h-16 w-full items-center justify-between border-b border-base-300 bg-base-100 px-6 lg:hidden">
+        <div className="flex items-center gap-4">
+          <Link
+            to="/dashboard"
+            className="flex items-center gap-3"
+            aria-label="Trekr dashboard"
+          >
+            <img src={logo} alt="Trekr" className="h-10 w-auto rounded-2xl" />
+          </Link>
+        </div>
+        <div className="flex items-center gap-3">
+          <span className="text-sm opacity-80">{username || "—"}</span>
+          <button
+            type="button"
+            className="btn btn-ghost btn-sm"
+            onClick={onLogout}
+            aria-label="Log out"
+            title="Log out"
+          >
+            <MdLogout className="size-5" />
+          </button>
+        </div>
+      </div>
+    </>
+  );
+};
+
+export default DashboardTopbar;
Index: frontend/src/pages/Dashboard/pages/ControlCenter/ControlCenter.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/ControlCenter/ControlCenter.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/ControlCenter/ControlCenter.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,16 @@
+import React from "react";
+
+const ControlCenter = () => {
+  return (
+    <div className="space-y-4">
+      <h1 className="text-2xl font-bold">Control Center</h1>
+      <div className="card bg-base-200 border border-base-300">
+        <div className="card-body">
+          <p className="opacity-80">Control Center content goes here.</p>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default ControlCenter;
Index: frontend/src/pages/Dashboard/pages/Discipline/Discipline.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Discipline/Discipline.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Discipline/Discipline.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,16 @@
+import React from "react";
+
+const Discipline = () => {
+  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>
+    </div>
+  );
+};
+
+export default Discipline;
Index: frontend/src/pages/Dashboard/pages/Finance/Finance.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Finance/Finance.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Finance/Finance.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,16 @@
+import React from "react";
+
+const Finance = () => {
+  return (
+    <div className="space-y-4">
+      <h1 className="text-2xl font-bold">Finance</h1>
+      <div className="card bg-base-200 border border-base-300">
+        <div className="card-body">
+          <p className="opacity-80">Finance dashboard content goes here.</p>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default Finance;
Index: frontend/src/pages/Dashboard/pages/Investing/Investing.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Investing/Investing.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Investing/Investing.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,85 @@
+import React, { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
+
+import api from "../../../../api/axios";
+
+import InvestingCenteredCard from "./components/InvestingCenteredCard.jsx";
+import InvestingStartCtaCard from "./components/InvestingStartCtaCard.jsx";
+
+const Investing = () => {
+  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 res = await api.get("/investing/status");
+        const tracking = Boolean(res?.data?.tracking);
+        if (!isMounted) return;
+        setIsTracking(tracking);
+        if (tracking) {
+          navigate("/dashboard/investing/tracking", { replace: true });
+        }
+      } catch (err) {
+        if (!isMounted) return;
+        const message =
+          err?.response?.data?.message || "Failed to load investing status";
+        setError(message);
+      } finally {
+        if (isMounted) setIsLoading(false);
+      }
+    };
+
+    run();
+    return () => {
+      isMounted = false;
+    };
+  }, [navigate]);
+
+  const onStart = async () => {
+    setError("");
+    setIsSubmitting(true);
+    try {
+      await api.post("/investing/start", {});
+      setIsTracking(true);
+      navigate("/dashboard/investing/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 <InvestingCenteredCard title="Investing" message="Loading…" />;
+  }
+
+  if (isTracking) {
+    return <InvestingCenteredCard title="Investing" message="Redirecting…" />;
+  }
+
+  return (
+    <div className="min-h-[70vh] w-full flex items-center justify-center">
+      <div className="w-full max-w-3xl">
+        <InvestingStartCtaCard
+          error={error}
+          onStart={onStart}
+          isSubmitting={isSubmitting}
+        />
+      </div>
+    </div>
+  );
+};
+
+export default Investing;
Index: frontend/src/pages/Dashboard/pages/Investing/InvestingTracking.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Investing/InvestingTracking.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Investing/InvestingTracking.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,264 @@
+import React, { useCallback, useEffect, useMemo, useState } from "react";
+import { useNavigate } from "react-router-dom";
+
+import api from "../../../../api/axios";
+import graphPlaceholder from "../../../../assets/graph-placeholder.svg";
+import { fetchCurrentPricesBySymbol } from "../../../../api/twelveData";
+
+import InvestingTrackingHeader from "./components/InvestingTrackingHeader.jsx";
+import InvestingProgressCard from "./components/InvestingProgressCard.jsx";
+import InvestingAssetsTable from "./components/InvestingAssetsTable.jsx";
+
+function formatDate(value) {
+  if (!value) return "—";
+  const d = new Date(value);
+  if (Number.isNaN(d.getTime())) return String(value);
+  return d.toLocaleDateString(undefined, {
+    year: "numeric",
+    month: "short",
+    day: "2-digit",
+  });
+}
+
+function formatMoney(value) {
+  if (value === null || value === undefined || value === "") return "—";
+  const num = Number(value);
+  if (Number.isNaN(num)) return String(value);
+  return num.toLocaleString(undefined, {
+    style: "currency",
+    currency: "USD",
+    maximumFractionDigits: 2,
+  });
+}
+
+export default function InvestingTracking() {
+  const navigate = useNavigate();
+  const pageSize = 5;
+
+  const [assets, setAssets] = useState([]);
+  const [page, setPage] = useState(0);
+  const [hasMore, setHasMore] = useState(false);
+  const [isLoading, setIsLoading] = useState(true);
+  const [isLoadingMore, setIsLoadingMore] = useState(false);
+  const [isDeletingId, setIsDeletingId] = useState(null);
+  const [pendingDelete, setPendingDelete] = useState(null);
+  const [error, setError] = useState("");
+
+  const [quotesBySymbol, setQuotesBySymbol] = useState({});
+  const [isQuotesLoading, setIsQuotesLoading] = useState(false);
+  const [quotesError, setQuotesError] = useState("");
+
+  const columns = useMemo(
+    () => [
+      { key: "tickerSymbol", label: "Ticker" },
+      { key: "quantity", label: "Quantity" },
+      { key: "buyPrice", label: "Buy Price" },
+      { key: "buyDate", label: "Buy Date" },
+      { key: "currentPrice", label: "Current Price" },
+      { key: "roi", label: "ROI" },
+      { key: "actions", label: "" },
+    ],
+    [],
+  );
+
+  const fetchPage = useCallback(async (nextPage) => {
+    const resp = await api.get("/investing/assets", {
+      params: { page: nextPage, size: pageSize },
+    });
+    const data = resp?.data ?? {};
+    const nextAssets = Array.isArray(data.assets) ? data.assets : [];
+    const nextHasMore = Boolean(data.hasMore);
+    return { nextAssets, nextHasMore };
+  }, []);
+
+  useEffect(() => {
+    let cancelled = false;
+    (async () => {
+      try {
+        setIsLoading(true);
+        setError("");
+        const { nextAssets, nextHasMore } = await fetchPage(0);
+        if (cancelled) return;
+        setAssets(nextAssets);
+        setHasMore(nextHasMore);
+        setPage(0);
+      } catch (e) {
+        if (cancelled) return;
+        setError(e?.response?.data?.message || "Failed to load assets.");
+      } finally {
+        if (!cancelled) setIsLoading(false);
+      }
+    })();
+    return () => {
+      cancelled = true;
+    };
+  }, [fetchPage]);
+
+  const tickerSymbols = useMemo(() => {
+    const set = new Set();
+    for (const a of assets) {
+      if (a?.tickerSymbol) set.add(String(a.tickerSymbol).toUpperCase());
+    }
+    return Array.from(set).sort();
+  }, [assets]);
+
+  useEffect(() => {
+    if (tickerSymbols.length === 0) {
+      setQuotesBySymbol({});
+      setIsQuotesLoading(false);
+      setQuotesError("");
+      return;
+    }
+
+    let cancelled = false;
+    let intervalId;
+
+    const fetchQuotes = async () => {
+      try {
+        setIsQuotesLoading(true);
+        setQuotesError("");
+        if (cancelled) return;
+        const map = await fetchCurrentPricesBySymbol(tickerSymbols);
+        if (cancelled) return;
+        setQuotesBySymbol(map);
+      } catch (err) {
+        if (cancelled) return;
+        const msg =
+          err?.message ||
+          "Could not fetch current prices (check API key / provider availability).";
+        setQuotesError(msg);
+        // Keep any previously loaded quotes instead of wiping the table.
+        // (If this is the first load, quotesBySymbol will already be empty.)
+      } finally {
+        if (!cancelled) setIsQuotesLoading(false);
+      }
+    };
+
+    fetchQuotes();
+    // Keep refresh infrequent to avoid provider rate limits.
+    intervalId = setInterval(fetchQuotes, 5 * 60_000);
+
+    return () => {
+      cancelled = true;
+      if (intervalId) clearInterval(intervalId);
+    };
+  }, [tickerSymbols.join(",")]);
+
+  const onLoadMore = async () => {
+    if (isLoadingMore || !hasMore) return;
+    const nextPage = page + 1;
+    try {
+      setIsLoadingMore(true);
+      setError("");
+      const { nextAssets, nextHasMore } = await fetchPage(nextPage);
+      setAssets((prev) => [...prev, ...nextAssets]);
+      setHasMore(nextHasMore);
+      setPage(nextPage);
+    } catch (e) {
+      setError(e?.response?.data?.message || "Failed to load more assets.");
+    } finally {
+      setIsLoadingMore(false);
+    }
+  };
+
+  const doDelete = async (assetId) => {
+    if (!assetId || isDeletingId) return;
+    try {
+      setIsDeletingId(assetId);
+      setError("");
+      await api.delete(`/investing/assets/${assetId}`);
+      setAssets((prev) => prev.filter((a) => a.assetId !== assetId));
+      setPendingDelete(null);
+    } catch (e) {
+      setError(e?.response?.data?.message || "Failed to delete asset.");
+    } finally {
+      setIsDeletingId(null);
+    }
+  };
+
+  const onRequestDelete = (asset) => {
+    if (!asset?.assetId) return;
+    setPendingDelete({
+      assetId: asset.assetId,
+      tickerSymbol: asset.tickerSymbol,
+    });
+  };
+
+  return (
+    <div className="space-y-6">
+      <InvestingTrackingHeader
+        onAddAsset={() => navigate("/dashboard/investing/assets/new")}
+      />
+      <InvestingProgressCard graphSrc={graphPlaceholder} />
+
+      {quotesError ? (
+        <div className="text-sm opacity-70">
+          Prices unavailable: {quotesError}
+        </div>
+      ) : null}
+
+      {pendingDelete ? (
+        <div className="modal modal-open" role="dialog" aria-modal="true">
+          <div className="modal-box">
+            <h3 className="text-lg font-semibold">Delete investment?</h3>
+            <p className="opacity-80 mt-2">
+              This will permanently delete{" "}
+              <span className="font-semibold">
+                {pendingDelete.tickerSymbol || "this asset"}
+              </span>
+              .
+            </p>
+
+            <div className="modal-action">
+              <button
+                type="button"
+                className="btn btn-ghost"
+                onClick={() => setPendingDelete(null)}
+                disabled={isDeletingId === pendingDelete.assetId}
+              >
+                Cancel
+              </button>
+              <button
+                type="button"
+                className="btn bg-red-500! text-white! hover:bg-red-600! border-0"
+                onClick={() => doDelete(pendingDelete.assetId)}
+                disabled={isDeletingId === pendingDelete.assetId}
+              >
+                {isDeletingId === pendingDelete.assetId
+                  ? "Deleting…"
+                  : "Delete"}
+              </button>
+            </div>
+          </div>
+          <button
+            type="button"
+            className="modal-backdrop"
+            aria-label="Close"
+            onClick={() => {
+              if (isDeletingId === pendingDelete.assetId) return;
+              setPendingDelete(null);
+            }}
+          />
+        </div>
+      ) : null}
+
+      <InvestingAssetsTable
+        columns={columns}
+        assets={assets}
+        isLoading={isLoading}
+        error={error}
+        quotesBySymbol={quotesBySymbol}
+        isQuotesLoading={isQuotesLoading}
+        isLoadingMore={isLoadingMore}
+        hasMore={hasMore}
+        onLoadMore={onLoadMore}
+        pageSize={pageSize}
+        formatDate={formatDate}
+        formatMoney={formatMoney}
+        onAddAsset={() => navigate("/dashboard/investing/assets/new")}
+        onRequestDelete={onRequestDelete}
+        isDeletingId={isDeletingId}
+      />
+    </div>
+  );
+}
Index: frontend/src/pages/Dashboard/pages/Investing/NewInvestment.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Investing/NewInvestment.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Investing/NewInvestment.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,250 @@
+import React, { useEffect, useMemo, useState } from "react";
+import { useNavigate } from "react-router-dom";
+
+import api from "../../../../api/axios";
+import { searchTickers } from "../../../../api/yahooFinance";
+
+export default function NewInvestment() {
+  const navigate = useNavigate();
+
+  const todayIso = useMemo(() => {
+    const now = new Date();
+    const yyyy = now.getFullYear();
+    const mm = String(now.getMonth() + 1).padStart(2, "0");
+    const dd = String(now.getDate()).padStart(2, "0");
+    return `${yyyy}-${mm}-${dd}`;
+  }, []);
+
+  const initialForm = useMemo(
+    () => ({ tickerSymbol: "", quantity: "", buyPrice: "", buyDate: "" }),
+    [],
+  );
+
+  const [form, setForm] = useState(initialForm);
+  const [error, setError] = useState("");
+  const [isSubmitting, setIsSubmitting] = useState(false);
+
+  const [tickerQuery, setTickerQuery] = useState("");
+  const [tickerOptions, setTickerOptions] = useState([]);
+  const [isLoadingTickers, setIsLoadingTickers] = useState(false);
+
+  useEffect(() => {
+    const q = tickerQuery.trim();
+    if (q.length < 2) {
+      setTickerOptions([]);
+      setIsLoadingTickers(false);
+      return;
+    }
+
+    let cancelled = false;
+    const handle = setTimeout(async () => {
+      try {
+        setIsLoadingTickers(true);
+        if (cancelled) return;
+        const data = await searchTickers(q, 20);
+        if (cancelled) return;
+        setTickerOptions(Array.isArray(data) ? data : []);
+      } catch {
+        if (!cancelled) setTickerOptions([]);
+      } finally {
+        if (!cancelled) setIsLoadingTickers(false);
+      }
+    }, 250);
+
+    return () => {
+      cancelled = true;
+      clearTimeout(handle);
+    };
+  }, [tickerQuery]);
+
+  const onSubmit = async (e) => {
+    e.preventDefault();
+    setError("");
+
+    if (!form.tickerSymbol) {
+      setError("Please select a ticker symbol");
+      return;
+    }
+
+    if (form.buyDate && form.buyDate > todayIso) {
+      setError("Buy date cannot be in the future");
+      return;
+    }
+
+    const quantityNum = form.quantity === "" ? null : Number(form.quantity);
+    if (!quantityNum || Number.isNaN(quantityNum) || quantityNum <= 0) {
+      setError("Quantity must be greater than 0");
+      return;
+    }
+
+    const buyPriceNum = form.buyPrice === "" ? null : Number(form.buyPrice);
+    if (
+      buyPriceNum !== null &&
+      (Number.isNaN(buyPriceNum) || buyPriceNum < 0)
+    ) {
+      setError("Buy price must be 0 or greater");
+      return;
+    }
+
+    try {
+      setIsSubmitting(true);
+      await api.post("/investing/assets", {
+        tickerSymbol: form.tickerSymbol,
+        quantity: quantityNum,
+        buyPrice: buyPriceNum,
+        buyDate: form.buyDate === "" ? null : form.buyDate,
+      });
+
+      navigate("/dashboard/investing/tracking", { replace: true });
+    } catch (err) {
+      const message =
+        err?.response?.data?.message ||
+        Object.values(err?.response?.data?.errors ?? {})[0] ||
+        "Failed to add investment";
+      setError(message);
+    } finally {
+      setIsSubmitting(false);
+    }
+  };
+
+  return (
+    <div className="max-w-2xl">
+      <div className="card bg-base-200 border border-base-300">
+        <div className="card-body">
+          <h1 className="text-2xl font-bold">Add new investment</h1>
+          <p className="opacity-80">
+            Add an asset you bought. (We’ll build performance tracking next.)
+          </p>
+
+          {error ? (
+            <div className="alert alert-error mt-4">
+              <span>{error}</span>
+            </div>
+          ) : null}
+
+          <form onSubmit={onSubmit} className="mt-6 space-y-4">
+            <div>
+              <label className="label">
+                <span className="label-text">Ticker symbol</span>
+              </label>
+              <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
+                <input
+                  className="input input-bordered w-full"
+                  placeholder="Search (e.g. AAPL, TSLA, NVDA)"
+                  value={tickerQuery}
+                  onChange={(e) => setTickerQuery(e.target.value)}
+                />
+                <select
+                  className="select select-bordered w-full"
+                  value={form.tickerSymbol}
+                  onChange={(e) =>
+                    setForm((p) => ({ ...p, tickerSymbol: e.target.value }))
+                  }
+                  required
+                >
+                  <option value="" disabled>
+                    {isLoadingTickers
+                      ? "Loading tickers…"
+                      : tickerOptions.length
+                        ? "Select a ticker"
+                        : "Search to load tickers"}
+                  </option>
+                  {tickerOptions.map((o) => {
+                    const symbol = o?.symbol ?? "";
+                    const name = o?.name ?? "";
+                    const exchange = o?.exchange ?? "";
+                    const label = [
+                      symbol,
+                      name ? `— ${name}` : "",
+                      exchange ? `(${exchange})` : "",
+                    ]
+                      .filter(Boolean)
+                      .join(" ");
+                    return (
+                      <option key={symbol} value={symbol}>
+                        {label}
+                      </option>
+                    );
+                  })}
+                </select>
+              </div>
+              <p className="text-xs opacity-70 mt-2">
+                Only tickers returned by Yahoo Finance can be selected.
+              </p>
+            </div>
+
+            <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
+              <div>
+                <label className="label">
+                  <span className="label-text">Quantity</span>
+                </label>
+                <input
+                  type="number"
+                  step="any"
+                  min="0"
+                  className="input input-bordered w-full"
+                  placeholder="10"
+                  value={form.quantity}
+                  onChange={(e) =>
+                    setForm((p) => ({ ...p, quantity: e.target.value }))
+                  }
+                  required
+                />
+              </div>
+
+              <div>
+                <label className="label">
+                  <span className="label-text">Buy price (optional)</span>
+                </label>
+                <input
+                  type="number"
+                  step="any"
+                  min="0"
+                  className="input input-bordered w-full"
+                  placeholder="187.20"
+                  value={form.buyPrice}
+                  onChange={(e) =>
+                    setForm((p) => ({ ...p, buyPrice: e.target.value }))
+                  }
+                />
+              </div>
+            </div>
+
+            <div>
+              <label className="label">
+                <span className="label-text">Buy date (optional)</span>
+              </label>
+              <input
+                type="date"
+                className="input input-bordered w-full"
+                value={form.buyDate}
+                onChange={(e) =>
+                  setForm((p) => ({ ...p, buyDate: e.target.value }))
+                }
+                max={todayIso}
+              />
+            </div>
+
+            <div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
+              <button
+                type="button"
+                className="btn btn-ghost"
+                onClick={() => navigate("/dashboard/investing/tracking")}
+                disabled={isSubmitting}
+              >
+                Cancel
+              </button>
+              <button
+                type="submit"
+                className="btn bg-green-400! text-black! hover:bg-green-500! border-0"
+                disabled={isSubmitting}
+              >
+                {isSubmitting ? "Saving…" : "Save investment"}
+              </button>
+            </div>
+          </form>
+        </div>
+      </div>
+    </div>
+  );
+}
Index: frontend/src/pages/Dashboard/pages/Investing/components/InvestingAssetsTable.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Investing/components/InvestingAssetsTable.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Investing/components/InvestingAssetsTable.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,151 @@
+import React from "react";
+import { MdDelete } from "react-icons/md";
+
+export default function InvestingAssetsTable({
+  columns,
+  assets,
+  isLoading,
+  error,
+  quotesBySymbol,
+  isQuotesLoading,
+  isLoadingMore,
+  hasMore,
+  onLoadMore,
+  pageSize,
+  formatDate,
+  formatMoney,
+  onAddAsset,
+  onRequestDelete,
+  onDelete,
+  isDeletingId,
+}) {
+  return (
+    <div className="card bg-base-200 border border-base-300">
+      <div className="card-body">
+        <div className="flex items-center justify-between gap-4">
+          <h2 className="text-lg font-semibold">Assets</h2>
+          <span className="text-sm opacity-70">
+            Showing {pageSize} per page
+          </span>
+        </div>
+
+        {error ? (
+          <div className="alert alert-error mt-4">
+            <span>{error}</span>
+          </div>
+        ) : null}
+
+        <div className="mt-4 overflow-x-auto">
+          <table className="table">
+            <thead>
+              <tr>
+                {columns.map((c) => (
+                  <th key={c.key}>{c.label}</th>
+                ))}
+              </tr>
+            </thead>
+            <tbody>
+              {isLoading ? (
+                <tr>
+                  <td colSpan={columns.length} className="opacity-70">
+                    Loading…
+                  </td>
+                </tr>
+              ) : assets.length === 0 ? (
+                <tr>
+                  <td colSpan={columns.length} className="py-10">
+                    <div className="flex flex-col items-center text-center gap-3">
+                      <p className="opacity-80">No investments yet.</p>
+                      <button
+                        type="button"
+                        className="btn bg-green-400! text-black! hover:bg-green-500! border-0"
+                        onClick={onAddAsset}
+                      >
+                        Add your first investment
+                      </button>
+                    </div>
+                  </td>
+                </tr>
+              ) : (
+                assets.map((a) => (
+                  <tr key={a.assetId}>
+                    <td className="font-semibold">{a.tickerSymbol ?? "—"}</td>
+                    <td>{a.quantity ?? "—"}</td>
+                    <td>{formatMoney(a.buyPrice)}</td>
+                    <td>{formatDate(a.buyDate)}</td>
+                    <td>
+                      {isQuotesLoading
+                        ? "Loading…"
+                        : formatMoney(
+                            quotesBySymbol?.[
+                              String(a.tickerSymbol ?? "").toUpperCase()
+                            ],
+                          )}
+                    </td>
+                    <td>
+                      {(() => {
+                        const symbol = String(
+                          a.tickerSymbol ?? "",
+                        ).toUpperCase();
+                        const current = Number(quotesBySymbol?.[symbol]);
+                        const buy = Number(a.buyPrice);
+                        if (!symbol || Number.isNaN(current)) return "—";
+                        if (Number.isNaN(buy) || buy <= 0) return "—";
+                        const roiPct = ((current - buy) / buy) * 100;
+                        const cls =
+                          roiPct > 0
+                            ? "text-green-400 font-semibold"
+                            : roiPct < 0
+                              ? "text-red-400 font-semibold"
+                              : "opacity-80";
+                        const sign = roiPct > 0 ? "+" : "";
+                        return (
+                          <span className={cls}>
+                            {sign}
+                            {roiPct.toFixed(2)}%
+                          </span>
+                        );
+                      })()}
+                    </td>
+                    <td className="text-right">
+                      <button
+                        type="button"
+                        className="btn btn-ghost btn-sm text-red-400 hover:text-red-300"
+                        title="Delete"
+                        onClick={() => {
+                          if (onRequestDelete) {
+                            onRequestDelete(a);
+                            return;
+                          }
+                          onDelete?.(a.assetId);
+                        }}
+                        disabled={isDeletingId === a.assetId}
+                      >
+                        <MdDelete className="size-5" />
+                      </button>
+                    </td>
+                  </tr>
+                ))
+              )}
+            </tbody>
+          </table>
+        </div>
+
+        <div className="mt-4 flex justify-center">
+          {!isLoading && assets.length === 0 ? null : hasMore ? (
+            <button
+              type="button"
+              className="btn"
+              onClick={onLoadMore}
+              disabled={isLoadingMore}
+            >
+              {isLoadingMore ? "Loading…" : "Load more"}
+            </button>
+          ) : (
+            <span className="text-sm opacity-70">No more assets.</span>
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}
Index: frontend/src/pages/Dashboard/pages/Investing/components/InvestingCenteredCard.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Investing/components/InvestingCenteredCard.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Investing/components/InvestingCenteredCard.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,16 @@
+import React from "react";
+
+export default function InvestingCenteredCard({ title, message }) {
+  return (
+    <div className="min-h-[60vh] w-full flex items-center justify-center">
+      <div className="w-full max-w-2xl">
+        <div className="card bg-base-200 border border-base-300">
+          <div className="card-body">
+            <h1 className="text-2xl font-bold">{title}</h1>
+            <p className="opacity-80">{message}</p>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
Index: frontend/src/pages/Dashboard/pages/Investing/components/InvestingProgressCard.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Investing/components/InvestingProgressCard.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Investing/components/InvestingProgressCard.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,27 @@
+import React from "react";
+
+export default function InvestingProgressCard({ graphSrc }) {
+  return (
+    <div className="card bg-base-200 border border-base-300">
+      <div className="card-body">
+        <div className="flex items-center justify-between gap-4">
+          <div>
+            <h2 className="text-lg font-semibold">Progress</h2>
+            <p className="opacity-80">
+              Graph placeholder — we’ll visualize portfolio value here.
+            </p>
+          </div>
+        </div>
+        <div className="mt-4 overflow-hidden rounded-xl bg-base-300/30 border border-base-300">
+          <div className="p-4">
+            <img
+              src={graphSrc}
+              alt="Graph placeholder"
+              className="w-full max-h-64 object-contain opacity-90"
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
Index: frontend/src/pages/Dashboard/pages/Investing/components/InvestingStartCtaCard.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Investing/components/InvestingStartCtaCard.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Investing/components/InvestingStartCtaCard.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,36 @@
+import React from "react";
+
+export default function InvestingStartCtaCard({
+  error,
+  onStart,
+  isSubmitting,
+}) {
+  return (
+    <div className="card bg-base-200 border border-base-300">
+      <div className="card-body">
+        <h1 className="text-2xl font-bold">Investing</h1>
+        <p className="opacity-80">
+          You’re not tracking investments yet. Start tracking to log your assets
+          and visualize your progress.
+        </p>
+
+        {error ? (
+          <div className="alert alert-error mt-4">
+            <span>{error}</span>
+          </div>
+        ) : null}
+
+        <div className="mt-6">
+          <button
+            type="button"
+            className="btn btn-lg w-full bg-green-400! text-black! hover:bg-green-500! border-0"
+            onClick={onStart}
+            disabled={isSubmitting}
+          >
+            {isSubmitting ? "Starting…" : "Start Tracking Investments"}
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+}
Index: frontend/src/pages/Dashboard/pages/Investing/components/InvestingTrackingHeader.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Investing/components/InvestingTrackingHeader.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Investing/components/InvestingTrackingHeader.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,19 @@
+import React from "react";
+
+export default function InvestingTrackingHeader({ onAddAsset }) {
+  return (
+    <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
+      <div>
+        <h1 className="text-2xl font-bold">Investing</h1>
+        <p className="opacity-80">Track your assets over time.</p>
+      </div>
+      <button
+        type="button"
+        className="btn bg-green-400! text-black! hover:bg-green-500! border-0"
+        onClick={onAddAsset}
+      >
+        + Add new investment
+      </button>
+    </div>
+  );
+}
Index: frontend/src/pages/Dashboard/pages/Training/NewTrainingSession.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/NewTrainingSession.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Training/NewTrainingSession.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,241 @@
+import React, { useEffect, useMemo, useState } from "react";
+import { useNavigate } from "react-router-dom";
+
+import api from "../../../../api/axios";
+
+import WorkoutTypeSelect from "./components/WorkoutTypeSelect.jsx";
+import CaloriesEstimate from "./components/CaloriesEstimate.jsx";
+
+const NewTrainingSession = () => {
+  const navigate = useNavigate();
+
+  const [workoutTypes, setWorkoutTypes] = useState([]);
+  const [profileWeightKg, setProfileWeightKg] = useState(null);
+  const [isLoading, setIsLoading] = useState(true);
+
+  const [type, setType] = useState("");
+  const [durationMinutes, setDurationMinutes] = useState("");
+  const [autoCalculateCalories, setAutoCalculateCalories] = useState(true);
+  const [calories, setCalories] = useState("");
+
+  const [error, setError] = useState("");
+  const [isSubmitting, setIsSubmitting] = useState(false);
+
+  useEffect(() => {
+    let cancelled = false;
+    (async () => {
+      try {
+        setIsLoading(true);
+        setError("");
+
+        const [typesRes, profileRes] = await Promise.all([
+          api.get("/training/workout-types"),
+          api.get("/training/profile"),
+        ]);
+
+        if (cancelled) return;
+
+        const types = Array.isArray(typesRes?.data) ? typesRes.data : [];
+        setWorkoutTypes(types);
+
+        const weight = profileRes?.data?.weight;
+        setProfileWeightKg(
+          weight === null || weight === undefined || weight === ""
+            ? null
+            : Number(weight),
+        );
+      } catch (err) {
+        if (cancelled) return;
+        setError(
+          err?.response?.data?.message ||
+            "Failed to load workout types/training profile.",
+        );
+      } finally {
+        if (!cancelled) setIsLoading(false);
+      }
+    })();
+    return () => {
+      cancelled = true;
+    };
+  }, []);
+
+  const selectedWorkout = useMemo(
+    () => workoutTypes.find((t) => t.type === type) ?? null,
+    [workoutTypes, type],
+  );
+
+  const estimatedCalories = useMemo(() => {
+    const met = selectedWorkout?.met;
+    const durationNum = durationMinutes === "" ? NaN : Number(durationMinutes);
+    const weightNum = profileWeightKg;
+    if (!Number.isFinite(Number(met))) return null;
+    if (!Number.isFinite(durationNum) || durationNum <= 0) return null;
+    if (!Number.isFinite(weightNum) || weightNum <= 0) return null;
+
+    const caloriesValue = Number(met) * weightNum * (durationNum / 60);
+    if (!Number.isFinite(caloriesValue)) return null;
+    return Math.round(caloriesValue * 100) / 100;
+  }, [durationMinutes, profileWeightKg, selectedWorkout?.met]);
+
+  const onSubmit = async (e) => {
+    e.preventDefault();
+    setError("");
+
+    if (isLoading) return;
+
+    if (!type) {
+      setError("Please select a workout type.");
+      return;
+    }
+
+    const durationNum = durationMinutes === "" ? NaN : Number(durationMinutes);
+    if (!Number.isFinite(durationNum) || durationNum < 1) {
+      setError("Duration must be at least 1 minute.");
+      return;
+    }
+
+    const caloriesNum = calories === "" ? NaN : Number(calories);
+    if (!autoCalculateCalories) {
+      if (!Number.isFinite(caloriesNum) || caloriesNum < 0) {
+        setError("Calories must be 0 or greater.");
+        return;
+      }
+    }
+
+    try {
+      setIsSubmitting(true);
+      await api.post("/training/sessions", {
+        type,
+        durationMinutes: durationNum,
+        autoCalculateCalories,
+        calories: autoCalculateCalories ? null : caloriesNum,
+      });
+
+      navigate("/dashboard/training/tracking", { replace: true });
+    } catch (err) {
+      setError(
+        err?.response?.data?.message || "Failed to add training session.",
+      );
+    } finally {
+      setIsSubmitting(false);
+    }
+  };
+
+  return (
+    <div className="min-h-[70vh] w-full flex items-center justify-center">
+      <form
+        onSubmit={onSubmit}
+        className="card bg-base-200 border border-base-300 w-full max-w-2xl"
+      >
+        <div className="card-body">
+          <div className="flex items-start justify-between gap-4">
+            <div>
+              <h1 className="text-2xl font-bold">Add training session</h1>
+              <p className="opacity-80 mt-1">
+                Choose a workout type, enter the duration, and we’ll save your
+                session.
+              </p>
+            </div>
+          </div>
+
+          {error ? <p className="text-error text-sm mt-2">{error}</p> : null}
+
+          {isLoading ? (
+            <p className="opacity-80 mt-2">Loading workout types…</p>
+          ) : null}
+
+          <div className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
+            <WorkoutTypeSelect
+              workoutTypes={workoutTypes}
+              value={type}
+              onChange={(e) => setType(e.target.value)}
+              disabled={isLoading}
+            />
+
+            <div>
+              <label className="label">Duration (minutes)</label>
+              <input
+                type="number"
+                className="input input-bordered w-full"
+                value={durationMinutes}
+                onChange={(e) => setDurationMinutes(e.target.value)}
+                min={1}
+                step={1}
+                required
+                disabled={isLoading}
+              />
+            </div>
+          </div>
+
+          <div className="mt-4">
+            <label className="label cursor-pointer justify-start gap-3">
+              <input
+                type="checkbox"
+                className="checkbox"
+                checked={autoCalculateCalories}
+                onChange={(e) => {
+                  const next = e.target.checked;
+                  setAutoCalculateCalories(next);
+                  if (
+                    !next &&
+                    (calories === "" || calories === null) &&
+                    estimatedCalories !== null
+                  ) {
+                    setCalories(String(estimatedCalories));
+                  }
+                }}
+              />
+              <span className="label-text">Auto-calculate calories burned</span>
+            </label>
+            <p className="text-xs opacity-70 mt-1">
+              Uses your training profile weight and the workout type.
+            </p>
+            <CaloriesEstimate
+              estimatedCalories={estimatedCalories}
+              profileWeightKg={profileWeightKg}
+            />
+          </div>
+
+          <div className="mt-2">
+            <label className="label">Calories (kcal)</label>
+            <input
+              type="number"
+              className="input input-bordered w-full"
+              value={calories}
+              onChange={(e) => setCalories(e.target.value)}
+              min={0}
+              step={1}
+              disabled={autoCalculateCalories}
+              placeholder={
+                autoCalculateCalories
+                  ? "Calculated automatically"
+                  : "Enter calories"
+              }
+              required={!autoCalculateCalories}
+            />
+          </div>
+
+          <div className="mt-6 flex flex-col gap-3 sm:flex-row sm:justify-end">
+            <button
+              type="button"
+              className="btn btn-ghost"
+              onClick={() => navigate("/dashboard/training/tracking")}
+              disabled={isSubmitting}
+            >
+              Cancel
+            </button>
+            <button
+              type="submit"
+              className="btn bg-green-400! text-black! hover:bg-green-500!"
+              disabled={isSubmitting || isLoading}
+            >
+              {isSubmitting ? "Saving…" : "Save session"}
+            </button>
+          </div>
+        </div>
+      </form>
+    </div>
+  );
+};
+
+export default NewTrainingSession;
Index: frontend/src/pages/Dashboard/pages/Training/Training.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/Training.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Training/Training.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,114 @@
+import React, { useEffect, useMemo, useState } from "react";
+import { useNavigate } from "react-router-dom";
+
+import api from "../../../../api/axios";
+
+import TrainingCenteredCard from "./components/TrainingCenteredCard.jsx";
+import TrainingStartCtaCard from "./components/TrainingStartCtaCard.jsx";
+import TrainingStartForm from "./components/TrainingStartForm.jsx";
+
+const Training = () => {
+  const navigate = useNavigate();
+
+  const [isLoading, setIsLoading] = useState(true);
+  const [isTracking, setIsTracking] = useState(false);
+  const [step, setStep] = useState("cta"); // cta | form
+  const [error, setError] = useState("");
+  const [isSubmitting, setIsSubmitting] = useState(false);
+
+  const initialForm = useMemo(() => ({ gender: "", age: "", weight: "" }), []);
+  const [form, setForm] = useState(initialForm);
+
+  useEffect(() => {
+    let isMounted = true;
+    const run = async () => {
+      setIsLoading(true);
+      setError("");
+      try {
+        const res = await api.get("/training/status");
+        const tracking = Boolean(res?.data?.tracking);
+        if (!isMounted) return;
+        setIsTracking(tracking);
+        if (tracking) {
+          navigate("/dashboard/training/tracking", { replace: true });
+        }
+      } catch (err) {
+        if (!isMounted) return;
+        const message =
+          err?.response?.data?.message || "Failed to load training status";
+        setError(message);
+      } finally {
+        if (isMounted) setIsLoading(false);
+      }
+    };
+
+    run();
+    return () => {
+      isMounted = false;
+    };
+  }, [navigate]);
+
+  const onStart = () => {
+    setError("");
+    setStep("form");
+  };
+
+  const onCancel = () => {
+    setStep("cta");
+    setForm(initialForm);
+    setError("");
+  };
+
+  const onSubmit = async (e) => {
+    e.preventDefault();
+    setError("");
+    setIsSubmitting(true);
+    try {
+      await api.post("/training/start", {
+        gender: form.gender,
+        age: form.age === "" ? null : Number(form.age),
+        weight: form.weight === "" ? null : Number(form.weight),
+      });
+
+      setIsTracking(true);
+      navigate("/dashboard/training/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 <TrainingCenteredCard title="Training" message="Loading…" />;
+  }
+
+  if (isTracking) {
+    return <TrainingCenteredCard title="Training" message="Redirecting…" />;
+  }
+
+  return (
+    <div className="min-h-[70vh] w-full flex items-center justify-center">
+      <div className="w-full max-w-3xl">
+        {step === "cta" ? (
+          <TrainingStartCtaCard error={error} onStart={onStart} />
+        ) : (
+          <TrainingStartForm
+            form={form}
+            setForm={setForm}
+            onSubmit={onSubmit}
+            onCancel={onCancel}
+            error={error}
+            isSubmitting={isSubmitting}
+          />
+        )}
+      </div>
+    </div>
+  );
+};
+
+export default Training;
Index: frontend/src/pages/Dashboard/pages/Training/TrainingTracking.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/TrainingTracking.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Training/TrainingTracking.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,120 @@
+import React, { useCallback, useEffect, useMemo, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import api from "../../../../api/axios";
+import graphPlaceholder from "../../../../assets/graph-placeholder.svg";
+
+import TrainingTrackingHeader from "./components/TrainingTrackingHeader.jsx";
+import TrainingProgressCard from "./components/TrainingProgressCard.jsx";
+import TrainingSessionsTable from "./components/TrainingSessionsTable.jsx";
+
+function formatDate(value) {
+  if (!value) return "—";
+  const d = new Date(value);
+  if (Number.isNaN(d.getTime())) return String(value);
+  return d.toLocaleDateString(undefined, {
+    year: "numeric",
+    month: "short",
+    day: "2-digit",
+  });
+}
+
+function titleCase(value) {
+  if (!value) return "—";
+  const s = String(value);
+  return s.charAt(0).toUpperCase() + s.slice(1);
+}
+
+export default function TrainingTracking() {
+  const navigate = useNavigate();
+  const pageSize = 5;
+
+  const [sessions, setSessions] = 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 columns = useMemo(
+    () => [
+      { key: "date", label: "Date" },
+      { key: "type", label: "Type" },
+      { key: "duration", label: "Duration (min)" },
+      { key: "calories", label: "Calories" },
+    ],
+    [],
+  );
+
+  const fetchPage = useCallback(async (nextPage) => {
+    const resp = await api.get("/training/sessions", {
+      params: { page: nextPage, size: pageSize },
+    });
+    const data = resp?.data ?? {};
+    const nextSessions = Array.isArray(data.sessions) ? data.sessions : [];
+    const nextHasMore = Boolean(data.hasMore);
+    return { nextSessions, nextHasMore };
+  }, []);
+
+  useEffect(() => {
+    let cancelled = false;
+    (async () => {
+      try {
+        setIsLoading(true);
+        setError("");
+        const { nextSessions, nextHasMore } = await fetchPage(0);
+        if (cancelled) return;
+        setSessions(nextSessions);
+        setHasMore(nextHasMore);
+        setPage(0);
+      } catch (e) {
+        if (cancelled) return;
+        setError(
+          e?.response?.data?.message || "Failed to load training sessions.",
+        );
+      } finally {
+        if (!cancelled) setIsLoading(false);
+      }
+    })();
+    return () => {
+      cancelled = true;
+    };
+  }, [fetchPage]);
+
+  const onLoadMore = async () => {
+    if (isLoadingMore || !hasMore) return;
+    const nextPage = page + 1;
+    try {
+      setIsLoadingMore(true);
+      setError("");
+      const { nextSessions, nextHasMore } = await fetchPage(nextPage);
+      setSessions((prev) => [...prev, ...nextSessions]);
+      setHasMore(nextHasMore);
+      setPage(nextPage);
+    } catch (e) {
+      setError(e?.response?.data?.message || "Failed to load more sessions.");
+    } finally {
+      setIsLoadingMore(false);
+    }
+  };
+
+  return (
+    <div className="space-y-6">
+      <TrainingTrackingHeader
+        onAddSession={() => navigate("/dashboard/training/sessions/new")}
+      />
+      <TrainingProgressCard graphSrc={graphPlaceholder} />
+      <TrainingSessionsTable
+        columns={columns}
+        sessions={sessions}
+        isLoading={isLoading}
+        error={error}
+        isLoadingMore={isLoadingMore}
+        hasMore={hasMore}
+        onLoadMore={onLoadMore}
+        pageSize={pageSize}
+        formatDate={formatDate}
+        titleCase={titleCase}
+      />
+    </div>
+  );
+}
Index: frontend/src/pages/Dashboard/pages/Training/components/CaloriesEstimate.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/components/CaloriesEstimate.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Training/components/CaloriesEstimate.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,21 @@
+import React from "react";
+
+const CaloriesEstimate = ({ estimatedCalories, profileWeightKg }) => {
+  return (
+    <div className="mt-2">
+      {estimatedCalories === null ? (
+        <p className="text-xs opacity-70">
+          Estimated calories: —
+          {profileWeightKg === null ? " (missing profile weight)" : ""}
+        </p>
+      ) : (
+        <p className="text-xs opacity-80">
+          Estimated calories:{" "}
+          <span className="font-semibold">{estimatedCalories}</span> kcal
+        </p>
+      )}
+    </div>
+  );
+};
+
+export default CaloriesEstimate;
Index: frontend/src/pages/Dashboard/pages/Training/components/TrainingCenteredCard.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/components/TrainingCenteredCard.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Training/components/TrainingCenteredCard.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,17 @@
+import React from "react";
+
+const TrainingCenteredCard = ({ title, message, children }) => {
+  return (
+    <div className="min-h-[60vh] w-full flex items-center justify-center">
+      <div className="card bg-base-200 border border-base-300 w-full max-w-xl">
+        <div className="card-body items-center text-center">
+          {title ? <h1 className="text-3xl font-bold">{title}</h1> : null}
+          {message ? <p className="opacity-80">{message}</p> : null}
+          {children}
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default TrainingCenteredCard;
Index: frontend/src/pages/Dashboard/pages/Training/components/TrainingProgressCard.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/components/TrainingProgressCard.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Training/components/TrainingProgressCard.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,23 @@
+import React from "react";
+
+const TrainingProgressCard = ({ graphSrc }) => {
+  return (
+    <div className="card bg-base-200 border border-base-300">
+      <div className="card-body">
+        <div className="flex items-center justify-between gap-4">
+          <h2 className="card-title">Progress</h2>
+          <span className="badge badge-ghost">Graph placeholder</span>
+        </div>
+        <div className="mt-4 w-full overflow-hidden rounded-xl border border-base-300 bg-base-100">
+          <img
+            src={graphSrc}
+            alt="Training progress graph placeholder"
+            className="block h-65 w-full object-cover"
+          />
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default TrainingProgressCard;
Index: frontend/src/pages/Dashboard/pages/Training/components/TrainingSessionsTable.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/components/TrainingSessionsTable.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Training/components/TrainingSessionsTable.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,81 @@
+import React from "react";
+
+const TrainingSessionsTable = ({
+  columns,
+  sessions,
+  isLoading,
+  error,
+  isLoadingMore,
+  hasMore,
+  onLoadMore,
+  pageSize,
+  formatDate,
+  titleCase,
+}) => {
+  return (
+    <div className="card bg-base-200 border border-base-300">
+      <div className="card-body">
+        <div className="flex items-center justify-between gap-4">
+          <h2 className="card-title">Recent sessions</h2>
+          <span className="badge badge-ghost">Showing up to {pageSize}</span>
+        </div>
+
+        {error ? <p className="text-error text-sm mt-2">{error}</p> : null}
+
+        <div className="mt-4 overflow-x-auto">
+          <table className="table table-zebra w-full">
+            <thead>
+              <tr>
+                {columns.map((c) => (
+                  <th key={c.key}>{c.label}</th>
+                ))}
+              </tr>
+            </thead>
+            <tbody>
+              {isLoading ? (
+                <tr>
+                  <td colSpan={columns.length}>
+                    <span className="opacity-80">Loading…</span>
+                  </td>
+                </tr>
+              ) : sessions.length === 0 ? (
+                <tr>
+                  <td colSpan={columns.length}>
+                    <span className="opacity-80">No sessions yet.</span>
+                  </td>
+                </tr>
+              ) : (
+                sessions.map((s) => (
+                  <tr
+                    key={
+                      s.trainingId ??
+                      `${s.date}-${s.type}-${s.duration}-${s.calories}`
+                    }
+                  >
+                    <td>{formatDate(s.date)}</td>
+                    <td>{titleCase(s.type)}</td>
+                    <td>{s.duration ?? "—"}</td>
+                    <td>{s.calories ?? "—"}</td>
+                  </tr>
+                ))
+              )}
+            </tbody>
+          </table>
+        </div>
+
+        <div className="mt-4 flex items-center justify-center">
+          <button
+            type="button"
+            className="btn btn-outline"
+            onClick={onLoadMore}
+            disabled={isLoading || isLoadingMore || !hasMore}
+          >
+            {isLoadingMore ? "Loading…" : hasMore ? "Load more" : "No more"}
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default TrainingSessionsTable;
Index: frontend/src/pages/Dashboard/pages/Training/components/TrainingStartCtaCard.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/components/TrainingStartCtaCard.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Training/components/TrainingStartCtaCard.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,26 @@
+import React from "react";
+
+const TrainingStartCtaCard = ({ error, onStart }) => {
+  return (
+    <div className="card bg-base-200 border border-base-300">
+      <div className="card-body items-center text-center">
+        <h1 className="text-4xl font-bold">You are not tracking training</h1>
+        <p className="opacity-80 max-w-xl">
+          Start tracking to log sessions, see trends, and build consistency.
+        </p>
+
+        {error ? <p className="text-error text-sm mt-2">{error}</p> : null}
+
+        <button
+          type="button"
+          className="btn btn-lg mt-6 w-full max-w-md bg-green-400! text-black! hover:bg-green-500!"
+          onClick={onStart}
+        >
+          Start Tracking
+        </button>
+      </div>
+    </div>
+  );
+};
+
+export default TrainingStartCtaCard;
Index: frontend/src/pages/Dashboard/pages/Training/components/TrainingStartForm.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/components/TrainingStartForm.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Training/components/TrainingStartForm.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,94 @@
+import React from "react";
+
+const TrainingStartForm = ({
+  form,
+  setForm,
+  onSubmit,
+  onCancel,
+  error,
+  isSubmitting,
+}) => {
+  return (
+    <form
+      onSubmit={onSubmit}
+      className="card bg-base-200 border border-base-300"
+    >
+      <div className="card-body items-center text-center">
+        <h1 className="text-3xl font-bold">Start tracking training</h1>
+        <p className="opacity-80 max-w-xl">
+          Fill in a few details to set up your training profile.
+        </p>
+
+        {error ? <p className="text-error text-sm mt-2">{error}</p> : null}
+
+        <div className="mt-4 grid w-full grid-cols-1 gap-4 md:grid-cols-3">
+          <div>
+            <label className="label">Gender</label>
+            <select
+              className="select select-bordered w-full"
+              value={form.gender}
+              onChange={(e) =>
+                setForm((p) => ({ ...p, gender: e.target.value }))
+              }
+              required
+            >
+              <option value="" disabled>
+                Select…
+              </option>
+              <option value="male">Male</option>
+              <option value="female">Female</option>
+              <option value="other">Other</option>
+            </select>
+          </div>
+
+          <div>
+            <label className="label">Age</label>
+            <input
+              type="number"
+              className="input input-bordered w-full"
+              value={form.age}
+              onChange={(e) => setForm((p) => ({ ...p, age: e.target.value }))}
+              min={1}
+              max={120}
+              required
+            />
+          </div>
+
+          <div>
+            <label className="label">Weight (kg)</label>
+            <input
+              type="number"
+              className="input input-bordered w-full"
+              value={form.weight}
+              onChange={(e) =>
+                setForm((p) => ({ ...p, weight: e.target.value }))
+              }
+              min={1}
+              step="0.1"
+              required
+            />
+          </div>
+        </div>
+
+        <div className="mt-6 flex w-full flex-col items-center gap-3 sm:flex-row sm:justify-center">
+          <button
+            className="btn btn-lg w-full sm:w-auto bg-green-400! text-black! hover:bg-green-500!"
+            type="submit"
+            disabled={isSubmitting}
+          >
+            {isSubmitting ? "Starting…" : "Start Tracking"}
+          </button>
+          <button
+            type="button"
+            className="btn btn-ghost btn-lg w-full sm:w-auto"
+            onClick={onCancel}
+          >
+            Cancel
+          </button>
+        </div>
+      </div>
+    </form>
+  );
+};
+
+export default TrainingStartForm;
Index: frontend/src/pages/Dashboard/pages/Training/components/TrainingTrackingHeader.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/components/TrainingTrackingHeader.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Training/components/TrainingTrackingHeader.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,22 @@
+import React from "react";
+
+const TrainingTrackingHeader = ({ onAddSession }) => {
+  return (
+    <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
+      <div>
+        <h1 className="text-2xl font-bold">Training</h1>
+        <p className="opacity-80 mt-1">Your recent sessions and progress.</p>
+      </div>
+
+      <button
+        type="button"
+        className="btn bg-green-400! text-black! hover:bg-green-500!"
+        onClick={onAddSession}
+      >
+        + Add new training session
+      </button>
+    </div>
+  );
+};
+
+export default TrainingTrackingHeader;
Index: frontend/src/pages/Dashboard/pages/Training/components/WorkoutTypeSelect.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/components/WorkoutTypeSelect.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Training/components/WorkoutTypeSelect.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,27 @@
+import React from "react";
+
+const WorkoutTypeSelect = ({ workoutTypes, value, onChange, disabled }) => {
+  return (
+    <div>
+      <label className="label">Workout type</label>
+      <select
+        className="select select-bordered w-full"
+        value={value}
+        onChange={onChange}
+        required
+        disabled={disabled}
+      >
+        <option value="" disabled>
+          Select…
+        </option>
+        {workoutTypes.map((t) => (
+          <option key={t.type} value={t.type}>
+            {t.label ?? t.type}
+          </option>
+        ))}
+      </select>
+    </div>
+  );
+};
+
+export default WorkoutTypeSelect;
Index: frontend/src/pages/Dashboard/pages/Weight/Weight.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Weight/Weight.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Weight/Weight.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,16 @@
+import React from "react";
+
+const Weight = () => {
+  return (
+    <div className="space-y-4">
+      <h1 className="text-2xl font-bold">Weight</h1>
+      <div className="card bg-base-200 border border-base-300">
+        <div className="card-body">
+          <p className="opacity-80">Weight dashboard content goes here.</p>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default Weight;
Index: frontend/vite.config.js
===================================================================
--- frontend/vite.config.js	(revision ff01f66118570f9849d473fe7df4fe26b5e3a361)
+++ frontend/vite.config.js	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -6,3 +6,21 @@
 export default defineConfig({
   plugins: [react(), tailwindcss()],
+  server: {
+    proxy: {
+      // Dev-only Yahoo Finance proxy to avoid browser CORS.
+      // Frontend can call `/yahoo/...` and `/yahoo2/...` as same-origin.
+      '/yahoo': {
+        target: 'https://query1.finance.yahoo.com',
+        changeOrigin: true,
+        secure: true,
+        rewrite: (path) => path.replace(/^\/yahoo/, ''),
+      },
+      '/yahoo2': {
+        target: 'https://query2.finance.yahoo.com',
+        changeOrigin: true,
+        secure: true,
+        rewrite: (path) => path.replace(/^\/yahoo2/, ''),
+      },
+    },
+  },
 })
