tangled
alpha
login
or
join now
jack.orbyt.video
/
VTGymTracker
0
fork
atom
my swift app for VT gyms
gymtracker.jackhannon.net
0
fork
atom
overview
issues
pulls
pipelines
Data Flow documentation
jack.orbyt.video
1 month ago
142a6390
151380b9
+267
1 changed file
expand all
collapse all
unified
split
API_DATA_FLOW.md
+267
API_DATA_FLOW.md
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
# API Data Flow Explainer
2
+
3
+
This document explains exactly how the VT Gym Tracker app retrieves and processes gym occupancy data from the Virginia Tech RecSports API.
4
+
5
+
## API Endpoint
6
+
7
+
**URL:** `https://connect.recsports.vt.edu/FacilityOccupancy/GetFacilityData`
8
+
9
+
**Method:** `POST`
10
+
11
+
**Content-Type:** `application/x-www-form-urlencoded`
12
+
13
+
**Note:** This is not a REST API endpoint. It requires POST requests with form-encoded data.
14
+
15
+
## Request Format
16
+
17
+
Each request requires two parameters in the request body:
18
+
19
+
```
20
+
facilityId={UUID}&occupancyDisplayType={UUID}
21
+
```
22
+
23
+
### Facility IDs
24
+
25
+
The app tracks three facilities:
26
+
27
+
- **McComas Hall:** `da73849e-434d-415f-975a-4f9e799b9c39`
28
+
- **War Memorial Hall:** `55069633-b56e-43b7-a68a-64d79364988d`
29
+
- **Bouldering Wall:** `da838218-ae53-4c6f-b744-2213299033fc`
30
+
31
+
### Occupancy Display Type
32
+
33
+
A constant UUID required by the API: `00000000-0000-0000-0000-000000004490`
34
+
35
+
### Example Request
36
+
37
+
```http
38
+
POST /FacilityOccupancy/GetFacilityData HTTP/1.1
39
+
Host: connect.recsports.vt.edu
40
+
Content-Type: application/x-www-form-urlencoded
41
+
42
+
facilityId=da73849e-434d-415f-975a-4f9e799b9c39&occupancyDisplayType=00000000-0000-0000-0000-000000004490
43
+
```
44
+
45
+
## Response Format
46
+
47
+
The API returns **HTML**, not JSON. The HTML contains occupancy data in `data-*` attributes on HTML elements.
48
+
49
+
### Example Response Structure
50
+
51
+
```html
52
+
<canvas class="occupancy-chart"
53
+
data-occupancy="275"
54
+
data-remaining="325">
55
+
<!-- Chart rendering code -->
56
+
</canvas>
57
+
```
58
+
59
+
The app extracts two values:
60
+
- `data-occupancy`: Current number of people in the facility
61
+
- `data-remaining`: Remaining capacity
62
+
63
+
## Data Flow
64
+
65
+
### 1. Request Initiation
66
+
67
+
**Entry Point:** `GymService.fetchAllGymOccupancy()`
68
+
69
+
The app fetches data for all three facilities concurrently using Swift's `async let`:
70
+
71
+
```swift
72
+
async let mc = fetchOne(facilityId: Constants.mcComasFacilityId)
73
+
async let wm = fetchOne(facilityId: Constants.warMemorialFacilityId)
74
+
async let bw = fetchOne(facilityId: Constants.boulderingWallFacilityId)
75
+
let (m, w, b) = await (mc, wm, bw)
76
+
```
77
+
78
+
This parallel fetching improves performance by requesting all facilities simultaneously rather than sequentially.
79
+
80
+
### 2. HTTP Request (`GymOccupancyFetcher.fetchOne`)
81
+
82
+
For each facility:
83
+
84
+
1. **Create URLRequest:**
85
+
```swift
86
+
var req = URLRequest(url: Constants.facilityDataAPIURL)
87
+
req.httpMethod = "POST"
88
+
req.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
89
+
```
90
+
91
+
2. **Build Request Body:**
92
+
```swift
93
+
req.httpBody = "facilityId=\(facilityId)&occupancyDisplayType=\(Constants.occupancyDisplayType)".data(using: .utf8)
94
+
```
95
+
96
+
3. **Execute Request:**
97
+
```swift
98
+
let (data, response) = try await urlSession.data(for: req)
99
+
```
100
+
101
+
4. **Validate Response:**
102
+
- Check HTTP status code is 200-299
103
+
- Verify data exists
104
+
- Convert data to UTF-8 string
105
+
106
+
### 3. URLSession Configuration
107
+
108
+
The app uses a custom `URLSession` configuration:
109
+
110
+
```swift
111
+
let c = URLSessionConfiguration.default
112
+
c.timeoutIntervalForRequest = 30 // 30 second timeout
113
+
c.requestCachePolicy = .reloadIgnoringLocalCacheData // Always fetch fresh data
114
+
```
115
+
116
+
**Why no caching?** Occupancy data changes frequently. Caching would show stale counts.
117
+
118
+
### 4. HTML Parsing (`OccupancyHTMLParser.parse`)
119
+
120
+
The HTML response is parsed using regex to extract the `data-occupancy` and `data-remaining` attributes:
121
+
122
+
**Pattern:** `data-occupancy="([0-9]+)"` and `data-remaining="([0-9]+)"`
123
+
124
+
**Process:**
125
+
1. Search HTML string for attribute pattern
126
+
2. Extract captured group (the numeric value)
127
+
3. Convert to `Int`
128
+
4. Return tuple: `(occupancy: Int, remaining: Int)?`
129
+
130
+
**Why regex instead of HTML parser?**
131
+
- No external dependencies required
132
+
- Simple attribute extraction doesn't need full HTML parsing
133
+
- Lightweight and fast
134
+
135
+
### 5. Data Processing (`GymService.storeAndNotify`)
136
+
137
+
After parsing:
138
+
139
+
1. **Update Published Properties:**
140
+
```swift
141
+
self.mcComasOccupancy = mcComasData?.occupancy
142
+
self.warMemorialOccupancy = warMemorialData?.occupancy
143
+
self.boulderingWallOccupancy = boulderingWallData?.occupancy
144
+
```
145
+
These `@Published` properties trigger SwiftUI view updates.
146
+
147
+
2. **Store in App Group UserDefaults:**
148
+
```swift
149
+
let sharedDefaults = UserDefaults(suiteName: Constants.appGroupID)
150
+
sharedDefaults.set(mc, forKey: "mcComasOccupancy")
151
+
```
152
+
This allows widgets and the watch app to access the data.
153
+
154
+
3. **Notify Widgets:**
155
+
```swift
156
+
WidgetCenter.shared.reloadAllTimelines()
157
+
```
158
+
Widgets are immediately notified to refresh with new data.
159
+
160
+
## Refresh Schedule
161
+
162
+
### Foreground (App Active)
163
+
164
+
- **Interval:** 30 seconds
165
+
- **Trigger:** Timer fires when app is active (`UIApplication.didBecomeActiveNotification`)
166
+
- **Stops:** When app backgrounds (`UIApplication.willResignActiveNotification`)
167
+
168
+
**Why 30 seconds?** Balances data freshness with battery and network usage.
169
+
170
+
### Background/Widgets
171
+
172
+
- **Widget Timeline:** Requests refresh every 15 minutes (WidgetKit managed)
173
+
- **Widgets read from:** App Group UserDefaults (shared storage)
174
+
175
+
## Error Handling
176
+
177
+
### Network Failures
178
+
179
+
1. **All Facilities Fail:**
180
+
- `isOnline` set to `false`
181
+
- Retry scheduled after 60 seconds
182
+
- Prevents immediate retry loop
183
+
184
+
2. **Partial Success:**
185
+
- If any facility succeeds, `isOnline` remains `true`
186
+
- Only successful facilities update their values
187
+
- Failed facilities retain previous values
188
+
189
+
### Parsing Failures
190
+
191
+
- If HTML parsing fails, `fetchOne` returns `nil`
192
+
- The facility's occupancy remains unchanged
193
+
- No error is thrown; the app continues with available data
194
+
195
+
## Data Storage
196
+
197
+
### In-App (`GymService`)
198
+
199
+
- `@Published` properties for SwiftUI binding
200
+
- Updated on main thread (`@MainActor`)
201
+
202
+
### Shared Storage (App Group)
203
+
204
+
**App Group ID:** `group.VTGymApp.D8VXFBV8SJ`
205
+
206
+
**Keys:**
207
+
- `mcComasOccupancy` (Int)
208
+
- `warMemorialOccupancy` (Int)
209
+
- `boulderingWallOccupancy` (Int)
210
+
- `lastFetchDate` (Date)
211
+
212
+
**Used by:**
213
+
- Main iOS app
214
+
- Widget extensions
215
+
- Watch app
216
+
217
+
## Testing/Debugging Override
218
+
219
+
The app supports custom occupancy values for testing:
220
+
221
+
```swift
222
+
@Published var useCustomOccupancy: Bool = false
223
+
@Published var customMcComasOccupancy: Int? = 275
224
+
@Published var customWarMemorialOccupancy: Int? = 1025
225
+
@Published var customBoulderingWallOccupancy: Int? = 6
226
+
```
227
+
228
+
When enabled, these values override API data. Useful for:
229
+
- Testing UI without network
230
+
- Debugging display logic
231
+
- Demonstrating app functionality
232
+
233
+
## Complete Flow Diagram
234
+
235
+
```
236
+
ContentView.onAppear
237
+
↓
238
+
GymService.fetchAllGymOccupancy()
239
+
↓
240
+
GymOccupancyFetcher.fetchAll()
241
+
├─→ fetchOne(McComas) ──┐
242
+
├─→ fetchOne(War Memorial) ──┤ Concurrent HTTP POST requests
243
+
└─→ fetchOne(Bouldering) ────┘
244
+
↓
245
+
POST https://connect.recsports.vt.edu/FacilityOccupancy/GetFacilityData
246
+
Body: facilityId={UUID}&occupancyDisplayType={UUID}
247
+
↓
248
+
HTML Response with data-occupancy and data-remaining attributes
249
+
↓
250
+
OccupancyHTMLParser.parse(html)
251
+
↓
252
+
Regex extraction: data-occupancy="([0-9]+)" and data-remaining="([0-9]+)"
253
+
↓
254
+
Return (occupancy: Int, remaining: Int)?
255
+
↓
256
+
GymService.storeAndNotify()
257
+
├─→ Update @Published properties (SwiftUI updates)
258
+
├─→ Store in App Group UserDefaults (widgets/watch access)
259
+
└─→ WidgetCenter.reloadAllTimelines() (notify widgets)
260
+
```
261
+
262
+
## Key Files
263
+
264
+
- **`GymOccupancyFetcher.swift`:** HTTP requests and concurrent fetching
265
+
- **`OccupancyHTMLParser.swift`:** HTML parsing via regex
266
+
- **`GymService.swift`:** Data management and refresh scheduling
267
+
- **`Constants.swift`:** API URLs, facility IDs, and configuration