git-fit 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/db/migrations/002_add_device_app_lineage.rb +29 -0
- data/lib/git-fit.rb +34 -0
- data/lib/git_fit/ability/computer.rb +14 -18
- data/lib/git_fit/ability/snapshot.rb +4 -7
- data/lib/git_fit/cli/ability.rb +0 -28
- data/lib/git_fit/export/json.rb +3 -0
- data/lib/git_fit/fit/decoder.js +1 -1
- data/lib/git_fit/fit/decoder.rb +9 -0
- data/lib/git_fit/import/apple_health.rb +2 -1
- data/lib/git_fit/source/base.rb +8 -0
- data/lib/git_fit/sync/base.rb +8 -0
- data/lib/git_fit/sync/garmin_base.rb +37 -0
- data/lib/git_fit/sync/strava.rb +15 -0
- data/lib/git_fit/sync/xingzhe.rb +8 -0
- data/lib/git_fit/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d61225229494679f4d293ddcfe384f79d9b6a2b7ea4007728fe45a2b47464506
|
|
4
|
+
data.tar.gz: 9e5934dc3360b9bf9a3dd925006379647f59140b3e2efb852b048ca3704d6fa8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4c1637ae4f4ac30c144703f4e9043b92278e7a30d6972c47c579740a7838207c1bedfd781724cfa74baaa4d40ecd5db4a22876f9e428e3492411bd85e18abb58
|
|
7
|
+
data.tar.gz: f51296343450139ecb8d8446a697648464c307902e05e25d76621af17cd3af2554b8e5b5eb324a5a5e005d05e681007684ced428e1513aeb1c2a508a96a21870
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Issue workouts#37: device / app / lineage columns
|
|
4
|
+
# device — physical hardware that produced the track file (e.g. "XOSS SPRINT")
|
|
5
|
+
# app — software the user directly interacted with (e.g. "Garmin Connect")
|
|
6
|
+
# lineage — denormalized "device → app → source" chain for query/display
|
|
7
|
+
Sequel.migration do
|
|
8
|
+
up do
|
|
9
|
+
alter_table(:activities) do
|
|
10
|
+
add_column :device, String
|
|
11
|
+
add_column :app, String
|
|
12
|
+
add_column :lineage, String
|
|
13
|
+
add_index :device
|
|
14
|
+
add_index :app
|
|
15
|
+
add_index :lineage
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
down do
|
|
20
|
+
alter_table(:activities) do
|
|
21
|
+
drop_index :lineage
|
|
22
|
+
drop_index :app
|
|
23
|
+
drop_index :device
|
|
24
|
+
drop_column :lineage
|
|
25
|
+
drop_column :app
|
|
26
|
+
drop_column :device
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/git-fit.rb
CHANGED
|
@@ -7,6 +7,22 @@ require 'fileutils'
|
|
|
7
7
|
module GitFit
|
|
8
8
|
SPORT_CATEGORIES = %w[run ride hike walk swim ski workout other].freeze
|
|
9
9
|
|
|
10
|
+
# Per-source default app label (used when an adapter doesn't set `app` itself).
|
|
11
|
+
# Dynamic sources (strava per-device, apple_health per sourceName) set `app` explicitly.
|
|
12
|
+
DEFAULT_APPS = {
|
|
13
|
+
'garmin' => 'Garmin Connect',
|
|
14
|
+
'garmin_cn' => 'Garmin Connect',
|
|
15
|
+
'keep' => 'Keep',
|
|
16
|
+
'igpsport' => 'iGPSPORT',
|
|
17
|
+
'xoss' => 'XOSS',
|
|
18
|
+
'codoon' => 'Codoon',
|
|
19
|
+
'joyrun' => 'Joyrun',
|
|
20
|
+
'xingzhe' => 'XingZhe',
|
|
21
|
+
'strava' => 'Strava',
|
|
22
|
+
'2bulu' => '2bulu',
|
|
23
|
+
'apple_health' => 'Apple Health',
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
10
26
|
@registered_configs = []
|
|
11
27
|
|
|
12
28
|
class << self
|
|
@@ -37,6 +53,24 @@ module GitFit
|
|
|
37
53
|
}
|
|
38
54
|
end
|
|
39
55
|
|
|
56
|
+
# Resolved `app` label for an activity: explicit value wins, else DEFAULT_APPS, else raw source.
|
|
57
|
+
def default_app(app, source)
|
|
58
|
+
return app unless app.nil? || app.to_s.empty?
|
|
59
|
+
DEFAULT_APPS.fetch(source, source)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Lineage chain "device → app → source"; blank parts dropped, case-insensitive
|
|
63
|
+
# dedup so app==source collapses (Keep + keep → "Keep"). Display-only, never
|
|
64
|
+
# used for dedup decisions (workouts#37).
|
|
65
|
+
def build_lineage(device, app, source)
|
|
66
|
+
seen = []
|
|
67
|
+
[device, app, source].each do |part|
|
|
68
|
+
next if part.nil? || part.to_s.empty?
|
|
69
|
+
seen << part unless seen.any? { |s| s.casecmp?(part) }
|
|
70
|
+
end
|
|
71
|
+
seen.join(' → ')
|
|
72
|
+
end
|
|
73
|
+
|
|
40
74
|
def format_time(seconds)
|
|
41
75
|
return nil unless seconds
|
|
42
76
|
hours = seconds / 3600
|
|
@@ -44,13 +44,11 @@ module GitFit
|
|
|
44
44
|
def normalize_as_of(as_of)
|
|
45
45
|
return nil if as_of.nil? || as_of.to_s.empty?
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
unless valid
|
|
49
|
-
raise ArgumentError, "Ability: invalid --as-of '#{as_of}' (expected YYYY-MM-DD)\n" \
|
|
50
|
-
' Fix: git fit ability compute --as-of 2025-03-15'
|
|
51
|
-
end
|
|
52
|
-
|
|
47
|
+
Date.iso8601(as_of.to_s)
|
|
53
48
|
as_of
|
|
49
|
+
rescue Date::Error
|
|
50
|
+
raise ArgumentError, "Ability: invalid --as-of '#{as_of}' (expected YYYY-MM-DD)\n" \
|
|
51
|
+
' Fix: git fit ability compute --as-of 2025-03-15'
|
|
54
52
|
end
|
|
55
53
|
|
|
56
54
|
def load_rows
|
|
@@ -99,21 +97,19 @@ module GitFit
|
|
|
99
97
|
# --- fitness layer ---
|
|
100
98
|
|
|
101
99
|
def fitness_snapshots(rows)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
Snapshot::PERIOD_TYPES.
|
|
109
|
-
|
|
100
|
+
buckets = Snapshot::PERIOD_TYPES.to_h { |type| [type, bucketize(rows, type)] }
|
|
101
|
+
base = buckets.flat_map do |type, per|
|
|
102
|
+
per.map do |key, period_rows|
|
|
103
|
+
[[type, key], fitness_fields(type, key, period_rows)]
|
|
104
|
+
end
|
|
105
|
+
end.to_h
|
|
106
|
+
Snapshot::PERIOD_TYPES.flat_map do |type|
|
|
107
|
+
buckets[type].keys.sort.map do |key|
|
|
110
108
|
fields = base[[type, key]]
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
out << fields.merge('trend_vs_prev' => prev ? trend_fields(fields, prev) : nil)
|
|
109
|
+
prev = Snapshot.previous_key(type, key)&.then { |pk| base[[type, pk]] }
|
|
110
|
+
fields.merge('trend_vs_prev' => prev ? trend_fields(fields, prev) : nil)
|
|
114
111
|
end
|
|
115
112
|
end
|
|
116
|
-
out
|
|
117
113
|
end
|
|
118
114
|
|
|
119
115
|
def bucketize(rows, type)
|
|
@@ -84,21 +84,18 @@ module GitFit
|
|
|
84
84
|
format('%<year>04d-W%<week>02d', year: date.cwyear, week: date.cweek)
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
-
# Distinct ISO weeks overlapping [start, end]
|
|
87
|
+
# Distinct ISO weeks overlapping [start, end] (cwyear*53+cweek is monotonic;
|
|
88
|
+
# cweek never exceeds 53).
|
|
88
89
|
def week_count(start_date, end_date)
|
|
89
|
-
|
|
90
|
-
(start_date..end_date).each { |d| keys[week_key_of_date(d)] = true }
|
|
91
|
-
keys.size
|
|
90
|
+
(end_date.cwyear * 53 + end_date.cweek) - (start_date.cwyear * 53 + start_date.cweek) + 1
|
|
92
91
|
end
|
|
93
92
|
|
|
94
93
|
# Sample-count confidence: <3 low (prompt-excluded), 3-9 medium, >=10 high.
|
|
95
94
|
def confidence_for(count)
|
|
96
95
|
if count < 3
|
|
97
96
|
'low'
|
|
98
|
-
elsif count < 10
|
|
99
|
-
'medium'
|
|
100
97
|
else
|
|
101
|
-
'high'
|
|
98
|
+
count < 10 ? 'medium' : 'high'
|
|
102
99
|
end
|
|
103
100
|
end
|
|
104
101
|
|
data/lib/git_fit/cli/ability.rb
CHANGED
|
@@ -29,33 +29,5 @@ module GitFit
|
|
|
29
29
|
rescue StandardError => e
|
|
30
30
|
say_status :error, "Ability compute failed: #{e.message}", :red
|
|
31
31
|
end
|
|
32
|
-
|
|
33
|
-
desc 'show', 'Print a single ability snapshot by period key'
|
|
34
|
-
option :period, type: :string, required: true, desc: 'Period key, e.g. 2025-03 / 2025-Q1 / 2025-W12 / 2025 / all'
|
|
35
|
-
option :output, type: :string, aliases: '-o', desc: 'ability.json path to read'
|
|
36
|
-
|
|
37
|
-
def show
|
|
38
|
-
path = options[:output] || git_fit_config.export_config.dig('ability', 'path') || 'site/ability.json'
|
|
39
|
-
doc = JSON.parse(File.read(path))
|
|
40
|
-
snapshot = find_snapshot(doc, options[:period])
|
|
41
|
-
if snapshot
|
|
42
|
-
puts JSON.pretty_generate(snapshot)
|
|
43
|
-
else
|
|
44
|
-
say_status :warn, "Ability: no snapshot for period '#{options[:period]}' in #{path}", :yellow
|
|
45
|
-
end
|
|
46
|
-
rescue StandardError => e
|
|
47
|
-
say_status :error, "Ability show failed: #{e.message}", :red
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
def find_snapshot(doc, period_key)
|
|
53
|
-
candidates = (doc.dig('fitness', 'snapshots') || []) +
|
|
54
|
-
(doc['sports'] || {}).values.flat_map { |s| s['snapshots'] || [] }
|
|
55
|
-
matches = candidates.select { |s| s['period_key'] == period_key }
|
|
56
|
-
return matches.first if matches.one?
|
|
57
|
-
|
|
58
|
-
matches # both fitness and sport layers may carry the key — print all
|
|
59
|
-
end
|
|
60
32
|
end
|
|
61
33
|
end
|
data/lib/git_fit/export/json.rb
CHANGED
|
@@ -85,6 +85,9 @@ module GitFit
|
|
|
85
85
|
elevation_max: a[:elevation_max_terrain] || a[:elevation_max],
|
|
86
86
|
steps: a[:steps],
|
|
87
87
|
source: a[:source],
|
|
88
|
+
device: a[:device],
|
|
89
|
+
app: a[:app],
|
|
90
|
+
lineage: a[:lineage],
|
|
88
91
|
}
|
|
89
92
|
if a[:divisions]
|
|
90
93
|
base[:divisions] = ::JSON.parse(a[:divisions])
|