feat: project cover art with AI generation, branding rename, RBAC fix, computation graph

- Add DALL-E cover art generation for projects (Azure OpenAI + standard OpenAI)
- CoverArtSection component with generate/upload/remove/focus-point controls
- Client-side image compression (10MB input → WebP/JPEG, max 1920px)
- DALL-E settings in admin panel (deployment, endpoint, API key)
- MCP assistant tools for cover art (generate_project_cover, remove_project_cover)
- Rename "Planarchy" → "plANARCHY" across all UI-facing text (13 files)
- Fix hardcoded canEdit={true} on project detail page — now checks user role
- Computation graph visualization (2D/3D) for calculation rules
- OG image and OpenGraph metadata

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-03-18 11:31:56 +01:00
parent 21af720f90
commit 093e13b88f
86 changed files with 5623 additions and 744 deletions
@@ -10,6 +10,7 @@ import { SkillRadarChart } from "./SkillRadarChart.js";
import { AiSummaryCard } from "./AiSummaryCard.js";
import { SkillMatrixUpload } from "./SkillMatrixUpload.js";
import { usePermissions } from "~/hooks/usePermissions.js";
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
interface ResourceDetailProps {
resourceId: string;
@@ -46,10 +47,10 @@ const allocationStatusColor: Record<string, string> = {
CANCELLED: "bg-red-100 text-red-500",
};
function StatCard({ label, value, sub }: { label: string; value: string | number; sub?: string }) {
function StatCard({ label, value, sub, tooltip }: { label: string; value: string | number; sub?: string; tooltip?: string }) {
return (
<div className="bg-white rounded-xl border border-gray-200 p-4">
<div className="text-xs text-gray-500 mb-1">{label}</div>
<div className="text-xs text-gray-500 mb-1 flex items-center">{label}{tooltip && <InfoTooltip content={tooltip} />}</div>
<div className="text-xl font-bold text-gray-900">{value}</div>
{sub && <div className="text-xs text-gray-400 mt-0.5">{sub}</div>}
</div>
@@ -276,22 +277,26 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
<StatCard
label="LCR"
value={`${(resource.lcrCents / 100).toFixed(0)} ${resource.currency}/h`}
tooltip="Loaded Cost Rate: fully-loaded hourly cost including salary, benefits, and overhead. Used in budget calculations."
/>
)}
{canViewCosts && (
<StatCard
label="UCR"
value={`${(resource.ucrCents / 100).toFixed(0)} ${resource.currency}/h`}
tooltip="Unit Cost Rate: the rate charged to the client or project for this resource's time."
/>
)}
<StatCard
label="Chargeability Target"
value={`${resource.chargeabilityTarget}%`}
tooltip="The percentage of working time this resource is expected to spend on chargeable/billable work."
/>
{canViewCosts && (
<StatCard
label="Actual (this month)"
value={chargeStats != null ? `${chargeStats.actualChargeability}%` : "—"}
tooltip="Actual chargeability = chargeable hours / total available hours x 100 for the current month."
sub={
includeProposedChargeability
? "Incl. proposed + imported TBD planning"
@@ -303,12 +308,14 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
<StatCard
label="Expected (this month)"
value={chargeStats != null ? `${chargeStats.expectedChargeability}%` : "—"}
tooltip="Expected chargeability based on all non-cancelled bookings for the current month."
sub="All non-cancelled bookings"
/>
)}
<StatCard
label="Hours This Month"
value={`${Math.round(totalHoursThisMonth)}h`}
tooltip="Sum of allocated hours for all active projects in the current calendar month."
sub={`${activeProjectIds.size} active project${activeProjectIds.size !== 1 ? "s" : ""}`}
/>
{canViewScores && resourceWithMeta.valueScore != null && (
@@ -316,6 +323,7 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
<StatCard
label="Value Score"
value={resourceWithMeta.valueScore}
tooltip="Composite score (0-100) combining skill depth, breadth, cost efficiency, chargeability, and experience. Hover for breakdown."
sub="Price/Quality"
/>
{resourceWithMeta.valueScoreBreakdown && (
@@ -391,7 +399,7 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
{/* Main Skills Badges */}
{mainSkills.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h2 className="text-sm font-semibold text-gray-800 mb-3">Main Skills</h2>
<h2 className="text-sm font-semibold text-gray-800 mb-3 flex items-center">Main Skills<InfoTooltip content="Up to 2 skills flagged as primary strengths. Used for staffing matching priority." /></h2>
<div className="flex flex-wrap gap-2">
{mainSkills.map((s) => (
<span
@@ -415,7 +423,7 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
{/* Roles */}
{resourceRoles.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h2 className="text-sm font-semibold text-gray-800 mb-3">Roles</h2>
<h2 className="text-sm font-semibold text-gray-800 mb-3 flex items-center">Roles<InfoTooltip content="Job functions assigned to this resource. The primary role is used in staffing and timeline displays." /></h2>
<div className="flex flex-wrap gap-2">
{resourceRoles.map((rr) => (
<span
@@ -438,7 +446,7 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
{/* Skills */}
{skills.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h2 className="text-sm font-semibold text-gray-800 mb-3">Skills</h2>
<h2 className="text-sm font-semibold text-gray-800 mb-3 flex items-center">Skills<InfoTooltip content="Full skill inventory with proficiency level (1-5) and years of experience. Imported via skill matrix XLSX." /></h2>
<div className="flex flex-wrap gap-2">
{skills.map((s) => (
<span