Session 4 complete - RedFlag update management platform

🚩 Private development - version retention only

 Complete web dashboard (React + TypeScript + TailwindCSS)
 Production-ready server backend (Go + Gin + PostgreSQL)
 Linux agent with APT + Docker scanning + local CLI tools
 JWT authentication and REST API
 Update discovery and approval workflow

🚧 Status: Alpha software - active development
📦 Purpose: Version retention during development
⚠️  Not for public use or deployment
This commit is contained in:
Fimeg
2025-10-13 16:46:31 -04:00
commit 55b7d03010
57 changed files with 7326 additions and 0 deletions

View File

@@ -0,0 +1,201 @@
import axios, { AxiosResponse } from 'axios';
import {
Agent,
UpdatePackage,
DashboardStats,
AgentListResponse,
UpdateListResponse,
UpdateApprovalRequest,
ScanRequest,
ListQueryParams,
ApiResponse,
ApiError
} from '@/types';
// Create axios instance
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || '/api/v1',
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor to add auth token
api.interceptors.request.use((config) => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Response interceptor to handle errors
api.interceptors.response.use(
(response: AxiosResponse) => response,
(error) => {
if (error.response?.status === 401) {
// Clear token and redirect to login
localStorage.removeItem('auth_token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
// API endpoints
export const agentApi = {
// Get all agents
getAgents: async (params?: ListQueryParams): Promise<AgentListResponse> => {
const response = await api.get('/agents', { params });
return response.data;
},
// Get single agent
getAgent: async (id: string): Promise<Agent> => {
const response = await api.get(`/agents/${id}`);
return response.data;
},
// Trigger scan on agents
triggerScan: async (request: ScanRequest): Promise<void> => {
await api.post('/agents/scan', request);
},
// Trigger scan on single agent
scanAgent: async (id: string): Promise<void> => {
await api.post(`/agents/${id}/scan`);
},
};
export const updateApi = {
// Get all updates
getUpdates: async (params?: ListQueryParams): Promise<UpdateListResponse> => {
const response = await api.get('/updates', { params });
return response.data;
},
// Get single update
getUpdate: async (id: string): Promise<UpdatePackage> => {
const response = await api.get(`/updates/${id}`);
return response.data;
},
// Approve updates
approveUpdates: async (request: UpdateApprovalRequest): Promise<void> => {
await api.post('/updates/approve', request);
},
// Approve single update
approveUpdate: async (id: string, scheduledAt?: string): Promise<void> => {
await api.post(`/updates/${id}/approve`, { scheduled_at: scheduledAt });
},
// Reject/cancel update
rejectUpdate: async (id: string): Promise<void> => {
await api.post(`/updates/${id}/reject`);
},
// Install update immediately
installUpdate: async (id: string): Promise<void> => {
await api.post(`/updates/${id}/install`);
},
};
export const statsApi = {
// Get dashboard statistics
getDashboardStats: async (): Promise<DashboardStats> => {
const response = await api.get('/stats/summary');
return response.data;
},
};
export const authApi = {
// Simple login (using API key or token)
login: async (credentials: { token: string }): Promise<{ token: string }> => {
const response = await api.post('/auth/login', credentials);
return response.data;
},
// Verify token
verifyToken: async (): Promise<{ valid: boolean }> => {
const response = await api.get('/auth/verify');
return response.data;
},
// Logout
logout: async (): Promise<void> => {
await api.post('/auth/logout');
},
};
// Utility functions
export const createQueryString = (params: Record<string, any>): string => {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
if (Array.isArray(value)) {
value.forEach(v => searchParams.append(key, v));
} else {
searchParams.append(key, value.toString());
}
}
});
return searchParams.toString();
};
// Error handling utility
export const handleApiError = (error: any): ApiError => {
if (axios.isAxiosError(error)) {
const status = error.response?.status;
const data = error.response?.data;
if (status === 401) {
return {
message: 'Authentication required. Please log in.',
code: 'UNAUTHORIZED',
};
}
if (status === 403) {
return {
message: 'Access denied. You do not have permission to perform this action.',
code: 'FORBIDDEN',
};
}
if (status === 404) {
return {
message: 'The requested resource was not found.',
code: 'NOT_FOUND',
};
}
if (status === 429) {
return {
message: 'Too many requests. Please try again later.',
code: 'RATE_LIMIT_EXCEEDED',
};
}
if (status >= 500) {
return {
message: 'Server error. Please try again later.',
code: 'SERVER_ERROR',
};
}
return {
message: data?.message || error.message || 'An error occurred',
code: data?.code || 'UNKNOWN_ERROR',
details: data?.details,
};
}
return {
message: error.message || 'An unexpected error occurred',
code: 'UNKNOWN_ERROR',
};
};
export default api;

View File

@@ -0,0 +1,241 @@
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { Agent, UpdatePackage, FilterState } from '@/types';
// Auth store
interface AuthState {
token: string | null;
isAuthenticated: boolean;
setToken: (token: string) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
token: null,
isAuthenticated: false,
setToken: (token) => set({ token, isAuthenticated: true }),
logout: () => set({ token: null, isAuthenticated: false }),
}),
{
name: 'auth-storage',
partialize: (state) => ({ token: state.token, isAuthenticated: state.isAuthenticated }),
storage: createJSONStorage(() => localStorage),
}
)
);
// UI store for global state
interface UIState {
sidebarOpen: boolean;
theme: 'light' | 'dark';
activeTab: string;
setSidebarOpen: (open: boolean) => void;
setTheme: (theme: 'light' | 'dark') => void;
setActiveTab: (tab: string) => void;
}
export const useUIStore = create<UIState>()(
persist(
(set) => ({
sidebarOpen: true,
theme: 'light',
activeTab: 'dashboard',
setSidebarOpen: (open) => set({ sidebarOpen: open }),
setTheme: (theme) => set({ theme }),
setActiveTab: (tab) => set({ activeTab: tab }),
}),
{
name: 'ui-storage',
storage: createJSONStorage(() => localStorage),
}
)
);
// Agent store
interface AgentState {
agents: Agent[];
selectedAgent: Agent | null;
loading: boolean;
error: string | null;
setAgents: (agents: Agent[]) => void;
setSelectedAgent: (agent: Agent | null) => void;
setLoading: (loading: boolean) => void;
setError: (error: string | null) => void;
updateAgentStatus: (agentId: string, status: Agent['status'], lastCheckin: string) => void;
addAgent: (agent: Agent) => void;
removeAgent: (agentId: string) => void;
}
export const useAgentStore = create<AgentState>((set, get) => ({
agents: [],
selectedAgent: null,
loading: false,
error: null,
setAgents: (agents) => set({ agents }),
setSelectedAgent: (agent) => set({ selectedAgent: agent }),
setLoading: (loading) => set({ loading }),
setError: (error) => set({ error }),
updateAgentStatus: (agentId, status, lastCheckin) => {
const { agents } = get();
const updatedAgents = agents.map(agent =>
agent.id === agentId
? { ...agent, status, last_checkin: lastCheckin }
: agent
);
set({ agents: updatedAgents });
},
addAgent: (agent) => {
const { agents } = get();
set({ agents: [...agents, agent] });
},
removeAgent: (agentId) => {
const { agents } = get();
set({ agents: agents.filter(agent => agent.id !== agentId) });
},
}));
// Updates store
interface UpdateState {
updates: UpdatePackage[];
selectedUpdate: UpdatePackage | null;
filters: FilterState;
loading: boolean;
error: string | null;
setUpdates: (updates: UpdatePackage[]) => void;
setSelectedUpdate: (update: UpdatePackage | null) => void;
setFilters: (filters: Partial<FilterState>) => void;
setLoading: (loading: boolean) => void;
setError: (error: string | null) => void;
updateUpdateStatus: (updateId: string, status: UpdatePackage['status']) => void;
bulkUpdateStatus: (updateIds: string[], status: UpdatePackage['status']) => void;
}
export const useUpdateStore = create<UpdateState>((set, get) => ({
updates: [],
selectedUpdate: null,
filters: {
status: [],
severity: [],
type: [],
search: '',
},
loading: false,
error: null,
setUpdates: (updates) => set({ updates }),
setSelectedUpdate: (update) => set({ selectedUpdate: update }),
setLoading: (loading) => set({ loading }),
setError: (error) => set({ error }),
setFilters: (newFilters) => {
const { filters } = get();
set({ filters: { ...filters, ...newFilters } });
},
updateUpdateStatus: (updateId, status) => {
const { updates } = get();
const updatedUpdates = updates.map(update =>
update.id === updateId
? { ...update, status, updated_at: new Date().toISOString() }
: update
);
set({ updates: updatedUpdates });
},
bulkUpdateStatus: (updateIds, status) => {
const { updates } = get();
const updatedUpdates = updates.map(update =>
updateIds.includes(update.id)
? { ...update, status, updated_at: new Date().toISOString() }
: update
);
set({ updates: updatedUpdates });
},
}));
// Real-time updates store
interface RealtimeState {
isConnected: boolean;
lastUpdate: string | null;
notifications: Array<{
id: string;
type: 'info' | 'success' | 'warning' | 'error';
title: string;
message: string;
timestamp: string;
read: boolean;
}>;
setConnected: (connected: boolean) => void;
setLastUpdate: (timestamp: string) => void;
addNotification: (notification: Omit<typeof RealtimeState.prototype.notifications[0], 'id' | 'timestamp' | 'read'>) => void;
markNotificationRead: (id: string) => void;
clearNotifications: () => void;
}
export const useRealtimeStore = create<RealtimeState>((set, get) => ({
isConnected: false,
lastUpdate: null,
notifications: [],
setConnected: (isConnected) => set({ isConnected }),
setLastUpdate: (lastUpdate) => set({ lastUpdate }),
addNotification: (notification) => {
const { notifications } = get();
const newNotification = {
...notification,
id: Math.random().toString(36).substring(7),
timestamp: new Date().toISOString(),
read: false,
};
set({ notifications: [newNotification, ...notifications] });
},
markNotificationRead: (id) => {
const { notifications } = get();
const updatedNotifications = notifications.map(notification =>
notification.id === id ? { ...notification, read: true } : notification
);
set({ notifications: updatedNotifications });
},
clearNotifications: () => set({ notifications: [] }),
}));
// Settings store
interface SettingsState {
autoRefresh: boolean;
refreshInterval: number;
notificationsEnabled: boolean;
compactView: boolean;
setAutoRefresh: (enabled: boolean) => void;
setRefreshInterval: (interval: number) => void;
setNotificationsEnabled: (enabled: boolean) => void;
setCompactView: (enabled: boolean) => void;
}
export const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
autoRefresh: true,
refreshInterval: 30000, // 30 seconds
notificationsEnabled: true,
compactView: false,
setAutoRefresh: (autoRefresh) => set({ autoRefresh }),
setRefreshInterval: (refreshInterval) => set({ refreshInterval }),
setNotificationsEnabled: (notificationsEnabled) => set({ notificationsEnabled }),
setCompactView: (compactView) => set({ compactView }),
}),
{
name: 'settings-storage',
storage: createJSONStorage(() => localStorage),
}
)
);

View File

@@ -0,0 +1,247 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
// Utility function for combining class names
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// Date formatting utilities
export const formatDate = (dateString: string): string => {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
export const formatRelativeTime = (dateString: string): string => {
const date = new Date(dateString);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffMins < 1) {
return 'Just now';
} else if (diffMins < 60) {
return `${diffMins} minute${diffMins !== 1 ? 's' : ''} ago`;
} else if (diffHours < 24) {
return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`;
} else if (diffDays < 7) {
return `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`;
} else {
return formatDate(dateString);
}
};
export const isOnline = (lastCheckin: string): boolean => {
const lastCheck = new Date(lastCheckin);
const now = new Date();
const diffMs = now.getTime() - lastCheck.getTime();
const diffMins = Math.floor(diffMs / 60000);
return diffMins < 10; // Consider online if checked in within 10 minutes
};
// Size formatting utilities
export const formatBytes = (bytes: number): string => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
// Version comparison utilities
export const versionCompare = (v1: string, v2: string): number => {
const parts1 = v1.split('.').map(Number);
const parts2 = v2.split('.').map(Number);
const maxLength = Math.max(parts1.length, parts2.length);
for (let i = 0; i < maxLength; i++) {
const part1 = parts1[i] || 0;
const part2 = parts2[i] || 0;
if (part1 > part2) return 1;
if (part1 < part2) return -1;
}
return 0;
};
// Status and severity utilities
export const getStatusColor = (status: string): string => {
switch (status) {
case 'online':
return 'text-success-600 bg-success-100';
case 'offline':
return 'text-danger-600 bg-danger-100';
case 'pending':
return 'text-warning-600 bg-warning-100';
case 'approved':
case 'scheduled':
return 'text-blue-600 bg-blue-100';
case 'installing':
return 'text-indigo-600 bg-indigo-100';
case 'installed':
return 'text-success-600 bg-success-100';
case 'failed':
return 'text-danger-600 bg-danger-100';
default:
return 'text-gray-600 bg-gray-100';
}
};
export const getSeverityColor = (severity: string): string => {
switch (severity) {
case 'critical':
return 'text-danger-600 bg-danger-100';
case 'high':
return 'text-warning-600 bg-warning-100';
case 'medium':
return 'text-blue-600 bg-blue-100';
case 'low':
return 'text-gray-600 bg-gray-100';
default:
return 'text-gray-600 bg-gray-100';
}
};
export const getPackageTypeIcon = (type: string): string => {
switch (type) {
case 'apt':
return '📦';
case 'docker':
return '🐳';
case 'yum':
case 'dnf':
return '🐧';
case 'windows':
return '🪟';
case 'winget':
return '📱';
default:
return '📋';
}
};
// Filter and search utilities
export const filterUpdates = (
updates: any[],
filters: {
status: string[];
severity: string[];
type: string[];
search: string;
}
): any[] => {
return updates.filter(update => {
// Status filter
if (filters.status.length > 0 && !filters.status.includes(update.status)) {
return false;
}
// Severity filter
if (filters.severity.length > 0 && !filters.severity.includes(update.severity)) {
return false;
}
// Type filter
if (filters.type.length > 0 && !filters.type.includes(update.package_type)) {
return false;
}
// Search filter
if (filters.search) {
const searchLower = filters.search.toLowerCase();
return (
update.package_name.toLowerCase().includes(searchLower) ||
update.current_version.toLowerCase().includes(searchLower) ||
update.available_version.toLowerCase().includes(searchLower)
);
}
return true;
});
};
// Error handling utilities
export const getErrorMessage = (error: any): string => {
if (typeof error === 'string') {
return error;
}
if (error?.message) {
return error.message;
}
if (error?.response?.data?.message) {
return error.response.data.message;
}
return 'An unexpected error occurred';
};
// Debounce utility
export const debounce = <T extends (...args: any[]) => any>(
func: T,
wait: number
): ((...args: Parameters<T>) => void) => {
let timeout: NodeJS.Timeout;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
};
// Local storage utilities
export const storage = {
get: (key: string): string | null => {
try {
return localStorage.getItem(key);
} catch {
return null;
}
},
set: (key: string, value: string): void => {
try {
localStorage.setItem(key, value);
} catch {
// Silent fail for storage issues
}
},
remove: (key: string): void => {
try {
localStorage.removeItem(key);
} catch {
// Silent fail for storage issues
}
},
getJSON: <T = any>(key: string): T | null => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch {
return null;
}
},
setJSON: (key: string, value: any): void => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch {
// Silent fail for storage issues
}
},
};