Dedup Garmin data storage, configurable chart colors, taxonomy fixes, sync/progression fixes

- Remove duplicated Garmin fields from storage; decode display-only fields
  (activity name/type, lap duration/HR, structured workout raw JSON) from
  RawJSON at API-response time instead of storing redundant columns
- Add a fully configurable chart color system (pace/HR main-line colors, 4
  effort-kind colors, tint/darken/brighten intensity knobs) under Profile >
  Chart colors
- Rename training types and fix their display order (Easy, Long, 60'/30'
  Threshold, Tempo, Intervals, MAS Test, Race) everywhere they're listed
- Add an Efficiency Factor progression metric; fix Progression chart axes to
  use tight non-zero-based domains, m:ss/km pace formatting, and rounded
  ticks instead of raw floating-point labels
- Expose the raw get_workout_by_id() payload in the raw-data viewer
  alongside activity/lap/detail JSON; enlarge the modal and shrink array
  indentation for readability
- Fix "last sync" reporting a meaningless activity count: record one
  combined sync run per manual "Sync now" and count genuinely new
  activities instead of re-listing whatever Garmin returned for the queried
  window
- Let a Review Queue activity be manually cleared back to Unclassified, and
  make "Reset all" available even while disconnected from Garmin

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 06:33:47 +02:00
parent 9818d35910
commit 518bccf5ab
56 changed files with 2334 additions and 890 deletions

View File

@@ -10,7 +10,7 @@ body {
}
.app {
max-width: 960px;
max-width: 1100px;
margin: 0 auto;
padding: 0 1.5rem 3rem;
}
@@ -42,18 +42,57 @@ body {
cursor: pointer;
}
.tab-icon {
margin-right: 0.4rem;
}
.tab.active {
background: #3b82f6;
border-color: #3b82f6;
color: white;
}
/* Pill-shaped and icon-led, deliberately unlike the rectangular .tab
buttons: this opens account/profile settings, not an application view
alongside Activities/Progression/Training plan, so it reads more like an
account chip (à la Slack/GitHub's corner avatar) than another nav tab. */
.profile-name {
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 0.4rem;
background: #1a1d24;
border: 1px solid #2a2d35;
color: #9aa0ab;
padding: 0.4rem 0.9rem 0.4rem 0.7rem;
border-radius: 999px;
cursor: pointer;
font-weight: 600;
font-size: 0.85rem;
}
.profile-name::before {
content: "⚙️";
font-size: 0.8rem;
}
.profile-name:hover:not(.active) {
border-color: #3b82f6;
color: #e6e6e6;
}
.profile-name.active {
background: #3b82f6;
border-color: #3b82f6;
color: white;
}
.garmin-connection {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.75rem 0;
border-bottom: 1px solid #2a2d35;
padding-top: 0.75rem;
border-top: 1px solid #2a2d35;
}
.garmin-connection-row {
@@ -62,6 +101,22 @@ body {
gap: 0.75rem;
}
.garmin-connection-main {
justify-content: space-between;
}
.garmin-connection-actions {
display: flex;
align-items: center;
gap: 0.75rem;
}
.garmin-connection-status {
display: flex;
align-items: center;
gap: 0.75rem;
}
.garmin-connection-message {
margin: 0;
font-size: 0.8rem;
@@ -128,6 +183,13 @@ button {
cursor: pointer;
}
input[type="color"] {
padding: 0.2rem;
width: 3.5rem;
height: 2rem;
cursor: pointer;
}
button:hover:not(:disabled) {
border-color: #3b82f6;
}
@@ -151,11 +213,15 @@ button:disabled {
.filter-pills {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
flex-wrap: nowrap;
overflow-x: auto;
margin-bottom: 1rem;
padding-bottom: 0.25rem;
}
.filter-pill {
flex-shrink: 0;
white-space: nowrap;
border-radius: 999px;
padding: 0.35rem 0.9rem;
font-size: 0.85rem;
@@ -197,14 +263,30 @@ button:disabled {
.review-item-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
font-size: 1rem;
}
.review-item-title {
display: flex;
align-items: center;
gap: 0.6rem;
flex-wrap: wrap;
}
.review-item-header span {
color: #9aa0ab;
font-size: 0.85rem;
}
.review-item-datetime {
display: flex;
flex-direction: column;
align-items: flex-end;
flex-shrink: 0;
line-height: 1.3;
}
.review-item-stats {
display: flex;
gap: 1rem;
@@ -219,11 +301,49 @@ button:disabled {
margin: 0.5rem 0;
}
.review-item-actions {
.classify-control {
position: relative;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-top: 0.5rem;
align-items: center;
gap: 0.4rem;
}
.classify-label {
font-size: 0.85rem;
}
.classify-lock {
padding: 0.25rem 0.5rem;
font-size: 0.85rem;
line-height: 1;
}
.classify-dropdown {
position: absolute;
top: 100%;
left: 0;
z-index: 10;
margin-top: 0.25rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 0.4rem;
background: #14161c;
border: 1px solid #2a2d35;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
.classify-dropdown button {
text-align: left;
white-space: nowrap;
}
.classify-dropdown-unassign {
color: #9aa0ab;
padding-bottom: 0.4rem;
margin-bottom: 0.15rem;
border-bottom: 1px solid #2a2d35;
}
.expected-actual-charts {
@@ -239,14 +359,15 @@ button:disabled {
.mini-chart-label {
display: block;
text-align: center;
font-size: 0.75rem;
color: #9aa0ab;
margin-bottom: 0.15rem;
}
.phase-legend {
flex-basis: 100%;
display: flex;
justify-content: center;
gap: 0.75rem;
flex-wrap: wrap;
}
@@ -292,8 +413,8 @@ button:disabled {
background: #14161c;
border: 1px solid #2a2d35;
border-radius: 8px;
width: min(720px, 90vw);
max-height: 80vh;
width: min(1100px, 94vw);
max-height: 88vh;
display: flex;
flex-direction: column;
}
@@ -307,42 +428,75 @@ button:disabled {
font-weight: 600;
}
.modal-header-actions {
display: flex;
gap: 0.5rem;
font-weight: 400;
}
.modal-header-actions button {
font-size: 0.75rem;
padding: 0.25rem 0.6rem;
}
.modal-json {
margin: 0;
padding: 1rem;
overflow: auto;
font-size: 0.8rem;
line-height: 1.4;
line-height: 1.5;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.json-row {
white-space: pre-wrap;
word-break: break-word;
}
.kinds-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1rem;
}
.lock-badge {
.json-toggle {
display: inline-block;
border-radius: 999px;
padding: 0.15rem 0.6rem;
font-size: 0.75rem;
width: 1rem;
padding: 0;
border: none;
background: none;
color: #9aa0ab;
border: 1px solid #2a2d35;
cursor: help;
cursor: pointer;
font-size: 0.7rem;
font-family: inherit;
}
.kinds-table th,
.kinds-table td {
text-align: left;
padding: 0.5rem;
border-bottom: 1px solid #2a2d35;
.json-toggle:hover {
color: #e6e6e6;
}
.kinds-table-actions {
display: flex;
gap: 0.5rem;
.json-key {
color: #9aa0ab;
}
.json-bracket {
color: #6b7280;
}
.json-summary {
color: #6b7280;
font-style: italic;
}
.json-string {
color: #6fcf97;
}
.json-number {
color: #6cb6ff;
}
.json-boolean {
color: #f5a742;
}
.json-null {
color: #6b7280;
font-style: italic;
}
.kind-editor {
@@ -352,7 +506,51 @@ button:disabled {
border: 1px solid #2a2d35;
border-radius: 8px;
padding: 1rem;
max-width: 480px;
max-width: 580px;
}
.profile-page {
display: flex;
flex-direction: column;
gap: 1.75rem;
}
.training-type-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.training-type-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
border: 1px solid #2a2d35;
border-radius: 6px;
padding: 0.75rem;
}
.training-type-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.75rem;
}
.training-type-description {
margin: 0;
font-size: 0.85rem;
color: #9aa0ab;
white-space: pre-wrap;
}
.training-type-meta {
margin: 0;
font-size: 0.8rem;
color: #6b7280;
}
.kind-editor textarea {