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
@@ -1,6 +1,7 @@
"use client";
import { useState, useEffect } from "react";
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
import { trpc } from "~/lib/trpc/client.js";
const INPUT_CLASS = "app-input";
@@ -90,6 +91,11 @@ export function SystemSettingsClient() {
const [scoreSaved, setScoreSaved] = useState(false);
const [recomputeResult, setRecomputeResult] = useState<{ updated: number } | null>(null);
// DALL-E settings
const [dalleDeployment, setDalleDeployment] = useState("");
const [dalleEndpoint, setDalleEndpoint] = useState("");
const [dalleApiKey, setDalleApiKey] = useState("");
// SMTP settings
const [smtpHost, setSmtpHost] = useState("");
const [smtpPort, setSmtpPort] = useState(587);
@@ -131,6 +137,9 @@ export function SystemSettingsClient() {
if (settings.scoreVisibleRoles) {
setScoreVisibleRoles(settings.scoreVisibleRoles as SystemRole[]);
}
// DALL-E
setDalleDeployment(settings.azureDalleDeployment ?? "");
setDalleEndpoint(settings.azureDalleEndpoint ?? "");
// SMTP
setSmtpHost(settings.smtpHost ?? "");
setSmtpPort(settings.smtpPort ?? 587);
@@ -269,6 +278,9 @@ export function SystemSettingsClient() {
aiTemperature: temperature,
aiSummaryPrompt: summaryPrompt || undefined,
...(apiKey ? { azureOpenAiApiKey: apiKey } : {}),
azureDalleDeployment: dalleDeployment,
azureDalleEndpoint: provider === "azure" && dalleEndpoint ? dalleEndpoint : undefined,
...(dalleApiKey ? { azureDalleApiKey: dalleApiKey } : {}),
});
}
@@ -293,8 +305,8 @@ export function SystemSettingsClient() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
<div className={PANEL_CLASS}>
<h2 className="text-sm font-semibold uppercase tracking-[0.18em] text-gray-700 dark:text-gray-200">
AI Provider
<h2 className="text-sm font-semibold uppercase tracking-[0.18em] text-gray-700 dark:text-gray-200 flex items-center">
AI Provider <InfoTooltip content="Configure the AI service used for generating resource skill profile summaries. Either OpenAI directly or Azure OpenAI Service." />
</h2>
{/* Provider toggle */}
@@ -522,8 +534,8 @@ export function SystemSettingsClient() {
{/* Generation settings */}
<div className={PANEL_CLASS}>
<h2 className="text-sm font-semibold uppercase tracking-[0.18em] text-gray-700 dark:text-gray-200">
Generation Settings
<h2 className="text-sm font-semibold uppercase tracking-[0.18em] text-gray-700 dark:text-gray-200 flex items-center">
Generation Settings <InfoTooltip content="Fine-tune how the AI generates skill profile summaries. These settings affect output length, creativity, and the prompt template." />
</h2>
{/* Max completion tokens */}
@@ -989,11 +1001,76 @@ export function SystemSettingsClient() {
</div>
</div>
{/* ── DALL-E Image Generation ────────────────────────────────── */}
<div className={PANEL_CLASS}>
<div>
<h2 className="text-base font-semibold text-gray-900 dark:text-gray-100 flex items-center">
DALL-E Image Generation <InfoTooltip content="Configure the DALL-E model used for generating project cover art. Uses the same provider (OpenAI / Azure) as the chat model above." />
</h2>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Used to generate AI cover art for projects. Leave blank to disable AI cover generation.
</p>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label className={LABEL_CLASS}>
<span className="flex items-center">
Deployment Name <InfoTooltip content="The DALL-E model deployment name (e.g. dall-e-3). For OpenAI this is the model name, for Azure it is the deployment name." />
</span>
</label>
<input
type="text"
className={INPUT_CLASS}
value={dalleDeployment}
onChange={(e) => setDalleDeployment(e.target.value)}
placeholder="dall-e-3"
/>
</div>
{provider === "azure" && (
<>
<div>
<label className={LABEL_CLASS}>
<span className="flex items-center">
Endpoint <InfoTooltip content="Azure endpoint for the DALL-E deployment. Leave empty to use the same endpoint as the chat model." />
</span>
</label>
<input
type="text"
className={INPUT_CLASS}
value={dalleEndpoint}
onChange={(e) => setDalleEndpoint(e.target.value)}
placeholder="Leave empty to use same endpoint as chat"
/>
</div>
<div>
<label className={LABEL_CLASS}>
<span className="flex items-center">
API Key{" "}
<InfoTooltip content="API key for the DALL-E endpoint. Leave empty to use the same API key as the chat model." />
<span className="ml-1 text-xs font-normal text-gray-400">(optional)</span>
</span>
</label>
<input
type="password"
className={INPUT_CLASS}
value={dalleApiKey}
onChange={(e) => setDalleApiKey(e.target.value)}
placeholder="Leave empty to use same API key as chat"
/>
</div>
</>
)}
</div>
</div>
{/* ── SMTP / Email ──────────────────────────────────────────── */}
<div className={PANEL_CLASS}>
<div>
<h2 className="text-base font-semibold text-gray-900 dark:text-gray-100">
Email Notifications (SMTP)
<h2 className="text-base font-semibold text-gray-900 dark:text-gray-100 flex items-center">
Email Notifications (SMTP) <InfoTooltip content="Configure SMTP to send email notifications for vacation approvals/rejections. Without SMTP, only in-app notifications are sent." />
</h2>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Used to send email notifications when vacation requests are approved or rejected.
@@ -1002,7 +1079,7 @@ export function SystemSettingsClient() {
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label className={LABEL_CLASS}>SMTP Host</label>
<label className={LABEL_CLASS}><span className="flex items-center">SMTP Host <InfoTooltip content="The SMTP server hostname (e.g. smtp.gmail.com, smtp.office365.com)." /></span></label>
<input
type="text"
className={INPUT_CLASS}
@@ -1012,7 +1089,7 @@ export function SystemSettingsClient() {
/>
</div>
<div>
<label className={LABEL_CLASS}>SMTP Port</label>
<label className={LABEL_CLASS}><span className="flex items-center">SMTP Port <InfoTooltip content="Common ports: 587 (STARTTLS), 465 (SSL/TLS), 25 (unencrypted). Use 587 for most providers." /></span></label>
<input
type="number"
className={INPUT_CLASS}
@@ -1023,7 +1100,7 @@ export function SystemSettingsClient() {
/>
</div>
<div>
<label className={LABEL_CLASS}>SMTP Username</label>
<label className={LABEL_CLASS}><span className="flex items-center">SMTP Username <InfoTooltip content="Authentication username for the SMTP server. Often the same as the email address." /></span></label>
<input
type="text"
className={INPUT_CLASS}
@@ -1034,8 +1111,8 @@ export function SystemSettingsClient() {
/>
</div>
<div>
<label className={LABEL_CLASS}>
SMTP Password{" "}
<label className={LABEL_CLASS}><span className="flex items-center">
SMTP Password <InfoTooltip content="The SMTP authentication password. Stored encrypted. Leave blank to keep the existing password." />{" "}</span>
{settings?.hasSmtpPassword && (
<span className="text-gray-400 font-normal text-xs">
(set leave blank to keep)
@@ -1052,7 +1129,7 @@ export function SystemSettingsClient() {
/>
</div>
<div>
<label className={LABEL_CLASS}>From Address</label>
<label className={LABEL_CLASS}><span className="flex items-center">From Address <InfoTooltip content="The sender email address shown in notification emails (e.g. noreply@planarchy.app)." /></span></label>
<input
type="email"
className={INPUT_CLASS}
@@ -1111,8 +1188,8 @@ export function SystemSettingsClient() {
{/* ── Vacation Defaults ─────────────────────────────────────── */}
<div className={PANEL_CLASS}>
<div>
<h2 className="text-base font-semibold text-gray-900 dark:text-gray-100">
Vacation Defaults
<h2 className="text-base font-semibold text-gray-900 dark:text-gray-100 flex items-center">
Vacation Defaults <InfoTooltip content="Sets the default vacation entitlement applied when creating new resources or using the bulk-set tool in Vacation Management." />
</h2>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Default annual leave entitlement for new resources and the entitlement bulk-set tool.
@@ -1120,7 +1197,7 @@ export function SystemSettingsClient() {
</div>
<div className="max-w-xs">
<label className={LABEL_CLASS}>Default Annual Leave Days</label>
<label className={LABEL_CLASS}><span className="flex items-center">Default Annual Leave Days <InfoTooltip content="The number of vacation days granted per year. In Germany, the legal minimum is 20 days; 28-30 is common. This value is used when creating new entitlement records." /></span></label>
<input
type="number"
className={INPUT_CLASS}
@@ -1151,8 +1228,8 @@ export function SystemSettingsClient() {
<div className={PANEL_CLASS}>
<div>
<h2 className="text-base font-semibold text-gray-900 dark:text-gray-100">
Viewer Anonymization
<h2 className="text-base font-semibold text-gray-900 dark:text-gray-100 flex items-center">
Viewer Anonymization <InfoTooltip content="When enabled, all resource names, EIDs, and emails are replaced with stable fictional aliases (e.g. superhero names) in the UI. Real data stays in the database. Useful for demos and screenshots." />
</h2>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Global debug mode that keeps real identities in the database but replaces displayed