Skip to content

Commit 216653c

Browse files
authored
Merge pull request #353 from jimmykane/develop
Feat: Jumps, Mapbox Integration & Dynamic Map Clusters
2 parents e045991 + dd46039 commit 216653c

File tree

243 files changed

+13173
-4603
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

243 files changed

+13173
-4603
lines changed

.agent/rules/rules.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ trigger: always_on
1414
- **Dependency Injection**:
1515
- Supported: Constructor Injection (Legacy/Current).
1616
- Preferred for New Code: `inject()` function.
17-
- **Signals & Observables Naming**:
18-
- **STRICT RULE**: Do **NOT** use the `$` suffix for Observables or Signals (e.g., use `isLoading`, not `isLoading$`). This applies to all variables.
19-
- Reason: Consistency and readability, avoiding "Swiss cheese" code style.
17+
- **Signals & Observables Naming**:
18+
- **STRICT RULE**: **ALWAYS** use the `$` suffix for Observables (e.g., `user$`, `isLoading$`).
19+
- **Signals**: Do **NOT** use the `$` suffix for Signals (e.g., `isLoading`, `user`).
20+
- Reason: Clear distinction between streams (Observables) and reactive state (Signals).
2021

2122
### Firebase
2223
- Use **Modular SDK** (`@angular/fire` v20+, `firebase` v9+).

angular.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,8 @@
2828
"src/sitemap.xml"
2929
],
3030
"styles": [
31-
"./node_modules/material-design-icons-iconfont/dist/material-design-icons.css",
32-
"./node_modules/leaflet/dist/leaflet.css",
33-
"./node_modules/leaflet-fullscreen/dist/leaflet.fullscreen.css",
31+
"./node_modules/material-symbols/rounded.css",
32+
"./node_modules/mapbox-gl/dist/mapbox-gl.css",
3433
"./src/styles.scss"
3534
],
3635
"scripts": [],

firestore.indexes.json

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
{
22
"indexes": [
3+
{
4+
"collectionGroup": "changelogs",
5+
"queryScope": "COLLECTION",
6+
"fields": [
7+
{
8+
"fieldPath": "published",
9+
"order": "ASCENDING"
10+
},
11+
{
12+
"fieldPath": "date",
13+
"order": "DESCENDING"
14+
},
15+
{
16+
"fieldPath": "__name__",
17+
"order": "DESCENDING"
18+
}
19+
],
20+
"density": "SPARSE_ALL"
21+
},
322
{
423
"collectionGroup": "COROSAPIWorkoutQueue",
524
"queryScope": "COLLECTION",
@@ -1087,8 +1106,8 @@
10871106
]
10881107
},
10891108
{
1090-
"collectionGroup": "tokens",
1091-
"fieldPath": "dateRefreshed",
1109+
"collectionGroup": "system",
1110+
"fieldPath": "gracePeriodUntil",
10921111
"ttl": false,
10931112
"indexes": [
10941113
{
@@ -1111,7 +1130,7 @@
11111130
},
11121131
{
11131132
"collectionGroup": "tokens",
1114-
"fieldPath": "openId",
1133+
"fieldPath": "dateRefreshed",
11151134
"ttl": false,
11161135
"indexes": [
11171136
{
@@ -1134,7 +1153,7 @@
11341153
},
11351154
{
11361155
"collectionGroup": "tokens",
1137-
"fieldPath": "userName",
1156+
"fieldPath": "openId",
11381157
"ttl": false,
11391158
"indexes": [
11401159
{
@@ -1156,8 +1175,8 @@
11561175
]
11571176
},
11581177
{
1159-
"collectionGroup": "system",
1160-
"fieldPath": "gracePeriodUntil",
1178+
"collectionGroup": "tokens",
1179+
"fieldPath": "userName",
11611180
"ttl": false,
11621181
"indexes": [
11631182
{
@@ -1179,4 +1198,4 @@
11791198
]
11801199
}
11811200
]
1182-
}
1201+
}

firestore.rules

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@ service cloud.firestore {
159159
allow read: if isAdmin();
160160
allow write: if false;
161161
}
162+
163+
match /changelogs/{docId} {
164+
allow read: if resource.data.published == true || isAdmin();
165+
allow write: if isAdmin();
166+
}
162167
}
163168
}
164169

functions/package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

functions/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"@google-cloud/billing": "^5.1.1",
99
"@google-cloud/billing-budgets": "^6.1.1",
1010
"@google-cloud/tasks": "^6.2.1",
11-
"@sports-alliance/sports-lib": "^7.2.2",
11+
"@sports-alliance/sports-lib": "^8.0.5",
1212
"blob": "^0.1.0",
1313
"bs58": "^4.0.1",
1414
"cors": "^2.8.5",

functions/src/OAuth2.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ vi.mock('./utils', () => ({
109109
isCorsAllowed: vi.fn().mockReturnValue(true),
110110
setAccessControlHeadersOnResponse: vi.fn(),
111111
getUserIDFromFirebaseToken: vi.fn().mockResolvedValue('testUserID'),
112-
isProUser: vi.fn().mockResolvedValue(true),
112+
hasProAccess: vi.fn().mockResolvedValue(true),
113113
PRO_REQUIRED_MESSAGE: 'Service sync is a Pro feature.'
114114
}));
115115

functions/src/config.spec.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
3+
// Hoisted admin mock & dotenv noop
4+
const adminMock = vi.hoisted(() => ({
5+
instanceId: vi.fn(() => ({
6+
app: { options: { projectId: 'mock-project' } }
7+
}))
8+
}));
9+
10+
vi.mock('dotenv', () => ({ config: vi.fn() }));
11+
12+
vi.mock('firebase-admin', () => ({
13+
default: {
14+
instanceId: adminMock.instanceId
15+
},
16+
instanceId: adminMock.instanceId
17+
}));
18+
19+
const envBackup: NodeJS.ProcessEnv = { ...process.env };
20+
21+
describe('config.ts', () => {
22+
beforeEach(() => {
23+
vi.resetModules();
24+
Object.assign(process.env, {
25+
SUUNTOAPP_CLIENT_ID: 'suunto-id',
26+
SUUNTOAPP_CLIENT_SECRET: 'suunto-secret',
27+
SUUNTOAPP_SUBSCRIPTION_KEY: 'suunto-sub',
28+
COROSAPI_CLIENT_ID: 'coros-id',
29+
COROSAPI_CLIENT_SECRET: 'coros-secret',
30+
GARMINAPI_CLIENT_ID: 'garmin-id',
31+
GARMINAPI_CLIENT_SECRET: 'garmin-secret',
32+
});
33+
delete process.env.GCLOUD_PROJECT; // force fallback to admin.instanceId
34+
});
35+
36+
afterEach(() => {
37+
process.env = { ...envBackup };
38+
vi.clearAllMocks();
39+
});
40+
41+
it('returns configured values and derives cloudtasks defaults from admin project', async () => {
42+
const { config } = await import('./config');
43+
44+
expect(config.suuntoapp.client_id).toBe('suunto-id');
45+
expect(config.suuntoapp.subscription_key).toBe('suunto-sub');
46+
expect(config.corosapi.client_secret).toBe('coros-secret');
47+
expect(config.garminapi.client_id).toBe('garmin-id');
48+
49+
expect(config.cloudtasks.projectId).toBe('mock-project');
50+
expect(config.cloudtasks.serviceAccountEmail).toBe('[email protected]');
51+
expect(config.debug.bucketName).toBe('quantified-self-io-debug-files');
52+
});
53+
54+
it('throws when a required env var is missing', async () => {
55+
delete process.env.SUUNTOAPP_CLIENT_ID;
56+
const { config } = await import('./config');
57+
58+
expect(() => config.suuntoapp.client_id).toThrow(/Missing required environment variable: SUUNTOAPP_CLIENT_ID/);
59+
});
60+
});

functions/src/config.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,18 @@ interface CloudTasksConfig {
2727
serviceAccountEmail: string;
2828
}
2929

30+
interface DebugConfig {
31+
bucketName: string;
32+
}
33+
3034
interface AppConfig {
3135
suuntoapp: SuuntoAppConfig;
3236
corosapi: CorosApiConfig;
3337
garminapi: GarminApiConfig;
3438
cloudtasks: CloudTasksConfig;
35-
39+
debug: DebugConfig;
3640
}
3741

38-
39-
4042
function getEnvVar(name: string): string {
4143
const value = process.env[name];
4244
if (!value) {
@@ -74,4 +76,9 @@ export const config: AppConfig = {
7476
serviceAccountEmail: `${process.env.GCLOUD_PROJECT || admin.instanceId().app.options.projectId}@appspot.gserviceaccount.com`,
7577
};
7678
},
79+
get debug() {
80+
return {
81+
bucketName: 'quantified-self-io-debug-files',
82+
};
83+
},
7784
};

functions/src/coros/auth/wrapper.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ vi.mock('firebase-functions/v1', () => ({
2828
}));
2929

3030
vi.mock('../../utils', () => ({
31-
isProUser: vi.fn().mockResolvedValue(true),
31+
hasProAccess: vi.fn().mockResolvedValue(true),
3232
PRO_REQUIRED_MESSAGE: 'Service sync is a Pro feature.'
3333
}));
3434

@@ -52,7 +52,7 @@ describe('COROS Auth Wrapper', () => {
5252

5353
beforeEach(() => {
5454
vi.clearAllMocks();
55-
(utils.isProUser as any).mockResolvedValue(true);
55+
(utils.hasProAccess as any).mockResolvedValue(true);
5656

5757
context = {
5858
app: { appId: 'test-app' },
@@ -67,7 +67,7 @@ describe('COROS Auth Wrapper', () => {
6767
it('should return redirect URI for pro user', async () => {
6868
const result = await getCOROSAPIAuthRequestTokenRedirectURI(data, context);
6969

70-
expect(utils.isProUser).toHaveBeenCalledWith('testUserID');
70+
expect(utils.hasProAccess).toHaveBeenCalledWith('testUserID');
7171
expect(oauth2.getServiceOAuth2CodeRedirectAndSaveStateToUser).toHaveBeenCalledWith(
7272
'testUserID',
7373
SERVICE_NAME,
@@ -77,7 +77,7 @@ describe('COROS Auth Wrapper', () => {
7777
});
7878

7979
it('should throw error for non-pro user', async () => {
80-
(utils.isProUser as any).mockResolvedValue(false);
80+
(utils.hasProAccess as any).mockResolvedValue(false);
8181

8282
await expect(getCOROSAPIAuthRequestTokenRedirectURI(data, context))
8383
.rejects.toThrow('Service sync is a Pro feature.');

0 commit comments

Comments
 (0)