Skip to content

Commit 82afd9f

Browse files
prazgaitisclaude
andauthored
Remove tracking kind, fix bonus classifications per forum rules (#243)
* Remove tracking kind, classify non-streak activities as bonus Based on forum rules review: mindfulness, skiing, workout with a friend, and retro abs are all bonus activities (not streak-eligible, not competitive). Remove the tracking kind entirely — four kinds remain: core, challenge, bonus, penalty. Also improve heuristic: non-streak + non-unit-based activities now default to bonus. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Rename challenge kind to special, matching forum rules terminology The forum rules use three tiers: core (streak + category leaderboard), special (streak-eligible workouts, no category leaderboard), and bonus (not streak-eligible). Rename challenge → special to match. Four kinds: core, special, bonus, penalty. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 65ee018 commit 82afd9f

5 files changed

Lines changed: 54 additions & 37 deletions

File tree

apps/web/components/admin/admin-activity-types-table.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ export function AdminActivityTypesTable({
159159
isNegative: createNegative,
160160
availableInFinalDays: createAvailableInFinalDays || undefined,
161161
categoryId: createCategoryId ? (createCategoryId as Id<"categories">) : undefined,
162-
kind: (createKind || undefined) as "core" | "challenge" | "bonus" | "penalty" | "tracking" | undefined,
162+
kind: (createKind || undefined) as "core" | "special" | "bonus" | "penalty" | undefined,
163163
displayOrder: Number.isFinite(parsedDisplayOrder) && createDisplayOrder !== "" ? parsedDisplayOrder : undefined,
164164
});
165165
setCreateName("");
@@ -256,7 +256,7 @@ export function AdminActivityTypesTable({
256256
availableInFinalDays: editAvailableInFinalDays || undefined,
257257
bonusThresholds: editThresholds,
258258
categoryId: editCategoryId ? (editCategoryId as Id<"categories">) : undefined,
259-
kind: (editKind || undefined) as "core" | "challenge" | "bonus" | "penalty" | "tracking" | undefined,
259+
kind: (editKind || undefined) as "core" | "special" | "bonus" | "penalty" | undefined,
260260
displayOrder: Number.isFinite(parsedDisplayOrder) && editDisplayOrder !== "" ? parsedDisplayOrder : undefined,
261261
});
262262
setEditingId(null);
@@ -422,10 +422,9 @@ export function AdminActivityTypesTable({
422422
>
423423
<option value=""></option>
424424
<option value="core">Core</option>
425-
<option value="challenge">Challenge</option>
425+
<option value="special">Special</option>
426426
<option value="bonus">Bonus</option>
427427
<option value="penalty">Penalty</option>
428-
<option value="tracking">Tracking</option>
429428
</select>
430429
</div>
431430
<div className="w-20">
@@ -655,11 +654,10 @@ export function AdminActivityTypesTable({
655654
>
656655
<option value=""></option>
657656
<option value="core">Core</option>
658-
<option value="challenge">Challenge</option>
657+
<option value="special">Special</option>
659658
<option value="bonus">Bonus</option>
660659
<option value="penalty">Penalty</option>
661-
<option value="tracking">Tracking</option>
662-
</select>
660+
</select>
663661
</div>
664662
<div className="w-20">
665663
<label className="mb-1 block text-[10px] text-zinc-500" title="Controls order in the logging menu (lower = first)">

apps/web/tests/api/activity-type-kind.test.ts

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ describe("Activity type kind field", () => {
6464
it("updateActivityType updates kind", async () => {
6565
const id = await createTestActivityType(t, challengeId, {
6666
name: "Bonus",
67-
kind: "challenge",
67+
kind: "special",
6868
});
6969

7070
await tWithAuth.mutation(api.mutations.activityTypes.updateActivityType, {
@@ -183,7 +183,7 @@ describe("Activity type kind field", () => {
183183
}
184184
});
185185

186-
it("classifies tracking activities by name match", async () => {
186+
it("classifies mindfulness and non-streak activities as bonus", async () => {
187187
await createTestActivityType(t, challengeId, {
188188
name: "10 Days of Mindfulness (Days 1-9)",
189189
scoringConfig: { type: "completion", fixedPoints: 1 },
@@ -197,18 +197,37 @@ describe("Activity type kind field", () => {
197197
isNegative: false,
198198
maxPerChallenge: 1,
199199
});
200+
await createTestActivityType(t, challengeId, {
201+
name: "Skiing Full Day",
202+
scoringConfig: { type: "unit_based", pointsPerUnit: 35, unit: "full days" },
203+
contributesToStreak: false,
204+
isNegative: false,
205+
});
206+
await createTestActivityType(t, challengeId, {
207+
name: "Workout with a Friend",
208+
scoringConfig: { type: "completion", fixedPoints: 25 },
209+
contributesToStreak: false,
210+
isNegative: false,
211+
maxPerChallenge: 1,
212+
});
213+
await createTestActivityType(t, challengeId, {
214+
name: "Retro Abs Bonus",
215+
scoringConfig: { type: "completion", fixedPoints: 15 },
216+
contributesToStreak: false,
217+
isNegative: false,
218+
});
200219

201220
const result = await tWithAuth.mutation(
202221
api.mutations.activityTypes.backfillKind,
203222
{ challengeId },
204223
);
205224

206225
for (const r of result.results) {
207-
expect(r.kind).toBe("tracking");
226+
expect(r.kind).toBe("bonus");
208227
}
209228
});
210229

211-
it("classifies challenge activities (completion, tiered, non-standard units)", async () => {
230+
it("classifies special activities (completion, tiered, non-standard units)", async () => {
212231
await createTestActivityType(t, challengeId, {
213232
name: "Hotel Room Workout",
214233
scoringConfig: { type: "completion", fixedPoints: 50 },
@@ -239,11 +258,11 @@ describe("Activity type kind field", () => {
239258
);
240259

241260
for (const r of result.results) {
242-
expect(r.kind).toBe("challenge");
261+
expect(r.kind).toBe("special");
243262
}
244263
});
245264

246-
it("classifies unit_based with maxPerChallenge as challenge, not core", async () => {
265+
it("classifies unit_based with maxPerChallenge as special, not core", async () => {
247266
await createTestActivityType(t, challengeId, {
248267
name: "The Max",
249268
scoringConfig: { type: "unit_based", pointsPerUnit: 25, unit: "circuits", maxUnits: 3 },
@@ -257,7 +276,7 @@ describe("Activity type kind field", () => {
257276
{ challengeId },
258277
);
259278

260-
expect(result.results[0]).toMatchObject({ name: "The Max", kind: "challenge" });
279+
expect(result.results[0]).toMatchObject({ name: "The Max", kind: "special" });
261280
});
262281

263282
it("skips rows that already have kind set", async () => {
@@ -266,7 +285,7 @@ describe("Activity type kind field", () => {
266285
scoringConfig: { type: "unit_based", pointsPerUnit: 8, unit: "miles" },
267286
contributesToStreak: true,
268287
isNegative: false,
269-
kind: "tracking", // manually set to something unusual
288+
kind: "bonus", // manually set to something different than heuristic would pick
270289
});
271290

272291
const result = await tWithAuth.mutation(
@@ -278,7 +297,7 @@ describe("Activity type kind field", () => {
278297
expect(result.updated).toBe(0);
279298
expect(result.results[0]).toMatchObject({
280299
name: "Custom Run",
281-
kind: "tracking",
300+
kind: "bonus",
282301
skipped: true,
283302
});
284303
});
@@ -345,7 +364,7 @@ describe("Activity type kind field", () => {
345364
Swimming: "core",
346365
"The Hunt Bonus": "bonus",
347366
Overindulge: "penalty",
348-
"The Murph": "challenge",
367+
"The Murph": "special",
349368
});
350369
});
351370
});

packages/backend/mutations/activityTypes.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@ const bonusThresholdsArg = v.optional(
1515
const kindArg = v.optional(
1616
v.union(
1717
v.literal("core"),
18-
v.literal("challenge"),
18+
v.literal("special"),
1919
v.literal("bonus"),
2020
v.literal("penalty"),
21-
v.literal("tracking"),
2221
)
2322
);
2423

@@ -235,11 +234,12 @@ export const backfillKind = mutation({
235234
"hunt week bonus",
236235
"category leader bonus",
237236
"mini-game bonus",
238-
]);
239-
240-
const TRACKING_NAMES = new Set([
241237
"10 days of mindfulness (days 1-9)",
242238
"10 days of mindfulness (day 10)",
239+
"workout with a friend",
240+
"retro abs bonus",
241+
"skiing full day",
242+
"skiing half day",
243243
]);
244244

245245
const results: { name: string; kind: string; skipped?: boolean }[] = [];
@@ -251,20 +251,24 @@ export const backfillKind = mutation({
251251
}
252252

253253
const nameLower = at.name.toLowerCase();
254-
let kind: "core" | "challenge" | "bonus" | "penalty" | "tracking";
254+
let kind: "core" | "special" | "bonus" | "penalty";
255255

256256
if (at.isNegative) {
257257
kind = "penalty";
258258
} else if (BONUS_NAMES.has(nameLower)) {
259259
kind = "bonus";
260-
} else if (TRACKING_NAMES.has(nameLower)) {
261-
kind = "tracking";
262260
} else if (
263261
at.scoringConfig?.type === "variable" ||
264262
(at.scoringConfig?.type === "fixed" && at.scoringConfig?.basePoints === 0)
265263
) {
266264
// Variable scoring = system-awarded bonuses
267265
kind = "bonus";
266+
} else if (
267+
!at.contributesToStreak &&
268+
!at.scoringConfig?.type?.includes("unit_based")
269+
) {
270+
// Non-streak, non-unit activities are bonuses
271+
kind = "bonus";
268272
} else if (
269273
at.scoringConfig?.type === "unit_based" &&
270274
["miles", "kilometers", "minutes"].includes(at.scoringConfig?.unit) &&
@@ -273,8 +277,8 @@ export const backfillKind = mutation({
273277
// Repeatable distance/duration activities = core fitness
274278
kind = "core";
275279
} else {
276-
// Everything else: completion workouts, tiered challenges, etc.
277-
kind = "challenge";
280+
// Everything else: special workouts (completion, tiered, non-standard units)
281+
kind = "special";
278282
}
279283

280284
if (!args.dryRun) {

packages/backend/mutations/apiMutations.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -846,10 +846,9 @@ export const createActivityTypeForUser = internalMutation({
846846
kind: v.optional(
847847
v.union(
848848
v.literal("core"),
849-
v.literal("challenge"),
849+
v.literal("special"),
850850
v.literal("bonus"),
851851
v.literal("penalty"),
852-
v.literal("tracking"),
853852
)
854853
),
855854
},
@@ -894,10 +893,9 @@ export const updateActivityTypeForUser = internalMutation({
894893
kind: v.optional(
895894
v.union(
896895
v.literal("core"),
897-
v.literal("challenge"),
896+
v.literal("special"),
898897
v.literal("bonus"),
899898
v.literal("penalty"),
900-
v.literal("tracking"),
901899
)
902900
),
903901
},

packages/backend/schema.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,14 @@ export default defineSchema({
7373
isNegative: v.boolean(),
7474
categoryId: v.optional(v.id("categories")),
7575
// Semantic classification orthogonal to categoryId (which drives Category Leader leaderboard).
76-
// core = standard fitness activities, challenge = special workouts,
77-
// bonus = system-awarded bonuses, penalty = negative-scoring, tracking = non-competitive tracking
76+
// core = standard fitness activities, special = special workouts (streak-eligible, not in category leaderboard),
77+
// bonus = system-awarded or non-competitive bonuses (not streak-eligible), penalty = negative-scoring
7878
kind: v.optional(
7979
v.union(
8080
v.literal("core"),
81-
v.literal("challenge"),
81+
v.literal("special"),
8282
v.literal("bonus"),
8383
v.literal("penalty"),
84-
v.literal("tracking"),
8584
),
8685
),
8786
// Threshold bonuses - auto-apply bonus points when metrics exceed thresholds
@@ -384,10 +383,9 @@ export default defineSchema({
384383
kind: v.optional(
385384
v.union(
386385
v.literal("core"),
387-
v.literal("challenge"),
386+
v.literal("special"),
388387
v.literal("bonus"),
389388
v.literal("penalty"),
390-
v.literal("tracking"),
391389
),
392390
),
393391
bonusThresholds: v.optional(

0 commit comments

Comments
 (0)