Compare commits

...

6 Commits
1.2.0 ... 1.2.1

Author SHA1 Message Date
adb5e3e137 Merge pull request 'Fixed styling issue on firefox' (#204) from Calendar-Firefox-Tweak into main
All checks were successful
Testing Site CD / Update Development (push) Successful in 3m52s
Live Site CD / Update Deployment (push) Successful in 4m1s
Reviewed-on: #204
2026-03-08 11:00:10 -05:00
1749c3e617 Fixed styling issue on firefox
All checks were successful
Pull Request CI / Merge Check (pull_request) Successful in 3m47s
2026-03-08 12:00:25 -04:00
52ee36be44 Merge pull request 'Added cache busting option for devs' (#203) from Developer-Page into main
All checks were successful
Testing Site CD / Update Development (push) Successful in 3m48s
Reviewed-on: #203
2026-03-08 09:34:27 -05:00
0cc327a9c4 Added cache busting option for devs
All checks were successful
Pull Request CI / Merge Check (pull_request) Successful in 3m44s
2026-03-08 10:34:29 -04:00
ef3cbbf370 Merge pull request 'Fixed member name filter to work by displayname or actualname' (#202) from Member-Management-Name-Fixes into main
All checks were successful
Testing Site CD / Update Development (push) Successful in 4m8s
Reviewed-on: #202
2026-03-08 09:01:51 -05:00
209d0cdf0f Fixed member name filter to work by displayname or actualname
All checks were successful
Pull Request CI / Merge Check (pull_request) Successful in 4m40s
2026-03-08 09:57:56 -04:00
9 changed files with 124 additions and 6 deletions

View File

@@ -7,7 +7,7 @@ import { requireLogin, requireMemberState, requireRole } from '../middleware/aut
import { getUserActiveLOA } from '../services/db/loaService'; import { getUserActiveLOA } from '../services/db/loaService';
import { getAllMembersLite, getMemberSettings, getMembersFull, getMembersLite, getUserData, getUserState, setUserSettings, getFilteredMembers, setUserState, getLastNonSuspendedState } from '../services/db/memberService'; import { getAllMembersLite, getMemberSettings, getMembersFull, getMembersLite, getUserData, getUserState, setUserSettings, getFilteredMembers, setUserState, getLastNonSuspendedState } from '../services/db/memberService';
import { getUserRoles, stripUserRoles } from '../services/db/rolesService'; import { getUserRoles, stripUserRoles } from '../services/db/rolesService';
import { memberSettings, MemberState, myData } from '@app/shared/types/member'; import { memberSettings, MemberState, myData, UserCacheBustResult } from '@app/shared/types/member';
import { Discharge } from '@app/shared/schemas/dischargeSchema'; import { Discharge } from '@app/shared/schemas/dischargeSchema';
import { Performance } from 'perf_hooks'; import { Performance } from 'perf_hooks';
@@ -211,6 +211,32 @@ router.post('/full/bulk', async (req: Request, res: Response) => {
} }
}) })
router.post('/cache/user/bust', [requireLogin, requireMemberState(MemberState.Member), requireRole('dev')], async (req: Request, res: Response) => {
try {
const clearedEntries = memberCache.Clear();
const payload: UserCacheBustResult = {
success: true,
clearedEntries,
bustedAt: new Date().toISOString(),
};
logger.info('app', 'User cache manually busted', {
actor: req.user.id,
clearedEntries,
});
return res.status(200).json(payload);
} catch (error) {
logger.error('app', 'Failed to bust user cache', {
caller: req.user?.id,
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
return res.status(500).json({ error: 'Failed to bust user cache' });
}
})
router.get('/:id', [requireLogin], async (req, res) => { router.get('/:id', [requireLogin], async (req, res) => {
const userId = req.params.id; const userId = req.params.id;

View File

@@ -16,4 +16,14 @@ export class CacheService<Key, Value> {
public Invalidate(key: Key): boolean { public Invalidate(key: Key): boolean {
return this.cacheMap.delete(key); return this.cacheMap.delete(key);
} }
public Size(): number {
return this.cacheMap.size;
}
public Clear(): number {
const priorSize = this.cacheMap.size;
this.cacheMap.clear();
return priorSize;
}
} }

View File

@@ -23,7 +23,8 @@ export async function getFilteredMembers(
} }
if (search) { if (search) {
whereClauses.push(`v.member_name LIKE ?`); whereClauses.push(`(v.member_name LIKE ? OR v.displayName LIKE ?)`);
params.push(`%${search}%`);
params.push(`%${search}%`); params.push(`%${search}%`);
} }

View File

@@ -50,4 +50,10 @@ export interface myData {
LOAs: LOARequest[]; LOAs: LOARequest[];
roles: Role[]; roles: Role[];
state: MemberState; state: MemberState;
}
export interface UserCacheBustResult {
success: boolean;
clearedEntries: number;
bustedAt: string;
} }

View File

@@ -1,5 +1,5 @@
import { Discharge } from "@shared/schemas/dischargeSchema"; import { Discharge } from "@shared/schemas/dischargeSchema";
import { memberSettings, Member, MemberLight, MemberCardDetails, PaginatedMembers, MemberState } from "@shared/types/member"; import { memberSettings, Member, MemberLight, MemberCardDetails, PaginatedMembers, MemberState, UserCacheBustResult } from "@shared/types/member";
// @ts-ignore // @ts-ignore
const addr = import.meta.env.VITE_APIHOST; const addr = import.meta.env.VITE_APIHOST;
@@ -157,4 +157,17 @@ export async function unsuspendMember(memberID: number): Promise<boolean> {
throw new Error("Failed to discharge member"); throw new Error("Failed to discharge member");
} }
return true; return true;
}
export async function bustUserCache(): Promise<UserCacheBustResult> {
const response = await fetch(`${addr}/members/cache/user/bust`, {
credentials: 'include',
method: 'POST',
});
if (!response.ok) {
throw new Error('Failed to bust user cache');
}
return response.json();
} }

View File

@@ -153,6 +153,12 @@ function blurAfter() {
</NavigationMenuLink> </NavigationMenuLink>
</NavigationMenuContent> </NavigationMenuContent>
</NavigationMenuItem> </NavigationMenuItem>
<NavigationMenuItem v-if="auth.hasRole('Dev')">
<NavigationMenuLink as-child :class="navigationMenuTriggerStyle()">
<RouterLink to="/developer" @click="blurAfter">Developer</RouterLink>
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList> </NavigationMenuList>
</NavigationMenu> </NavigationMenu>
</div> </div>

View File

@@ -227,6 +227,9 @@ onMounted(() => {
/* Ensure the calendar fills the container properly */ /* Ensure the calendar fills the container properly */
:global(.fc) { :global(.fc) {
height: 100% !important; height: 100% !important;
--fc-page-bg-color: transparent;
--fc-neutral-bg-color: color-mix(in srgb, var(--color-foreground) 8%, transparent);
--fc-neutral-text-color: var(--color-muted-foreground);
--fc-border-color: var(--color-border); --fc-border-color: var(--color-border);
--fc-button-bg-color: transparent; --fc-button-bg-color: transparent;
--fc-button-border-color: var(--color-border); --fc-button-border-color: var(--color-border);
@@ -299,6 +302,7 @@ onMounted(() => {
:global(.fc .fc-scrollgrid td), :global(.fc .fc-scrollgrid td),
:global(.fc .fc-scrollgrid th) { :global(.fc .fc-scrollgrid th) {
border-color: var(--color-border); border-color: var(--color-border);
background: var(--fc-page-bg-color);
} }
/* ---------- Built-in toolbar (if you keep it) ---------- */ /* ---------- Built-in toolbar (if you keep it) ---------- */
@@ -346,9 +350,7 @@ onMounted(() => {
text-decoration: none; text-decoration: none;
} }
:global(#app > div > div.flex-1.min-h-0 > div > div > div > div.fc.fc-media-screen.fc-direction-ltr.fc-theme-standard > div.fc-view-harness.fc-view-harness-passive > div > table > thead > tr > th) {
background-color: transparent;
}
:global(.fc .fc-daygrid-day-top) { :global(.fc .fc-daygrid-day-top) {
padding: 8px 8px 0 8px; padding: 8px 8px 0 8px;
} }

View File

@@ -0,0 +1,52 @@
<script setup lang="ts">
import Button from '@/components/ui/button/Button.vue';
import { bustUserCache } from '@/api/member';
import type { UserCacheBustResult } from '@shared/types/member';
import { ref } from 'vue';
const loading = ref(false);
const result = ref<UserCacheBustResult | null>(null);
const error = ref<string | null>(null);
async function onBustUserCache() {
loading.value = true;
error.value = null;
try {
result.value = await bustUserCache();
} catch (err) {
result.value = null;
error.value = err instanceof Error ? err.message : 'Failed to bust user cache';
} finally {
loading.value = false;
}
}
</script>
<template>
<div class="max-w-3xl mx-auto pt-10 px-4">
<h1 class="scroll-m-20 text-2xl font-semibold tracking-tight">Developer Tools</h1>
<p class="mt-2 text-sm text-muted-foreground">
Use this page to recover from stale in-memory authentication data after manual database changes.
</p>
<div class="mt-6 rounded-lg border p-5 bg-card">
<p class="font-medium">Server User Cache</p>
<p class="text-sm text-muted-foreground mt-1">
This clears the API server's cached user session data so the next request reloads from the database.
</p>
<div class="mt-4 flex items-center gap-3">
<Button :disabled="loading" @click="onBustUserCache">
{{ loading ? 'Busting Cache...' : 'Bust User Cache' }}
</Button>
</div>
<p v-if="result" class="mt-4 text-sm text-green-700">
Cache busted successfully. Cleared {{ result.clearedEntries }} entr{{ result.clearedEntries === 1 ? 'y' : 'ies' }} at
{{ new Date(result.bustedAt).toLocaleString() }}.
</p>
<p v-if="error" class="mt-4 text-sm text-red-700">{{ error }}</p>
</div>
</div>
</template>

View File

@@ -28,6 +28,8 @@ const router = createRouter({
{ path: '/trainingReport/new', component: () => import('@/pages/TrainingReport.vue'), meta: { requiresAuth: true, memberOnly: true } }, { path: '/trainingReport/new', component: () => import('@/pages/TrainingReport.vue'), meta: { requiresAuth: true, memberOnly: true } },
{ path: '/trainingReport/:id', component: () => import('@/pages/TrainingReport.vue'), meta: { requiresAuth: true, memberOnly: true } }, { path: '/trainingReport/:id', component: () => import('@/pages/TrainingReport.vue'), meta: { requiresAuth: true, memberOnly: true } },
{ path: '/developer', component: () => import('@/pages/DeveloperTools.vue'), meta: { requiresAuth: true, memberOnly: true, roles: ['Dev'] } },
// ADMIN / STAFF ROUTES // ADMIN / STAFF ROUTES
{ {
path: '/administration', path: '/administration',