feat(frontend): strip auth and configure for AngelusVigil

Strip the fullstack template of all auth/user/admin code and reconfigure
for the AngelusVigil threat detection dashboard with 3 pages.
This commit is contained in:
CarterPerez-dev 2026-02-28 10:34:02 -05:00
parent d471de31fd
commit 17efa613bf
43 changed files with 2135 additions and 0 deletions

View File

@ -0,0 +1,15 @@
node_modules
build
dist
.git
.gitignore
*.md
.env*
.vscode
.idea
*.log
npm-debug.log*
pnpm-debug.log*
.DS_Store
coverage
.nyc_output

View File

@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
.vite
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1,22 @@
# ©AngelaMos | 2025
# .stylelintignore
# Dependencies
node_modules/
# Production builds
dist/
build/
out/
# JS/TS files
**/*.js
**/*.jsx
**/*.ts
**/*.tsx
# Generated files
*.min.css
# Error system styles - ignore from linting
src/core/app/_toastStyles.scss

View File

@ -0,0 +1,94 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"includes": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.json"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 82,
"lineEnding": "lf"
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"jsxQuoteStyle": "double",
"semicolons": "asNeeded",
"trailingCommas": "es5",
"arrowParentheses": "always"
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"complexity": {
"noExcessiveCognitiveComplexity": {
"level": "error",
"options": { "maxAllowedComplexity": 25 }
},
"noForEach": "off",
"useLiteralKeys": "off"
},
"correctness": {
"noUnusedVariables": "error",
"noUnusedImports": "error",
"useExhaustiveDependencies": "warn",
"useHookAtTopLevel": "error",
"noUndeclaredVariables": "error"
},
"style": {
"useImportType": "error",
"useConst": "error",
"useTemplate": "error",
"useSelfClosingElements": "error",
"useFragmentSyntax": "error",
"noNonNullAssertion": "error",
"useConsistentArrayType": {
"level": "error",
"options": { "syntax": "shorthand" }
},
"useNamingConvention": "off"
},
"suspicious": {
"noExplicitAny": "error",
"noDebugger": "error",
"noConsole": "warn",
"noArrayIndexKey": "warn",
"noAssignInExpressions": "error",
"noDoubleEquals": "error",
"noRedeclare": "error",
"noVar": "error"
},
"security": {
"noDangerouslySetInnerHtml": "error"
},
"a11y": {
"useAltText": "error",
"useAnchorContent": "error",
"useKeyWithClickEvents": "error",
"noStaticElementInteractions": "error",
"useButtonType": "error",
"useValidAnchor": "error"
}
}
},
"overrides": [
{
"includes": ["src/main.tsx"],
"linter": {
"rules": {
"style": {
"noNonNullAssertion": "off"
}
}
}
}
]
}

View File

@ -0,0 +1,44 @@
<!doctype html>
<html lang="en">
<head>
<!--
AngelusVigil
Author(s): © AngelaMos, CarterPerez-dev
-->
<meta charset="UTF-8" />
<link
rel="icon"
type="image/x-icon"
href="/assets/favicon.ico"
/>
<link
rel="apple-touch-icon"
type="image/png"
href="/assets/apple-touch-icon.png"
/>
<link
rel="manifest"
href="/assets/site.webmanifest"
/>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>AngelusVigil</title>
<meta
name="description"
content="*Cracked*"
/>
<meta
name="author"
content=" ©AngelaMos | 2026 | CarterPerez-dev"
/>
</head>
<body>
<div id="root"></div>
<script
type="module"
src="/src/main.tsx"
></script>
</body>
</html>

View File

@ -0,0 +1,50 @@
{
"name": "frontend-template",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"lint": "biome check .",
"lint:fix": "biome check --write .",
"format": "biome format --write .",
"typecheck": "tsc -b",
"lint:scss": "stylelint '**/*.scss'",
"lint:scss:fix": "stylelint '**/*.scss' --fix"
},
"dependencies": {
"@tanstack/react-query": "^5.90.20",
"axios": "^1.13.4",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-error-boundary": "^6.1.0",
"react-icon": "^1.0.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.13.0",
"sonner": "^2.0.7",
"zod": "^4.3.6",
"zustand": "^5.0.10"
},
"devDependencies": {
"@biomejs/biome": "^2.3.13",
"@tanstack/react-query-devtools": "^5.91.2",
"@types/node": "^25.1.0",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
"sass": "^1.97.3",
"stylelint": "^17.0.0",
"stylelint-config-prettier-scss": "^1.0.0",
"stylelint-config-standard-scss": "^17.0.0",
"typescript": "~5.9.3",
"vite": "npm:rolldown-vite@7.3.1",
"vite-tsconfig-paths": "^6.0.5"
},
"pnpm": {
"overrides": {
"vite": "npm:rolldown-vite@7.2.5"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@ -0,0 +1,35 @@
// ===================
// © AngelaMos | 2026
// App.tsx
// ===================
import { QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { RouterProvider } from 'react-router-dom'
import { Toaster } from 'sonner'
import { queryClient } from '@/core/api'
import { router } from '@/core/app/routers'
export default function App(): React.ReactElement {
return (
<QueryClientProvider client={queryClient}>
<div className="app">
<RouterProvider router={router} />
<Toaster
position="top-right"
duration={2000}
theme="dark"
toastOptions={{
style: {
background: 'hsl(0, 0%, 12.2%)',
border: '1px solid hsl(0, 0%, 18%)',
color: 'hsl(0, 0%, 98%)',
},
}}
/>
</div>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}

View File

@ -0,0 +1,4 @@
// ===================
// © AngelaMos | 2026
// index.ts
// ===================

View File

@ -0,0 +1,7 @@
// ===================
// © AngelaMos | 2026
// index.ts
// ===================
export * from './hooks'
export * from './types'

View File

@ -0,0 +1,4 @@
// ===================
// © AngelaMos | 2026
// index.ts
// ===================

View File

@ -0,0 +1,4 @@
/**
* ©AngelaMos | 2026
* index.tsx
*/

View File

@ -0,0 +1,78 @@
// ===================
// © AngelaMos | 2026
// config.ts
// ===================
export const API_ENDPOINTS = {
HEALTH: '/health',
READY: '/ready',
THREATS: {
LIST: '/threats',
BY_ID: (id: string) => `/threats/${id}`,
},
STATS: '/stats',
MODELS: {
STATUS: '/models/status',
RETRAIN: '/models/retrain',
},
} as const
export const WS_ENDPOINTS = {
ALERTS: '/ws/alerts',
} as const
export const QUERY_KEYS = {
THREATS: {
ALL: ['threats'] as const,
LIST: (params: Record<string, unknown>) =>
[...QUERY_KEYS.THREATS.ALL, 'list', params] as const,
BY_ID: (id: string) =>
[...QUERY_KEYS.THREATS.ALL, 'detail', id] as const,
},
STATS: {
ALL: ['stats'] as const,
BY_RANGE: (range: string) =>
[...QUERY_KEYS.STATS.ALL, range] as const,
},
MODELS: {
ALL: ['models'] as const,
STATUS: () => [...QUERY_KEYS.MODELS.ALL, 'status'] as const,
},
} as const
export const ROUTES = {
DASHBOARD: '/',
THREATS: '/threats',
MODELS: '/models',
} as const
export const STORAGE_KEYS = {
UI: 'ui-storage',
} as const
export const QUERY_CONFIG = {
STALE_TIME: {
STANDARD: 1000 * 60 * 5,
FREQUENT: 1000 * 30,
STATIC: Infinity,
},
GC_TIME: {
DEFAULT: 1000 * 60 * 30,
LONG: 1000 * 60 * 60,
},
RETRY: {
DEFAULT: 3,
NONE: 0,
},
} as const
export const PAGINATION = {
DEFAULT_LIMIT: 50,
MAX_LIMIT: 100,
} as const
export const ALERTS = {
MAX_ITEMS: 50,
RECONNECT_BASE_MS: 1000,
RECONNECT_MAX_MS: 30000,
} as const

View File

@ -0,0 +1,24 @@
// ===================
// © AngelaMos | 2026
// api.config.ts
// ===================
import axios, { type AxiosError, type AxiosInstance } from 'axios'
import { transformAxiosError } from './errors'
const getBaseURL = (): string => {
return import.meta.env.VITE_API_URL ?? '/api'
}
export const apiClient: AxiosInstance = axios.create({
baseURL: getBaseURL(),
timeout: 15000,
headers: { 'Content-Type': 'application/json' },
})
apiClient.interceptors.response.use(
(response) => response,
(error: AxiosError): Promise<never> => {
return Promise.reject(transformAxiosError(error))
},
)

View File

@ -0,0 +1,100 @@
// ===================
// © AngelaMos | 2026
// errors.ts
// ===================
import type { AxiosError } from 'axios'
export const ApiErrorCode = {
NETWORK_ERROR: 'NETWORK_ERROR',
VALIDATION_ERROR: 'VALIDATION_ERROR',
NOT_FOUND: 'NOT_FOUND',
CONFLICT: 'CONFLICT',
RATE_LIMITED: 'RATE_LIMITED',
SERVER_ERROR: 'SERVER_ERROR',
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
} as const
export type ApiErrorCode = (typeof ApiErrorCode)[keyof typeof ApiErrorCode]
export class ApiError extends Error {
readonly code: ApiErrorCode
readonly statusCode: number
readonly details?: Record<string, string[]>
constructor(
message: string,
code: ApiErrorCode,
statusCode: number,
details?: Record<string, string[]>,
) {
super(message)
this.name = 'ApiError'
this.code = code
this.statusCode = statusCode
this.details = details
}
getUserMessage(): string {
const messages: Record<ApiErrorCode, string> = {
[ApiErrorCode.NETWORK_ERROR]:
'Unable to connect to the server. Check your connection.',
[ApiErrorCode.VALIDATION_ERROR]: 'Invalid request parameters.',
[ApiErrorCode.NOT_FOUND]: 'The requested resource was not found.',
[ApiErrorCode.CONFLICT]:
'This operation conflicts with an existing resource.',
[ApiErrorCode.RATE_LIMITED]:
'Too many requests. Please wait a moment and try again.',
[ApiErrorCode.SERVER_ERROR]:
'Something went wrong on the server. Please try again later.',
[ApiErrorCode.UNKNOWN_ERROR]:
'An unexpected error occurred. Please try again.',
}
return messages[this.code]
}
}
interface ApiErrorResponse {
detail?: string | { msg: string; type: string }[]
message?: string
}
export function transformAxiosError(error: AxiosError<unknown>): ApiError {
if (!error.response) {
return new ApiError('Network error', ApiErrorCode.NETWORK_ERROR, 0)
}
const { status } = error.response
const data = error.response.data as ApiErrorResponse | undefined
let message = 'An error occurred'
const details: Record<string, string[]> | undefined = undefined
if (data?.detail) {
if (typeof data.detail === 'string') {
message = data.detail
}
} else if (data?.message) {
message = data.message
}
const codeMap: Record<number, ApiErrorCode> = {
400: ApiErrorCode.VALIDATION_ERROR,
404: ApiErrorCode.NOT_FOUND,
409: ApiErrorCode.CONFLICT,
429: ApiErrorCode.RATE_LIMITED,
500: ApiErrorCode.SERVER_ERROR,
502: ApiErrorCode.SERVER_ERROR,
503: ApiErrorCode.SERVER_ERROR,
504: ApiErrorCode.SERVER_ERROR,
}
const code = codeMap[status] || ApiErrorCode.UNKNOWN_ERROR
return new ApiError(message, code, status, details)
}
declare module '@tanstack/react-query' {
interface Register {
defaultError: ApiError
}
}

View File

@ -0,0 +1,8 @@
// ===================
// © AngelaMos | 2026
// index.ts
// ===================
export * from './api.config'
export * from './errors'
export * from './query.config'

View File

@ -0,0 +1,98 @@
// ===================
// © AngelaMos | 2026
// query.config.ts
// ===================
import { MutationCache, QueryCache, QueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { QUERY_CONFIG } from '@/config'
import { ApiError, ApiErrorCode } from './errors'
const NO_RETRY_ERROR_CODES: readonly ApiErrorCode[] = [
ApiErrorCode.NOT_FOUND,
ApiErrorCode.VALIDATION_ERROR,
] as const
const shouldRetryQuery = (failureCount: number, error: Error): boolean => {
if (error instanceof ApiError) {
if (NO_RETRY_ERROR_CODES.includes(error.code)) {
return false
}
}
return failureCount < QUERY_CONFIG.RETRY.DEFAULT
}
const calculateRetryDelay = (attemptIndex: number): number => {
const baseDelay = 1000
const maxDelay = 30000
return Math.min(baseDelay * 2 ** attemptIndex, maxDelay)
}
const handleQueryCacheError = (
error: Error,
query: { state: { data: unknown } },
): void => {
if (query.state.data !== undefined) {
const message =
error instanceof ApiError
? error.getUserMessage()
: 'Background update failed'
toast.error(message)
}
}
const handleMutationCacheError = (
error: Error,
_variables: unknown,
_context: unknown,
mutation: { options: { onError?: unknown } },
): void => {
if (mutation.options.onError === undefined) {
const message =
error instanceof ApiError ? error.getUserMessage() : 'Operation failed'
toast.error(message)
}
}
export const QUERY_STRATEGIES = {
standard: {
staleTime: QUERY_CONFIG.STALE_TIME.STANDARD,
gcTime: QUERY_CONFIG.GC_TIME.DEFAULT,
},
frequent: {
staleTime: QUERY_CONFIG.STALE_TIME.FREQUENT,
gcTime: QUERY_CONFIG.GC_TIME.DEFAULT,
refetchInterval: QUERY_CONFIG.STALE_TIME.FREQUENT,
},
static: {
staleTime: QUERY_CONFIG.STALE_TIME.STATIC,
gcTime: QUERY_CONFIG.GC_TIME.LONG,
refetchOnMount: false,
refetchOnWindowFocus: false,
},
} as const
export type QueryStrategy = keyof typeof QUERY_STRATEGIES
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: QUERY_CONFIG.STALE_TIME.STANDARD,
gcTime: QUERY_CONFIG.GC_TIME.DEFAULT,
retry: shouldRetryQuery,
retryDelay: calculateRetryDelay,
refetchOnWindowFocus: true,
refetchOnMount: true,
refetchOnReconnect: true,
},
mutations: {
retry: QUERY_CONFIG.RETRY.NONE,
},
},
queryCache: new QueryCache({
onError: handleQueryCacheError,
}),
mutationCache: new MutationCache({
onError: handleMutationCacheError,
}),
})

View File

@ -0,0 +1,34 @@
// ===================
// © AngelaMos | 2026
// routers.tsx
// ===================
import { Navigate, createBrowserRouter, type RouteObject } from 'react-router-dom'
import { ROUTES } from '@/config'
import { Shell } from './shell'
const routes: RouteObject[] = [
{
element: <Shell />,
children: [
{
path: ROUTES.DASHBOARD,
lazy: () => import('@/pages/dashboard'),
},
{
path: ROUTES.THREATS,
lazy: () => import('@/pages/threats'),
},
{
path: ROUTES.MODELS,
lazy: () => import('@/pages/models'),
},
],
},
{
path: '*',
element: <Navigate to={ROUTES.DASHBOARD} replace />,
},
]
export const router = createBrowserRouter(routes)

View File

@ -0,0 +1,331 @@
// ===================
// © AngelaMos | 2026
// shell.module.scss
// ===================
@use '@/styles' as *;
$sidebar-width: 240px;
$sidebar-collapsed-width: 64px;
$header-height: 56px;
.shell {
display: flex;
min-height: 100vh;
min-height: 100dvh;
}
.sidebar {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: $sidebar-width;
background: $bg-surface-100;
border-right: 1px solid $border-default;
display: flex;
flex-direction: column;
z-index: $z-fixed;
@include transition-fast;
&.collapsed {
width: $sidebar-collapsed-width;
}
@include breakpoint-down('sm') {
transform: translateX(-100%);
&.open {
transform: translateX(0);
}
&.collapsed {
width: $sidebar-width;
}
}
}
.sidebarHeader {
height: $header-height;
padding: 0 $space-3;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid $border-default;
.sidebar.collapsed & {
justify-content: center;
padding: 0;
}
}
.logo {
font-size: $font-size-base;
font-weight: $font-weight-semibold;
color: $text-default;
@include transition-fast;
.sidebar.collapsed & {
display: none;
}
}
.nav {
flex: 1;
padding: $space-3;
display: flex;
flex-direction: column;
gap: $space-1;
}
.navItem {
display: flex;
align-items: center;
gap: $space-3;
padding: $space-2 $space-3;
border-radius: $radius-md;
font-size: $font-size-sm;
color: $text-light;
@include transition-fast;
@include hover {
background: $bg-surface-200;
color: $text-default;
}
&.active {
background: $bg-selection;
color: $text-default;
}
.sidebar.collapsed & {
justify-content: center;
}
}
.navIcon {
width: 17px;
height: 17px;
flex-shrink: 0;
}
.navLabel {
@include transition-fast;
.sidebar.collapsed & {
display: none;
}
}
.adminItem {
margin-top: auto;
border-top: 1px solid $border-default;
padding-top: $space-3;
}
.collapseBtn {
width: 45px;
height: 45px;
border-radius: $radius-md;
color: $text-light;
@include flex-center;
@include transition-fast;
svg {
width: 23.5px;
height: 23.5px;
}
@include hover {
background: $bg-surface-200;
color: $text-default;
}
@include breakpoint-down('sm') {
display: none;
}
}
.sidebarFooter {
padding: $space-3;
border-top: 1px solid $border-default;
}
.logoutBtn {
width: 100%;
display: flex;
align-items: center;
gap: $space-3;
padding: $space-3;
border-radius: $radius-md;
font-size: $font-size-sm;
color: $text-default;
@include transition-fast;
@include hover {
background: $bg-surface-200;
}
.sidebar.collapsed & {
justify-content: center;
.logoutText {
display: none;
}
}
}
.logoutIcon {
width: 18px;
height: 18px;
flex-shrink: 0;
}
.logoutText {
font-weight: $font-weight-medium;
@include transition-fast;
}
.overlay {
position: fixed;
inset: 0;
background: rgb(0, 0, 0, 50%);
z-index: calc($z-fixed - 1);
display: none;
border: none;
padding: 0;
cursor: pointer;
@include breakpoint-down('sm') {
display: block;
}
}
.main {
flex: 1;
display: flex;
flex-direction: column;
margin-left: $sidebar-width;
min-width: 0;
@include transition-fast;
&.collapsed {
margin-left: $sidebar-collapsed-width;
}
@include breakpoint-down('sm') {
margin-left: 0;
&.collapsed {
margin-left: 0;
}
}
}
.header {
position: sticky;
top: 0;
height: $header-height;
background: $bg-surface-100;
border-bottom: 1px solid $border-default;
z-index: $z-sticky;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 $space-4;
}
.headerLeft {
display: flex;
align-items: center;
gap: $space-3;
}
.menuBtn {
display: none;
width: 36px;
height: 36px;
border-radius: $radius-md;
color: $text-light;
align-items: center;
justify-content: center;
@include transition-fast;
svg {
width: 20px;
height: 20px;
}
@include hover {
background: $bg-surface-200;
color: $text-default;
}
@media (width <= 479px) {
display: flex;
}
}
.pageTitle {
font-size: $font-size-base;
font-weight: $font-weight-medium;
color: $text-default;
margin-left: 7px;
}
.headerRight {
display: flex;
align-items: center;
gap: $space-3;
}
.avatar {
width: 32px;
height: 32px;
border-radius: $radius-full;
background: $bg-surface-300;
color: $text-light;
font-size: $font-size-sm;
font-weight: $font-weight-medium;
@include flex-center;
cursor: pointer;
@include transition-fast;
@include hover {
filter: brightness(1.2);
}
}
.content {
flex: 1;
overflow-y: auto;
}
.loading {
@include flex-center;
height: 100%;
color: $text-muted;
}
.error {
@include flex-column-center;
height: 100%;
gap: $space-4;
padding: $space-6;
color: $error-default;
h2 {
font-size: $font-size-xl;
font-weight: $font-weight-semibold;
}
pre {
font-family: $font-mono;
font-size: $font-size-sm;
padding: $space-4;
background: $bg-surface-200;
border-radius: $radius-lg;
overflow-x: auto;
max-width: 100%;
}
}

View File

@ -0,0 +1,118 @@
// ===================
// © AngelaMos | 2026
// shell.tsx
// ===================
import { Suspense } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import { LuChevronLeft, LuChevronRight, LuCpu, LuLayoutDashboard, LuMenu, LuShield } from 'react-icons/lu'
import { NavLink, Outlet, useLocation } from 'react-router-dom'
import { ROUTES } from '@/config'
import { useUIStore } from '@/core/lib'
import styles from './shell.module.scss'
const NAV_ITEMS = [
{ path: ROUTES.DASHBOARD, label: 'Dashboard', icon: LuLayoutDashboard },
{ path: ROUTES.THREATS, label: 'Threats', icon: LuShield },
{ path: ROUTES.MODELS, label: 'Models', icon: LuCpu },
]
function ShellErrorFallback({ error }: { error: Error }): React.ReactElement {
return (
<div className={styles.error}>
<h2>Something went wrong</h2>
<pre>{error.message}</pre>
</div>
)
}
function ShellLoading(): React.ReactElement {
return <div className={styles.loading}>Loading...</div>
}
function getPageTitle(pathname: string): string {
const item = NAV_ITEMS.find((i) => i.path === pathname)
return item?.label ?? 'Dashboard'
}
export function Shell(): React.ReactElement {
const location = useLocation()
const { sidebarOpen, sidebarCollapsed, toggleSidebar, toggleSidebarCollapsed } =
useUIStore()
const pageTitle = getPageTitle(location.pathname)
return (
<div className={styles.shell}>
<aside
className={`${styles.sidebar} ${sidebarOpen ? styles.open : ''} ${sidebarCollapsed ? styles.collapsed : ''}`}
>
<div className={styles.sidebarHeader}>
<span className={styles.logo}>
{sidebarCollapsed ? 'AV' : 'AngelusVigil'}
</span>
<button
type="button"
className={styles.collapseBtn}
onClick={toggleSidebarCollapsed}
aria-label={sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
>
{sidebarCollapsed ? <LuChevronRight /> : <LuChevronLeft />}
</button>
</div>
<nav className={styles.nav}>
{NAV_ITEMS.map((item) => (
<NavLink
key={item.path}
to={item.path}
className={({ isActive }) =>
`${styles.navItem} ${isActive ? styles.active : ''}`
}
onClick={() => sidebarOpen && toggleSidebar()}
>
<item.icon className={styles.navIcon} />
<span className={styles.navLabel}>{item.label}</span>
</NavLink>
))}
</nav>
</aside>
{sidebarOpen && (
<button
type="button"
className={styles.overlay}
onClick={toggleSidebar}
onKeyDown={(e) => e.key === 'Escape' && toggleSidebar()}
aria-label="Close sidebar"
/>
)}
<div
className={`${styles.main} ${sidebarCollapsed ? styles.collapsed : ''}`}
>
<header className={styles.header}>
<div className={styles.headerLeft}>
<button
type="button"
className={styles.menuBtn}
onClick={toggleSidebar}
aria-label="Toggle menu"
>
<LuMenu />
</button>
<h1 className={styles.pageTitle}>{pageTitle}</h1>
</div>
</header>
<main className={styles.content}>
<ErrorBoundary FallbackComponent={ShellErrorFallback}>
<Suspense fallback={<ShellLoading />}>
<Outlet />
</Suspense>
</ErrorBoundary>
</main>
</div>
</div>
)
}

View File

@ -0,0 +1,67 @@
// ===================
// © AngelaMos | 2026
// toast.module.scss
// ===================
@use '@/styles' as *;
:global {
[data-sonner-toaster] {
--normal-bg: #{$bg-surface-100};
--normal-border: #{$border-default};
--normal-text: #{$text-default};
--success-bg: #{$bg-surface-100};
--success-border: #{$border-default};
--success-text: #{$text-default};
--error-bg: #{$bg-surface-100};
--error-border: #{$error-default};
--error-text: #{$text-default};
--warning-bg: #{$bg-surface-100};
--warning-border: #{$border-default};
--warning-text: #{$text-default};
--info-bg: #{$bg-surface-100};
--info-border: #{$border-default};
--info-text: #{$text-default};
font-family: $font-sans;
}
[data-sonner-toast] {
border-radius: $radius-md;
padding: $space-3 $space-4;
font-size: $font-size-sm;
border: 1px solid $border-default;
background: $bg-surface-100;
color: $text-default;
[data-title] {
font-weight: $font-weight-medium;
}
[data-description] {
color: $text-light;
font-size: $font-size-xs;
}
[data-close-button] {
background: none;
border: none;
padding: 0;
cursor: pointer;
color: $text-muted;
@include transition-fast;
@include hover {
color: $text-default;
}
}
}
[data-sonner-toast][data-type='error'] {
border-color: $error-default;
}
}

View File

@ -0,0 +1,15 @@
// ===========================
// ©AngelaMos | 2026
// main.tsx
// ===========================
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
import './styles.scss'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
)

View File

@ -0,0 +1,152 @@
// ===================
// © AngelaMos | 2026
// dashboard.module.scss
// ===================
@use '@/styles' as *;
.page {
min-height: calc(100vh - 56px);
padding: $space-6;
background-color: $bg-default;
}
.container {
max-width: 800px;
margin: 0 auto;
}
.header {
margin-bottom: $space-6;
}
.title {
font-size: $font-size-2xl;
font-weight: $font-weight-semibold;
color: $text-default;
margin-bottom: $space-2;
}
.subtitle {
font-size: $font-size-sm;
color: $text-lighter;
}
.userCard {
display: flex;
align-items: center;
gap: $space-4;
padding: $space-5;
background: $bg-surface-100;
border: 1px solid $border-default;
border-radius: $radius-lg;
margin-bottom: $space-8;
}
.avatar {
width: 56px;
height: 56px;
border-radius: $radius-full;
background: $bg-surface-300;
color: $text-default;
font-size: $font-size-xl;
font-weight: $font-weight-semibold;
@include flex-center;
flex-shrink: 0;
}
.userInfo {
@include flex-column;
gap: $space-1;
min-width: 0;
}
.userName {
font-size: $font-size-base;
font-weight: $font-weight-medium;
color: $text-default;
@include truncate;
}
.userEmail {
font-size: $font-size-sm;
color: $text-light;
@include truncate;
}
.userRole {
display: inline-flex;
align-self: flex-start;
padding: $space-0-5 $space-2;
background: $bg-surface-200;
border-radius: $radius-full;
font-size: $font-size-xs;
color: $text-lighter;
text-transform: uppercase;
letter-spacing: $tracking-wide;
}
.section {
margin-bottom: $space-8;
}
.sectionTitle {
font-size: $font-size-sm;
font-weight: $font-weight-medium;
color: $text-light;
text-transform: uppercase;
letter-spacing: $tracking-wide;
margin-bottom: $space-4;
}
.grid {
display: grid;
gap: $space-4;
@include breakpoint-up('md') {
grid-template-columns: repeat(3, 1fr);
}
}
.card {
padding: $space-4;
background: $bg-surface-100;
border: 1px solid $border-default;
border-radius: $radius-lg;
}
.hookName {
display: inline-block;
font-size: $font-size-sm;
font-weight: $font-weight-medium;
color: $text-default;
background: $bg-surface-200;
padding: $space-1 $space-2;
border-radius: $radius-sm;
margin-bottom: $space-2;
}
.description {
font-size: $font-size-sm;
color: $text-light;
margin-bottom: $space-2;
line-height: $line-height-relaxed;
}
.file {
font-size: $font-size-xs;
color: $text-muted;
font-family: $font-mono;
}
.list {
@include flex-column;
gap: $space-2;
padding-left: $space-5;
list-style: disc;
li {
font-size: $font-size-sm;
color: $text-light;
}
}

View File

@ -0,0 +1,10 @@
// ===================
// © AngelaMos | 2026
// index.tsx
// ===================
export function Component(): React.ReactElement {
return <div>Dashboard</div>
}
Component.displayName = 'DashboardPage'

View File

@ -0,0 +1,10 @@
// ===================
// © AngelaMos | 2026
// index.tsx
// ===================
export function Component(): React.ReactElement {
return <div>Models</div>
}
Component.displayName = 'ModelsPage'

View File

@ -0,0 +1,10 @@
// ===================
// © AngelaMos | 2026
// index.tsx
// ===================
export function Component(): React.ReactElement {
return <div>Threats</div>
}
Component.displayName = 'ThreatsPage'

View File

@ -0,0 +1,29 @@
// ===================
// © AngelaMos | 2026
// styles.scss
// ===================
@forward 'styles/tokens';
@forward 'styles/fonts';
@forward 'styles/mixins';
@use 'styles/reset';
@use 'styles/tokens' as *;
@use 'styles/fonts' as *;
#root {
min-height: 100vh;
min-height: 100dvh;
display: flex;
flex-direction: column;
background: $bg-default;
}
.app {
flex: 1;
display: flex;
flex-direction: column;
background: $bg-default;
color: $text-default;
font-family: $font-sans;
}

View File

@ -0,0 +1,12 @@
// ===================
// © AngelaMos | 2026
// _fonts.scss
// ===================
@use 'tokens' as *;
$font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Roboto,
'Helvetica Neue', Arial, sans-serif;
$font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas,
'Liberation Mono', monospace;

View File

@ -0,0 +1,8 @@
// ===================
// © AngelaMos | 2026
// _index.scss
// ===================
@forward 'tokens';
@forward 'fonts';
@forward 'mixins';

View File

@ -0,0 +1,120 @@
// ===================
// © AngelaMos | 2026
// _mixins.scss
// ===================
@use 'sass:map';
@use 'sass:list';
@use 'tokens' as *;
$breakpoints: (
'xs': $breakpoint-xs,
'sm': $breakpoint-sm,
'md': $breakpoint-md,
'lg': $breakpoint-lg,
'xl': $breakpoint-xl,
'2xl': $breakpoint-2xl,
);
@mixin breakpoint-up($size) {
@media (min-width: map.get($breakpoints, $size)) {
@content;
}
}
@mixin breakpoint-down($size) {
@media (width < map.get($breakpoints, $size)) {
@content;
}
}
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
@mixin flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
@mixin flex-column {
display: flex;
flex-direction: column;
}
@mixin flex-column-center {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
@mixin sr-only {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
@mixin truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@mixin line-clamp($lines: 2) {
display: -webkit-box;
-webkit-line-clamp: #{$lines};
-webkit-box-orient: vertical;
overflow: hidden;
}
@mixin transition-fast {
transition-property: background-color, border-color, color, opacity;
transition-duration: $duration-fast;
transition-timing-function: $ease-out;
}
@mixin transition-normal {
transition-property: background-color, border-color, color, opacity;
transition-duration: $duration-normal;
transition-timing-function: $ease-out;
}
@mixin absolute-fill {
position: absolute;
inset: 0;
}
@mixin absolute-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
@mixin hover {
@media (hover: hover) and (pointer: fine) {
&:hover {
@content;
}
}
}
@mixin hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}

View File

@ -0,0 +1,198 @@
// ===================
// © AngelaMos | 2026
// _reset.scss
// ===================
@use 'tokens' as *;
@use 'fonts' as *;
*,
*::before,
*::after {
box-sizing: border-box;
}
* {
margin: 0;
padding: 0;
-webkit-tap-highlight-color: transparent;
}
html {
font-size: 16px;
-moz-text-size-adjust: none;
-webkit-text-size-adjust: none;
text-size-adjust: none;
overflow-x: hidden;
}
@media (prefers-reduced-motion: no-preference) {
html {
interpolate-size: allow-keywords;
scroll-behavior: smooth;
}
}
body {
min-height: 100vh;
min-height: 100dvh;
line-height: $line-height-normal;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
background-color: $bg-default;
color: $text-default;
font-family: $font-sans;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: $line-height-tight;
text-wrap: balance;
overflow-wrap: break-word;
font-weight: $font-weight-semibold;
}
p {
text-wrap: pretty;
overflow-wrap: break-word;
}
ul,
ol {
list-style: none;
}
a {
color: inherit;
text-decoration: none;
}
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
height: auto;
}
input,
button,
textarea,
select {
font: inherit;
color: inherit;
}
input[type='text'],
input[type='email'],
input[type='password'],
input[type='search'],
input[type='number'],
input[type='tel'],
input[type='url'],
textarea,
select {
font-size: $font-size-sm;
appearance: none;
}
button {
background: none;
border: none;
cursor: pointer;
text-align: inherit;
font-family: inherit;
}
fieldset {
border: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
textarea:not([rows]) {
min-height: 10em;
}
:target {
scroll-margin-block: 5ex;
}
:focus-visible {
outline: 2px solid $border-strong;
outline-offset: 2px;
}
:focus:not(:focus-visible) {
outline: none;
}
[hidden] {
display: none !important;
}
[disabled] {
cursor: not-allowed;
opacity: 0.5;
}
dialog {
padding: 0;
border: none;
background: transparent;
}
summary {
cursor: pointer;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
@supports (padding: max(0px)) {
body {
padding-left: max(0px, env(safe-area-inset-left));
padding-right: max(0px, env(safe-area-inset-right));
}
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: $border-default;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: $border-strong;
}
::selection {
background-color: $bg-selection;
}

View File

@ -0,0 +1,168 @@
// ===================
// © AngelaMos | 2026
// _tokens.scss
// ===================
// ============================================================================
// SPACING (8px base system)
// ============================================================================
$space-0: 0;
$space-px: 1px;
$space-0-5: 0.125rem;
$space-1: 0.25rem;
$space-1-5: 0.375rem;
$space-2: 0.5rem;
$space-2-5: 0.625rem;
$space-3: 0.75rem;
$space-3-5: 0.875rem;
$space-4: 1rem;
$space-5: 1.25rem;
$space-6: 1.5rem;
$space-7: 1.75rem;
$space-8: 2rem;
$space-9: 2.25rem;
$space-10: 2.5rem;
$space-11: 2.75rem;
$space-12: 3rem;
$space-14: 3.5rem;
$space-16: 4rem;
$space-20: 5rem;
$space-24: 6rem;
$space-28: 7rem;
$space-32: 8rem;
// ============================================================================
// TYPOGRAPHY SCALE
// ============================================================================
$font-size-3xs: 0.625rem;
$font-size-2xs: 0.6875rem;
$font-size-xs: 0.75rem;
$font-size-sm: 0.875rem;
$font-size-base: 1rem;
$font-size-lg: 1.125rem;
$font-size-xl: 1.25rem;
$font-size-2xl: 1.5rem;
$font-size-3xl: 1.875rem;
$font-size-4xl: 2.25rem;
$font-size-5xl: 3rem;
// ============================================================================
// FONT WEIGHTS
// ============================================================================
$font-weight-regular: 400;
$font-weight-medium: 500;
$font-weight-semibold: 600;
// ============================================================================
// LINE HEIGHTS
// ============================================================================
$line-height-none: 1;
$line-height-tight: 1.2;
$line-height-snug: 1.375;
$line-height-normal: 1.5;
$line-height-relaxed: 1.625;
// ============================================================================
// LETTER SPACING
// ============================================================================
$tracking-tighter: -0.05em;
$tracking-tight: -0.025em;
$tracking-normal: 0;
$tracking-wide: 0.025em;
$tracking-wider: 0.05em;
// ============================================================================
// COLORS
// ============================================================================
$white: hsl(0, 0%, 100%);
$black: hsl(0, 0%, 0%);
// Auth
$bg-page: hsl(0, 0%, 10.5%);
$bg-card: hsl(0, 0%, 6.2%);
// Home/landing
$bg-landing: hsl(0, 0%, 10.8%);
$bg-default: hsl(0, 0%, 7.1%);
$bg-alternative: hsl(0, 0%, 5.9%);
$bg-surface-75: hsl(0, 0%, 9%);
$bg-surface-100: hsl(0, 0%, 12.2%);
$bg-surface-200: hsl(0, 0%, 14.1%);
$bg-surface-300: hsl(0, 0%, 16.1%);
$bg-control: hsl(0, 0%, 10%);
$bg-selection: hsl(0, 0%, 19.2%);
$bg-overlay: hsl(0, 0%, 14.1%);
$bg-overlay-hover: hsl(0, 0%, 18%);
$border-muted: hsl(0, 0%, 11.1%);
$border-default: hsl(0, 0%, 18%);
$border-strong: hsl(0, 0%, 22.4%);
$border-stronger: hsl(0, 0%, 27.1%);
$border-control: hsl(0, 0%, 22.4%);
$text-default: hsl(0, 0%, 98%);
$text-light: hsl(0, 0%, 70.6%);
$text-lighter: hsl(0, 0%, 53.7%);
$text-muted: hsl(0, 0%, 30.2%);
$error-default: hsl(0, 72%, 51%);
$error-light: hsl(0, 72%, 65%);
// ============================================================================
// BORDER RADIUS
// ============================================================================
$radius-none: 0;
$radius-xs: 2px;
$radius-sm: 4px;
$radius-md: 6px;
$radius-lg: 8px;
$radius-xl: 12px;
$radius-full: 9999px;
// ============================================================================
// Z-INDEX SCALE
// ============================================================================
$z-hide: -1;
$z-base: 0;
$z-dropdown: 100;
$z-sticky: 200;
$z-fixed: 300;
$z-overlay: 400;
$z-modal: 500;
$z-popover: 600;
$z-tooltip: 700;
$z-toast: 800;
$z-max: 9999;
// ============================================================================
// TRANSITIONS
// ============================================================================
$duration-instant: 0ms;
$duration-fast: 100ms;
$duration-normal: 150ms;
$duration-slow: 200ms;
$ease-out: cubic-bezier(0, 0, 0.2, 1);
$ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
// ============================================================================
// BREAKPOINTS
// ============================================================================
$breakpoint-xs: 360px;
$breakpoint-sm: 480px;
$breakpoint-md: 768px;
$breakpoint-lg: 1024px;
$breakpoint-xl: 1280px;
$breakpoint-2xl: 1536px;
// ============================================================================
// CONTAINER WIDTHS
// ============================================================================
$container-xs: 20rem;
$container-sm: 24rem;
$container-md: 28rem;
$container-lg: 32rem;
$container-xl: 36rem;
$container-2xl: 42rem;
$container-full: 100%;

View File

@ -0,0 +1,107 @@
// ©AngelaMos | 2025
// stylelint.config.js
/** @type {import('stylelint').Config} */
export default {
extends: ['stylelint-config-standard-scss', 'stylelint-config-prettier-scss'],
rules: {
'block-no-empty': true,
'declaration-no-important': true,
'color-no-invalid-hex': true,
'property-no-unknown': true,
'selector-pseudo-class-no-unknown': [
true,
{
ignorePseudoClasses: ['global'],
},
],
'selector-class-pattern': [
'^[a-z]([a-z0-9-]+)?(__[a-z0-9]([a-z0-9-]+)?)?(--[a-z0-9]([a-z0-9-]+)?)?$|^[a-z][a-zA-Z0-9]*$',
{
message:
'Selector should be in BEM format (e.g., .block__element--modifier) or CSS Modules camelCase (e.g., .testButton)',
},
],
'value-keyword-case': [
'lower',
{
camelCaseSvgKeywords: true,
ignoreKeywords: [
'BlinkMacSystemFont',
'SFMono-Regular',
'Menlo',
'Monaco',
'Consolas',
'Roboto',
'Arial',
'Helvetica',
'Times',
'Georgia',
'Verdana',
'Tahoma',
'Trebuchet',
'Impact',
'Comic',
],
},
],
'property-no-vendor-prefix': [
true,
{
ignoreProperties: ['text-size-adjust', 'appearance', 'backdrop-filter'],
},
],
'value-no-vendor-prefix': true,
'selector-no-vendor-prefix': true,
'property-no-deprecated': [
true,
{
ignoreProperties: ['clip'],
},
],
'container-name-pattern': null,
'layer-name-pattern': null,
'scss/at-rule-no-unknown': true,
'scss/declaration-nested-properties-no-divided-groups': true,
'scss/dollar-variable-no-missing-interpolation': true,
'scss/dollar-variable-empty-line-before': null,
'declaration-empty-line-before': null,
'custom-property-empty-line-before': null,
'no-descending-specificity': null,
'media-feature-name-no-unknown': [
true,
{
ignoreMediaFeatureNames: ['map'],
},
],
'color-function-notation': null,
'hue-degree-notation': null,
},
ignoreFiles: [
'node_modules/**',
'dist/**',
'build/**',
'**/*.js',
'**/*.ts',
'**/*.tsx',
],
overrides: [
{
files: ['**/styles/_reset.scss', '**/styles/_fonts.scss'],
rules: {
'declaration-no-important': null,
'scss/comment-no-empty': null,
},
},
],
}

View File

@ -0,0 +1,31 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}

View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,71 @@
/**
* ©AngelaMos | 2026
* vite.config.ts
*/
import path from 'node:path'
import react from '@vitejs/plugin-react'
import { defineConfig, loadEnv } from 'vite'
import tsconfigPaths from 'vite-tsconfig-paths'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, path.resolve(__dirname, '..'), '')
const isDev = mode === 'development'
return {
plugins: [react(), tsconfigPaths()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
css: {
preprocessorOptions: {
scss: {},
},
},
server: {
port: 5173,
host: '0.0.0.0',
proxy: {
'/api': {
target: env.VITE_API_TARGET || 'http://localhost:8000',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/api/, ''),
},
},
},
build: {
target: 'esnext',
cssTarget: 'chrome100',
sourcemap: isDev ? true : 'hidden',
minify: 'oxc',
rollupOptions: {
output: {
manualChunks(id: string): string | undefined {
if (id.includes('node_modules')) {
if (id.includes('react-dom') || id.includes('react-router')) {
return 'vendor-react'
}
if (id.includes('@tanstack/react-query')) {
return 'vendor-query'
}
if (id.includes('zustand')) {
return 'vendor-state'
}
}
return undefined
},
},
},
},
preview: {
port: 4173,
},
}
})