mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat: initialize monorepo with package.json for CCW workflow platform
This commit is contained in:
@@ -893,6 +893,10 @@ export interface Skill {
|
||||
version?: string;
|
||||
author?: string;
|
||||
location?: 'project' | 'user';
|
||||
folderName?: string;
|
||||
path?: string;
|
||||
allowedTools?: string[];
|
||||
supportingFiles?: string[];
|
||||
}
|
||||
|
||||
export interface SkillsResponse {
|
||||
@@ -967,6 +971,23 @@ export async function disableSkill(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch detailed information about a specific skill
|
||||
* @param skillName - Name of the skill to fetch
|
||||
* @param location - Location of the skill (project or user)
|
||||
* @param projectPath - Optional project path
|
||||
*/
|
||||
export async function fetchSkillDetail(
|
||||
skillName: string,
|
||||
location: 'project' | 'user',
|
||||
projectPath?: string
|
||||
): Promise<{ skill: Skill }> {
|
||||
const url = projectPath
|
||||
? `/api/skills/${encodeURIComponent(skillName)}?location=${location}&path=${encodeURIComponent(projectPath)}`
|
||||
: `/api/skills/${encodeURIComponent(skillName)}?location=${location}`;
|
||||
return fetchApi<{ skill: Skill }>(url);
|
||||
}
|
||||
|
||||
// ========== Commands API ==========
|
||||
|
||||
export interface Command {
|
||||
@@ -1214,6 +1235,34 @@ export async function deleteMemory(memoryId: string, projectPath?: string): Prom
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive a memory entry for a specific workspace
|
||||
* @param memoryId - Memory ID to archive
|
||||
* @param projectPath - Optional project path to filter data by workspace
|
||||
*/
|
||||
export async function archiveMemory(memoryId: string, projectPath?: string): Promise<void> {
|
||||
const url = projectPath
|
||||
? `/api/core-memory/memories/${encodeURIComponent(memoryId)}/archive?path=${encodeURIComponent(projectPath)}`
|
||||
: `/api/core-memory/memories/${encodeURIComponent(memoryId)}/archive`;
|
||||
return fetchApi<void>(url, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unarchive a memory entry for a specific workspace
|
||||
* @param memoryId - Memory ID to unarchive
|
||||
* @param projectPath - Optional project path to filter data by workspace
|
||||
*/
|
||||
export async function unarchiveMemory(memoryId: string, projectPath?: string): Promise<void> {
|
||||
const url = projectPath
|
||||
? `/api/core-memory/memories/${encodeURIComponent(memoryId)}/unarchive?path=${encodeURIComponent(projectPath)}`
|
||||
: `/api/core-memory/memories/${encodeURIComponent(memoryId)}/unarchive`;
|
||||
return fetchApi<void>(url, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Project Overview API ==========
|
||||
|
||||
export interface TechnologyStack {
|
||||
@@ -3829,6 +3878,9 @@ export interface CliSettingsEndpoint {
|
||||
};
|
||||
model?: string;
|
||||
includeCoAuthoredBy?: boolean;
|
||||
settingsFile?: string;
|
||||
availableModels?: string[];
|
||||
tags?: string[];
|
||||
};
|
||||
enabled: boolean;
|
||||
createdAt: string;
|
||||
@@ -3859,6 +3911,9 @@ export interface SaveCliSettingsRequest {
|
||||
};
|
||||
model?: string;
|
||||
includeCoAuthoredBy?: boolean;
|
||||
settingsFile?: string;
|
||||
availableModels?: string[];
|
||||
tags?: string[];
|
||||
};
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
110
ccw/frontend/src/lib/chartTheme.ts
Normal file
110
ccw/frontend/src/lib/chartTheme.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
// ========================================
|
||||
// Chart Theme Configuration
|
||||
// ========================================
|
||||
// Extracts Tailwind CSS custom properties for Recharts color palette
|
||||
|
||||
/**
|
||||
* Chart color palette extracted from CSS custom properties
|
||||
*/
|
||||
export interface ChartColors {
|
||||
primary: string;
|
||||
success: string;
|
||||
warning: string;
|
||||
info: string;
|
||||
destructive: string;
|
||||
indigo: string;
|
||||
orange: string;
|
||||
muted: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts HSL CSS variable to hex color for Recharts
|
||||
* @param hslString - HSL string in format "h s% l%"
|
||||
* @returns Hex color string
|
||||
*/
|
||||
function hslToHex(hslString: string): string {
|
||||
const [h, s, l] = hslString.split(' ').map((v) => parseFloat(v));
|
||||
const hue = h / 360;
|
||||
const saturation = s / 100;
|
||||
const lightness = l / 100;
|
||||
|
||||
const hueToRgb = (p: number, q: number, t: number) => {
|
||||
if (t < 0) t += 1;
|
||||
if (t > 1) t -= 1;
|
||||
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
||||
if (t < 1 / 2) return q;
|
||||
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
||||
return p;
|
||||
};
|
||||
|
||||
const q = lightness < 0.5 ? lightness * (1 + saturation) : lightness + saturation - lightness * saturation;
|
||||
const p = 2 * lightness - q;
|
||||
|
||||
const r = Math.round(hueToRgb(p, q, hue + 1 / 3) * 255);
|
||||
const g = Math.round(hueToRgb(p, q, hue) * 255);
|
||||
const b = Math.round(hueToRgb(p, q, hue - 1 / 3) * 255);
|
||||
|
||||
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chart colors from CSS custom properties
|
||||
* @returns Chart color palette object
|
||||
*/
|
||||
export function getChartColors(): ChartColors {
|
||||
const root = document.documentElement;
|
||||
const style = getComputedStyle(root);
|
||||
|
||||
const getColor = (varName: string): string => {
|
||||
const hsl = style.getPropertyValue(varName).trim();
|
||||
if (!hsl) {
|
||||
// Fallback colors if CSS variables are not available
|
||||
const fallbacks: Record<string, string> = {
|
||||
'--primary': '220 60% 65%',
|
||||
'--success': '142 71% 45%',
|
||||
'--warning': '38 92% 50%',
|
||||
'--info': '220 60% 60%',
|
||||
'--destructive': '8 75% 55%',
|
||||
'--indigo': '239 65% 60%',
|
||||
'--orange': '25 90% 55%',
|
||||
'--muted': '220 20% 96%',
|
||||
};
|
||||
return hslToHex(fallbacks[varName] || '220 60% 65%');
|
||||
}
|
||||
return hslToHex(hsl);
|
||||
};
|
||||
|
||||
return {
|
||||
primary: getColor('--primary'),
|
||||
success: getColor('--success'),
|
||||
warning: getColor('--warning'),
|
||||
info: getColor('--info'),
|
||||
destructive: getColor('--destructive'),
|
||||
indigo: getColor('--indigo'),
|
||||
orange: getColor('--orange'),
|
||||
muted: getColor('--muted'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Status color mapping for workflow status pie chart
|
||||
*/
|
||||
export const STATUS_COLORS: Record<string, keyof ChartColors> = {
|
||||
planning: 'info',
|
||||
in_progress: 'primary',
|
||||
completed: 'success',
|
||||
paused: 'warning',
|
||||
archived: 'muted',
|
||||
};
|
||||
|
||||
/**
|
||||
* Task type color mapping for task type bar chart
|
||||
*/
|
||||
export const TASK_TYPE_COLORS: Record<string, keyof ChartColors> = {
|
||||
implementation: 'primary',
|
||||
bugfix: 'destructive',
|
||||
refactor: 'indigo',
|
||||
documentation: 'info',
|
||||
testing: 'success',
|
||||
other: 'muted',
|
||||
};
|
||||
218
ccw/frontend/src/lib/webVitals.ts
Normal file
218
ccw/frontend/src/lib/webVitals.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
// ========================================
|
||||
// Web Vitals Performance Monitoring
|
||||
// ========================================
|
||||
// Measures and logs Core Web Vitals metrics (LCP, INP, CLS)
|
||||
// These are essential for measuring page performance and user experience
|
||||
|
||||
import {
|
||||
onCLS,
|
||||
onFCP,
|
||||
onINP,
|
||||
onLCP,
|
||||
onTTFB,
|
||||
type Metric,
|
||||
} from 'web-vitals';
|
||||
|
||||
/**
|
||||
* Threshold values for Web Vitals (WCAG recommendations)
|
||||
* @see https://web.dev/metrics/
|
||||
*/
|
||||
export const VITALS_THRESHOLDS = {
|
||||
LCP: 2500, // Largest Contentful Paint - target < 2.5s
|
||||
INP: 200, // Interaction to Next Paint - target < 200ms (replaces FID)
|
||||
CLS: 0.1, // Cumulative Layout Shift - target < 0.1
|
||||
FCP: 1800, // First Contentful Paint - target < 1.8s
|
||||
TTFB: 600, // Time to First Byte - target < 600ms
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Web Vitals metric entry
|
||||
*/
|
||||
export interface VitalsMetric extends Metric {
|
||||
vitalsName: string;
|
||||
isBad: boolean;
|
||||
rating: 'good' | 'needs-improvement' | 'poor';
|
||||
}
|
||||
|
||||
/**
|
||||
* Web Vitals callback function
|
||||
*/
|
||||
export type VitalsCallback = (metric: VitalsMetric) => void;
|
||||
|
||||
/**
|
||||
* Determine if a metric is within good range
|
||||
*/
|
||||
function isGoodMetric(name: string, value: number): boolean {
|
||||
switch (name) {
|
||||
case 'LCP':
|
||||
return value <= VITALS_THRESHOLDS.LCP;
|
||||
case 'INP':
|
||||
return value <= VITALS_THRESHOLDS.INP;
|
||||
case 'CLS':
|
||||
return value <= VITALS_THRESHOLDS.CLS;
|
||||
case 'FCP':
|
||||
return value <= VITALS_THRESHOLDS.FCP;
|
||||
case 'TTFB':
|
||||
return value <= VITALS_THRESHOLDS.TTFB;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rating for a metric
|
||||
*/
|
||||
function getMetricRating(name: string, value: number): 'good' | 'needs-improvement' | 'poor' {
|
||||
const goodThreshold = VITALS_THRESHOLDS[name as keyof typeof VITALS_THRESHOLDS];
|
||||
if (!goodThreshold) return 'good';
|
||||
|
||||
// Good threshold
|
||||
if (value <= goodThreshold) {
|
||||
return 'good';
|
||||
}
|
||||
|
||||
// Poor threshold (typically 1.25x of good threshold)
|
||||
const poorThreshold = goodThreshold * 1.25;
|
||||
if (value <= poorThreshold) {
|
||||
return 'needs-improvement';
|
||||
}
|
||||
|
||||
return 'poor';
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Web Vitals monitoring
|
||||
*
|
||||
* @param callback - Function to call when metrics are collected
|
||||
* @param reportAllMetrics - Include FCP and TTFB (optional, default false)
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* initWebVitals((metric) => {
|
||||
* console.log(`${metric.name}: ${metric.value}`);
|
||||
* if (metric.isBad) {
|
||||
* analytics.trackVitalsIssue(metric);
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function initWebVitals(
|
||||
callback: VitalsCallback,
|
||||
reportAllMetrics = false
|
||||
): void {
|
||||
// Core Web Vitals (always measured)
|
||||
onLCP((metric) => {
|
||||
const m: VitalsMetric = {
|
||||
...metric,
|
||||
vitalsName: 'LCP',
|
||||
isBad: !isGoodMetric('LCP', metric.value),
|
||||
rating: getMetricRating('LCP', metric.value),
|
||||
};
|
||||
callback(m);
|
||||
});
|
||||
|
||||
onINP((metric) => {
|
||||
const m: VitalsMetric = {
|
||||
...metric,
|
||||
vitalsName: 'INP',
|
||||
isBad: !isGoodMetric('INP', metric.value),
|
||||
rating: getMetricRating('INP', metric.value),
|
||||
};
|
||||
callback(m);
|
||||
});
|
||||
|
||||
onCLS((metric) => {
|
||||
const m: VitalsMetric = {
|
||||
...metric,
|
||||
vitalsName: 'CLS',
|
||||
isBad: !isGoodMetric('CLS', metric.value),
|
||||
rating: getMetricRating('CLS', metric.value),
|
||||
};
|
||||
callback(m);
|
||||
});
|
||||
|
||||
// Optional metrics
|
||||
if (reportAllMetrics) {
|
||||
onFCP((metric) => {
|
||||
const m: VitalsMetric = {
|
||||
...metric,
|
||||
vitalsName: 'FCP',
|
||||
isBad: !isGoodMetric('FCP', metric.value),
|
||||
rating: getMetricRating('FCP', metric.value),
|
||||
};
|
||||
callback(m);
|
||||
});
|
||||
|
||||
onTTFB((metric) => {
|
||||
const m: VitalsMetric = {
|
||||
...metric,
|
||||
vitalsName: 'TTFB',
|
||||
isBad: !isGoodMetric('TTFB', metric.value),
|
||||
rating: getMetricRating('TTFB', metric.value),
|
||||
};
|
||||
callback(m);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log Web Vitals metrics to console
|
||||
* Useful for development and debugging
|
||||
*/
|
||||
export function logWebVitals(): void {
|
||||
initWebVitals((metric) => {
|
||||
const style = metric.isBad
|
||||
? 'background: #ff6b6b; color: white; padding: 2px 6px; border-radius: 3px;'
|
||||
: 'background: #51cf66; color: white; padding: 2px 6px; border-radius: 3px;';
|
||||
|
||||
console.log(
|
||||
`%c${metric.vitalsName}%c ${metric.value.toFixed(2)}ms (${metric.rating})`,
|
||||
style,
|
||||
'background: none;'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Web Vitals to analytics service
|
||||
*
|
||||
* @param endpoint - Analytics endpoint URL
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* sendWebVitalsToAnalytics('/api/analytics/vitals');
|
||||
* ```
|
||||
*/
|
||||
export function sendWebVitalsToAnalytics(endpoint: string): void {
|
||||
initWebVitals((metric) => {
|
||||
// Only send bad metrics to reduce noise
|
||||
if (!metric.isBad) return;
|
||||
|
||||
// Queue the metric and send in batches
|
||||
const data = {
|
||||
metric: metric.vitalsName,
|
||||
value: metric.value,
|
||||
rating: metric.rating,
|
||||
delta: metric.delta,
|
||||
url: window.location.href,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Use sendBeacon for reliability (survives page unload)
|
||||
if (navigator.sendBeacon) {
|
||||
navigator.sendBeacon(endpoint, JSON.stringify(data));
|
||||
} else {
|
||||
// Fallback to fetch
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
keepalive: true,
|
||||
}).catch(() => {
|
||||
// Silently fail to avoid disrupting user experience
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default initWebVitals;
|
||||
Reference in New Issue
Block a user