A workout log that you can access over wifi from your home gym. It includes an HTTP server with an embedded web app and an embedded database.
Home Gym requires authentication for access and supports multiple users. Users are authorized to access only their own data.
The app is in MVP state. It's been tested in Chrome on a MacBook Pro and on an Android device. Test cases included only weightlifting exercises.
- Run the build.sh script.
- Copy the binary from the
bin
folder to the computer that will run it. - On the host computer, create an environment variable named
HOMEGYM_DB_PATH
with the path to the directory where you want the database files to be stored. - Double-click the binary to run it. To stop it, press Ctrl+C.
- In a web browser, go to
http://<ip or hostname>/homegym/signup/
and create an account. - Go to
http://<ip or hostname>/homegym/login/
to log in. - On the Exercises page, define some exercises.
- On the Activities page, define some activities and add exercises to them.
- On the Home page, add workout entries for the activities.
Exercises that use sets and reps to express volume can optionally track failed attempts. Failed attempts is a useful metric for skill-based exercises such as the snatch and the clean and jerk.
flowchart LR
user((User))
db[(Database)]
dal[DAL]
auth[Authorizer]
wl[WorkoutLog]
api[" 🔒" REST API]
gw[ API Gateway]
wa[Web app]
user --> wa --> gw
gw -->|ID| auth --> dal --> db
api --> wa
auth -->|token|gw
gw -->|token|api --> wl --> dal
An HTTP server implemented in Go using BadgerDB for storage.
ExerciseType
defines exercises in terms intensity and volume. It is a factory for ExerciseInstance
objects that hold information about the performance of an exercise. ExerciseType
also validates the data.
A limited number of intensity types and volume types are used to define how to express the performance of an exercise. For example, squats use weight as intensity and sets and reps to indicate volume. Tempo runs can use heart rate zones as intensity and time as volume.
Exercise performance data is stored in the same format for all exercises. The VolumeConstraint
property indicates how volume data should be interpreted.
classDiagram
direction LR
class ExerciseType{
<<type>>
Name string
ID string
IntensityType string
VolumeType string
VolumeConstraint int
validate() error
validateInstance() error
CreateInstance() ExerciseInstance
}
class volumeTypes {
<< collection >>
id string
}
class intensityTypes {
<< collection >>
id string
}
class volumeConstraints {
<< collection >>
enum int
}
class ExerciseInstance{
<<type>>
TypeID string
Index int
Segments []ExerciseSegment
}
class ExerciseSegment{
Intensity float32
Volume [][]float32
}
ExerciseType --> ExerciseInstance :creates
ExerciseInstance o-- ExerciseSegment
ExerciseType ..> volumeTypes
ExerciseType ..> intensityTypes
ExerciseType ..> volumeConstraints
/_ cSpell:disable _/
sequenceDiagram
Actor user
box rgb(75,56,54) HTTP Server
Participant fs as fileserver
Participant gw as api gateway
Participant ah as authHandler
end
Participant wl as WorkoutLog
Participant a as auth
Participant db as dal/database
rect rgb(50, 50, 50)
Note left of user: Signup
user->>fs: GET /homegym/signup/
fs->>user: signup.html
user->>ah: form POST /homegym/signup
ah->>wl: call NewUser()
wl->>a: hash password
a->>wl: return
wl->>db: add user records
db->>wl: return
wl->>ah: return
ah->>user: 302 StatusFound redirect to /homegym/login/
end
rect rgb(50,50,50)
Note left of user: Login
user->>fs: GET /homegym/login/
fs->>user: login.html
user->>ah: form POST /homegym/login
ah->>a: call IssueToken
a->>db: read user records
db->>a: return password hash
a->>db: create session
db->>a: return
a->>a: schedule session deletion
a->>db: read private key
db->>a: return
a->>ah: return sessionID, JWT
ah->>user: 302 /homegym/home/ + sessionID, userID, and token cookies
end
rect rgb(50,50,50)
Note left of user: GET home page
user->>gw: GET /homegym/home/ + cookies
gw->>ah: call ValidateToken
ah->>db: read session
db->>ah: return session
Note over ah: session not expired
ah->>db: read private key
db->>ah: return
Note over ah: token is valid
ah->>gw: return refreshed token
gw->>user: index.html + token cookie
end
/_ cSpell:enable _/
/_ cSpell:disable _/
Key | Value passed to/from dal | Description |
---|---|---|
user:{id}#id | string | username used to log in |
user:{id}#email | string | email address |
user:{id}#phash | string | password hash |
user:{id}#version | int | version of user record |
user:{id}#event:{date}#id:{id}#activity:{id} | []byte | activity name |
user:{id}#event:{id}#exercise:{id}#index:{index}#instance | []byte | exercise instance |
user:{id}#activity:{id}#name | string | activity name |
user:{id}#activity:{id}#exercise:{id} | string | exercise type id |
user:{id}#exercise:{id}#type | []byte | exercise type value |
tokenkey:{keyID} | []byte | token key |
pepperkey:{keyID} | []byte | pepper key |
session:{sessionid}#userID:{userID}#expires | int64 | session expiration time |
user:{id}#activity:{id}#program:{id} | []byte | training program |
user:{id}#program:{id}#instance:{id} | []byte | A run of a program |
user:{id}#activity:{id}#active-program | string | {programID}:{instanceID} |
/_ cSpell:enable _/
Event Keys:
- primary sort by date
- enable query by date range
- enable query by activity
- enable query by exercise type
A single-page app implemented in Vue.js using Quasar components. You can probably tell that this is my first SPA.