From 6fa33c9f7bb92ca8db56ff0ed9667ef888c8323f Mon Sep 17 00:00:00 2001 From: Dylan Garvis Date: Sun, 2 Aug 2026 14:26:24 -0400 Subject: [PATCH] feat(admin): show group schedule status --- .../app/admin/(console)/groups/[groupId]/page.tsx | 2 +- apps/web/src/app/admin/(console)/groups/page.tsx | 15 +++++++++++---- apps/web/src/lib/group-schedule.test.ts | 7 +++++++ apps/web/src/lib/group-schedule.ts | 5 +++++ design/us-019-admin-group-management.md | 2 +- design/us-020-scheduled-group-access.md | 1 + 6 files changed, 26 insertions(+), 6 deletions(-) diff --git a/apps/web/src/app/admin/(console)/groups/[groupId]/page.tsx b/apps/web/src/app/admin/(console)/groups/[groupId]/page.tsx index 47bbb93..839b133 100644 --- a/apps/web/src/app/admin/(console)/groups/[groupId]/page.tsx +++ b/apps/web/src/app/admin/(console)/groups/[groupId]/page.tsx @@ -112,7 +112,7 @@ export default async function GroupPage({ -
+

Weekly access schedule

When Minecraft access is enabled, members may log in only during these recurring UTC windows. Existing sessions are not disconnected when a window ends.

diff --git a/apps/web/src/app/admin/(console)/groups/page.tsx b/apps/web/src/app/admin/(console)/groups/page.tsx index 2157f22..ddf64d9 100644 --- a/apps/web/src/app/admin/(console)/groups/page.tsx +++ b/apps/web/src/app/admin/(console)/groups/page.tsx @@ -1,10 +1,11 @@ -import { groups, userGroupMemberships, users } from "@minecraft-account-manager/database"; +import { groupAccessWindows, groups, userGroupMemberships, users } from "@minecraft-account-manager/database"; import { asc, count, desc } from "drizzle-orm"; import Link from "next/link"; import { AdminModalForm } from "@/components/admin-modal-form"; import { GroupPolicyControl } from "@/components/group-policy-control"; import { db } from "@/lib/database"; import { effectiveGroupMemberCount } from "@/lib/group-management"; +import { groupScheduleStatus } from "@/lib/group-schedule"; import { createGroup, setGroupAccess, setGroupAnonymizedNetworkAccess } from "./actions"; const errors: Record = { @@ -26,11 +27,15 @@ export const dynamic = "force-dynamic"; export default async function GroupsPage({ searchParams }: { searchParams: Promise<{ error?: string; saved?: string }> }) { const query = await searchParams; - const [allGroups, memberships, [registeredUsers]] = await Promise.all([ + const [allGroups, memberships, [registeredUsers], scheduleCounts] = await Promise.all([ db.select().from(groups).orderBy(desc(groups.isDefault), asc(groups.name)), db.select({ groupId: userGroupMemberships.groupId }).from(userGroupMemberships), db.select({ count: count() }).from(users), + db.select({ groupId: groupAccessWindows.groupId, count: count() }) + .from(groupAccessWindows) + .groupBy(groupAccessWindows.groupId), ]); + const scheduleCountByGroup = new Map(scheduleCounts.map((schedule) => [schedule.groupId, Number(schedule.count)])); return (
@@ -60,10 +65,10 @@ export default async function GroupsPage({ searchParams }: { searchParams: Promi {query.saved &&

{savedMessages[query.saved] ?? "Group updated."}

}
- +
- + {allGroups.map((group) => { @@ -72,6 +77,7 @@ export default async function GroupsPage({ searchParams }: { searchParams: Promi memberships.map((membership) => membership.groupId), group, ); + const scheduleStatus = groupScheduleStatus(scheduleCountByGroup.get(group.id) ?? 0); return ( + diff --git a/apps/web/src/lib/group-schedule.test.ts b/apps/web/src/lib/group-schedule.test.ts index 654831f..ca7e849 100644 --- a/apps/web/src/lib/group-schedule.test.ts +++ b/apps/web/src/lib/group-schedule.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { evaluateGroupSchedule, + groupScheduleStatus, localWindowToUtc, parseScheduleWindows, utcWindowToLocal, @@ -13,6 +14,12 @@ const fridayEvening: WeeklyAccessWindow = { }; describe("weekly group access schedules", () => { + it("summarizes whether a group has configured windows", () => { + expect(groupScheduleStatus(0)).toBe("Unrestricted"); + expect(groupScheduleStatus(1)).toBe("1 window"); + expect(groupScheduleStatus(3)).toBe("3 windows"); + }); + it("allows an enabled group at any time when no schedule is configured", () => { expect(evaluateGroupSchedule([], new Date("2026-08-07T19:00:00Z"))).toEqual({ allowed: true, diff --git a/apps/web/src/lib/group-schedule.ts b/apps/web/src/lib/group-schedule.ts index 7e2c63d..a87135f 100644 --- a/apps/web/src/lib/group-schedule.ts +++ b/apps/web/src/lib/group-schedule.ts @@ -1,6 +1,11 @@ export const MINUTES_PER_WEEK = 7 * 24 * 60; const MAX_WINDOWS = 50; +export function groupScheduleStatus(windowCount: number) { + if (windowCount <= 0) return "Unrestricted"; + return `${windowCount} ${windowCount === 1 ? "window" : "windows"}`; +} + export interface WeeklyAccessWindow { startMinuteOfWeek: number; endMinuteOfWeek: number; diff --git a/design/us-019-admin-group-management.md b/design/us-019-admin-group-management.md index cbc0d3d..8c79ad9 100644 --- a/design/us-019-admin-group-management.md +++ b/design/us-019-admin-group-management.md @@ -14,7 +14,7 @@ As an administrator, I want a concise group policy table and focused group detai # Acceptance Criteria -- [x] The main Groups page lists name, Minecraft access, VPN/proxy/Tor access, and effective member count with the default group first and remaining names ordered alphabetically. +- [x] The main Groups page lists name, Minecraft access, schedule status, VPN/proxy/Tor access, and effective member count with the default group first and remaining names ordered alphabetically. - [x] Policy controls show their current state and require confirmation in an accessible modal before mutation. - [x] Selecting a group name opens a detail page with its description, policies, and effective members. - [x] Add group opens an accessible modal asking for name, description, Minecraft access, and VPN/proxy/Tor access. diff --git a/design/us-020-scheduled-group-access.md b/design/us-020-scheduled-group-access.md index 460915b..9072d84 100644 --- a/design/us-020-scheduled-group-access.md +++ b/design/us-020-scheduled-group-access.md @@ -16,6 +16,7 @@ As an administrator, I want an enabled group to have recurring access windows, s - [x] A group can have zero or more recurring weekly access windows stored and evaluated in UTC. - [x] The browser shows each UTC window's current equivalent in the administrator's local timezone while clearly identifying UTC as authoritative. +- [x] The Groups table identifies unrestricted groups and the configured window count, linking each status to schedule management. - [x] Administrators can add and remove multiple windows, including windows that cross the end of the UTC week. - [x] Window starts are inclusive and window ends are exclusive. - [x] No configured windows preserve unrestricted scheduling behavior while Minecraft access is enabled.
Access groups and their effective policies
NameMinecraft accessVPN accessUsers
NameMinecraft accessScheduleVPN accessUsers
@@ -79,6 +85,7 @@ export default async function GroupsPage({ searchParams }: { searchParams: Promi {group.isDefault && Default} {scheduleStatus} {memberCount}