feat(M1): Project scaffolding, DB schema, core infrastructure
- Initialize Rust workspace with 7 crates (pm-web, pm-worker, pm-core, pm-agent-client, pm-auth, pm-ca, pm-reports) - React + TypeScript + Vite + MUI frontend scaffold - Full PostgreSQL schema: all 17 tables with indexes and constraints - pm-core: config (TOML+env), db (SQLx pool + migrations), error (unified AppError + JSON envelope), request_id (ULID middleware), logging (tracing JSON/pretty) - pm-web: Axum skeleton, /status/health endpoint, static file serving - pm-worker: Tokio skeleton, heartbeat writer, schema version check - Embedded sqlx migrations with advisory lock (single-writer) - systemd unit files, setup.sh, build-frontend.sh - config.example.toml with all configuration keys - docs/runbooks/restore.md - cargo check passes with zero warnings Closes M1.
This commit is contained in:
37
frontend/src/App.tsx
Normal file
37
frontend/src/App.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { Routes, Route, Navigate } from 'react-router-dom'
|
||||
import { CssBaseline, ThemeProvider } from '@mui/material'
|
||||
import { lightTheme } from './theme/theme'
|
||||
|
||||
// Placeholder pages — implemented in M2+
|
||||
const PlaceholderPage = ({ title }: { title: string }) => (
|
||||
<div style={{ padding: 32 }}>
|
||||
<h2>{title}</h2>
|
||||
<p>Coming soon in a future milestone.</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<CssBaseline />
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
<Route path="/dashboard" element={<PlaceholderPage title="Dashboard" />} />
|
||||
<Route path="/hosts" element={<PlaceholderPage title="Hosts" />} />
|
||||
<Route path="/hosts/:id" element={<PlaceholderPage title="Host Detail" />} />
|
||||
<Route path="/jobs" element={<PlaceholderPage title="Jobs" />} />
|
||||
<Route path="/deployment" element={<PlaceholderPage title="Patch Deployment" />} />
|
||||
<Route path="/maintenance" element={<PlaceholderPage title="Maintenance Windows" />} />
|
||||
<Route path="/groups" element={<PlaceholderPage title="Groups" />} />
|
||||
<Route path="/reports" element={<PlaceholderPage title="Reports" />} />
|
||||
<Route path="/users" element={<PlaceholderPage title="Users" />} />
|
||||
<Route path="/certificates" element={<PlaceholderPage title="Certificates" />} />
|
||||
<Route path="/settings" element={<PlaceholderPage title="Settings" />} />
|
||||
<Route path="/login" element={<PlaceholderPage title="Login" />} />
|
||||
<Route path="*" element={<PlaceholderPage title="404 Not Found" />} />
|
||||
</Routes>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
8
frontend/src/api/client.ts
Normal file
8
frontend/src/api/client.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import axios from 'axios'
|
||||
|
||||
// Base API client — JWT interceptors added in M2
|
||||
export const apiClient = axios.create({
|
||||
baseURL: '/api/v1',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
timeout: 30_000,
|
||||
})
|
||||
2
frontend/src/index.css
Normal file
2
frontend/src/index.css
Normal file
@ -0,0 +1,2 @@
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: 'Roboto', sans-serif; }
|
||||
13
frontend/src/main.tsx
Normal file
13
frontend/src/main.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
)
|
||||
23
frontend/src/theme/theme.ts
Normal file
23
frontend/src/theme/theme.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { createTheme } from '@mui/material/styles'
|
||||
|
||||
export const lightTheme = createTheme({
|
||||
palette: {
|
||||
mode: 'light',
|
||||
primary: { main: '#1565C0' },
|
||||
secondary: { main: '#0288D1' },
|
||||
},
|
||||
typography: {
|
||||
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
||||
},
|
||||
})
|
||||
|
||||
export const darkTheme = createTheme({
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
primary: { main: '#42A5F5' },
|
||||
secondary: { main: '#26C6DA' },
|
||||
},
|
||||
typography: {
|
||||
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
||||
},
|
||||
})
|
||||
47
frontend/src/types/index.ts
Normal file
47
frontend/src/types/index.ts
Normal file
@ -0,0 +1,47 @@
|
||||
// Core TypeScript types — expanded per milestone
|
||||
|
||||
export type UserRole = 'admin' | 'operator'
|
||||
export type AuthProvider = 'local' | 'azure_sso'
|
||||
export type HostHealthStatus = 'pending' | 'healthy' | 'degraded' | 'unreachable'
|
||||
export type JobStatus = 'queued' | 'pending' | 'running' | 'succeeded' | 'failed' | 'cancelled'
|
||||
export type JobKind = 'patch_apply' | 'patch_remove' | 'reboot' | 'rollback'
|
||||
|
||||
export interface ApiError {
|
||||
error: {
|
||||
code: string
|
||||
message: string
|
||||
request_id?: string
|
||||
details?: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export interface Host {
|
||||
id: string
|
||||
fqdn: string
|
||||
ip_address: string
|
||||
display_name: string
|
||||
health_status: HostHealthStatus
|
||||
os_family?: string
|
||||
os_name?: string
|
||||
agent_version?: string
|
||||
registered_at: string
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string
|
||||
username: string
|
||||
display_name: string
|
||||
email: string
|
||||
role: UserRole
|
||||
auth_provider: AuthProvider
|
||||
mfa_enabled: boolean
|
||||
is_active: boolean
|
||||
last_login_at?: string
|
||||
}
|
||||
Reference in New Issue
Block a user