chore(format): Prettier über das gesamte Repo
ci / quality (push) Failing after 45s
ci / build (push) Has been skipped
deploy / deploy (push) Successful in 53s

Behebt die rote CI (format:check schlug an 36 Alt-Bestand-Dateien fehl:
migrations/meta-Snapshots, seed.ts, schemas/lead.ts, README u. a.) und erzwingt
nebenbei einen frischen web-Build beim Auto-Deploy (apps/web-Dateien geaendert
-> COPY-Layer invalidiert -> Vite baut neu).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Raffael SheikhandClaude Opus 4.8 committed 2026-06-07 17:50:07 +02:00
1 parent b340b45110
commit a1cf6f3883
36 files changed
+1559 -3510

No files matched your search

+7 -2
View File
@@ -111,13 +111,18 @@ export async function registerAuthRoutes(app: FastifyInstance): Promise<void> {
// die App ohne Authentik durchnavigierbar ist. Authentik kommt in der späteren
// Roadmap. Nur aktiv bei NODE_ENV=development.
if (env.NODE_ENV === 'development') {
const handler = async (req: import('fastify').FastifyRequest, reply: import('fastify').FastifyReply) => {
const handler = async (
req: import('fastify').FastifyRequest,
reply: import('fastify').FastifyReply,
) => {
const { getDb } = await import('../db.js');
const { user } = await import('@cockpit/db');
const { eq } = await import('drizzle-orm');
const { db } = getDb();
const found = (await db.select().from(user).where(eq(user.email, 'dejan@tool-ctrl.de')).limit(1))[0];
const found = (
await db.select().from(user).where(eq(user.email, 'dejan@tool-ctrl.de')).limit(1)
)[0];
const userRow = found
? found
: await upsertUserFromOidc({
+4 -1
View File
@@ -47,7 +47,10 @@ async function safeText(res: Response): Promise<string> {
* Startet ein laufendes Timesheet in Kimai (Clock-In). Liefert die Kimai-
* Timesheet-ID zurück oder null (nicht konfiguriert / Fehler).
*/
export async function kimaiClockIn(opts: { begin: Date; description?: string }): Promise<number | null> {
export async function kimaiClockIn(opts: {
begin: Date;
description?: string;
}): Promise<number | null> {
const cfg = kimaiConfig();
if (!cfg) {
logger.debug('kimai not configured — clock-in skipped');
+2 -1
View File
@@ -41,7 +41,8 @@ export const changeRouter = router({
const existing = (
await ctx.db.select().from(changeEntry).where(eq(changeEntry.id, id)).limit(1)
)[0];
if (!existing) throw new TRPCError({ code: 'NOT_FOUND', message: 'Eintrag nicht gefunden.' });
if (!existing)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Eintrag nicht gefunden.' });
return existing;
}
const [updated] = await ctx.db
+77 -67
View File
@@ -1,5 +1,15 @@
import { and, eq, ne, isNull, isNotNull, lt, count, asc, desc } from 'drizzle-orm';
import { area, project, blocker, dayEntry, user, risk, milestone, type Area, type Project } from '@cockpit/db';
import {
area,
project,
blocker,
dayEntry,
user,
risk,
milestone,
type Area,
type Project,
} from '@cockpit/db';
import { router, protectedProcedure } from '../trpc.js';
import { todayIso, previousWerktag } from '../lib/werktag.js';
import { aggregateAreaAmpel } from '../lib/ampel.js';
@@ -17,73 +27,73 @@ export const dashboardRouter = router({
showstopperRows,
milestoneRows,
] = await Promise.all([
ctx.db.select().from(area).orderBy(area.order),
ctx.db.select().from(project).where(eq(project.archiviert, false)),
ctx.db.select().from(blocker).where(isNull(blocker.resolvedAt)),
ctx.db
.select({ userId: dayEntry.userId, status: dayEntry.status })
.from(dayEntry)
.where(eq(dayEntry.date, todayDate)),
ctx.db.select({ c: count() }).from(user).where(eq(user.active, true)),
ctx.db
.select({
id: dayEntry.id,
userId: dayEntry.userId,
userName: user.name,
date: dayEntry.date,
ampel: dayEntry.ampel,
trend: dayEntry.trend,
reflectionText: dayEntry.reflectionText,
publishedAt: dayEntry.publishedAt,
})
.from(dayEntry)
.innerJoin(user, eq(user.id, dayEntry.userId))
// „Gestern im Team": volle Reflexionen vor heute (heutige bleiben privat
// bis morgen). Unabhängig vom publish-Cron.
.where(and(lt(dayEntry.date, todayDate), eq(dayEntry.status, 'full')))
.orderBy(desc(dayEntry.date), desc(dayEntry.reflectedAt))
.limit(50),
ctx.db
.select({
id: risk.id,
title: risk.title,
projectId: risk.projectId,
projectName: project.name,
})
.from(risk)
.innerJoin(project, eq(project.id, risk.projectId))
.where(
and(
eq(risk.showstopper, true),
eq(risk.kind, 'risiko'),
ne(risk.status, 'geschlossen'),
eq(project.archiviert, false),
),
ctx.db.select().from(area).orderBy(area.order),
ctx.db.select().from(project).where(eq(project.archiviert, false)),
ctx.db.select().from(blocker).where(isNull(blocker.resolvedAt)),
ctx.db
.select({ userId: dayEntry.userId, status: dayEntry.status })
.from(dayEntry)
.where(eq(dayEntry.date, todayDate)),
ctx.db.select({ c: count() }).from(user).where(eq(user.active, true)),
ctx.db
.select({
id: dayEntry.id,
userId: dayEntry.userId,
userName: user.name,
date: dayEntry.date,
ampel: dayEntry.ampel,
trend: dayEntry.trend,
reflectionText: dayEntry.reflectionText,
publishedAt: dayEntry.publishedAt,
})
.from(dayEntry)
.innerJoin(user, eq(user.id, dayEntry.userId))
// „Gestern im Team": volle Reflexionen vor heute (heutige bleiben privat
// bis morgen). Unabhängig vom publish-Cron.
.where(and(lt(dayEntry.date, todayDate), eq(dayEntry.status, 'full')))
.orderBy(desc(dayEntry.date), desc(dayEntry.reflectedAt))
.limit(50),
ctx.db
.select({
id: risk.id,
title: risk.title,
projectId: risk.projectId,
projectName: project.name,
})
.from(risk)
.innerJoin(project, eq(project.id, risk.projectId))
.where(
and(
eq(risk.showstopper, true),
eq(risk.kind, 'risiko'),
ne(risk.status, 'geschlossen'),
eq(project.archiviert, false),
),
// Anstehende Meilensteine (offen/überfällig, mit Datum) projektübergreifend.
ctx.db
.select({
id: milestone.id,
title: milestone.title,
due: milestone.due,
status: milestone.status,
projectId: milestone.projectId,
projectName: project.name,
projectLogoUrl: project.logoUrl,
areaId: project.areaId,
})
.from(milestone)
.innerJoin(project, eq(project.id, milestone.projectId))
.where(
and(
ne(milestone.status, 'done'),
isNotNull(milestone.due),
eq(project.archiviert, false),
),
)
.orderBy(asc(milestone.due))
.limit(15),
]);
),
// Anstehende Meilensteine (offen/überfällig, mit Datum) projektübergreifend.
ctx.db
.select({
id: milestone.id,
title: milestone.title,
due: milestone.due,
status: milestone.status,
projectId: milestone.projectId,
projectName: project.name,
projectLogoUrl: project.logoUrl,
areaId: project.areaId,
})
.from(milestone)
.innerJoin(project, eq(project.id, milestone.projectId))
.where(
and(
ne(milestone.status, 'done'),
isNotNull(milestone.due),
eq(project.archiviert, false),
),
)
.orderBy(asc(milestone.due))
.limit(15),
]);
const projectsByArea = new Map<string, Project[]>();
for (const p of allProjects) {
+1 -2
View File
@@ -65,8 +65,7 @@ export const dealRouter = router({
enteredStageAt: new Date(),
wonAt: input.stage === 'abschluss' ? new Date() : null,
// Gewonnener Deal = voller Auftragswert (nicht mit Wahrscheinlichkeit gewichtet).
wonValueEur:
input.stage === 'abschluss' ? sql`COALESCE(${deal.valueEur}, 0)` : null,
wonValueEur: input.stage === 'abschluss' ? sql`COALESCE(${deal.valueEur}, 0)` : null,
})
.where(eq(deal.id, input.dealId))
.returning();
+39 -45
View File
@@ -15,51 +15,45 @@ export const riskRouter = router({
.orderBy(desc(risk.showstopper), desc(risk.createdAt));
}),
create: protectedProcedure
.input(RiskSchemas.createRiskInput)
.mutation(async ({ ctx, input }) => {
const [created] = await ctx.db
.insert(risk)
.values({
projectId: input.projectId,
kind: input.kind,
title: input.title,
wahrscheinlichkeit: input.wahrscheinlichkeit,
auswirkung: input.auswirkung,
herkunft: input.herkunft ?? null,
showstopper: input.showstopper,
massnahme: input.massnahme ?? null,
strategie: input.strategie ?? null,
ownerUserId: input.ownerUserId ?? null,
status: input.status,
createdBy: ctx.user.id,
})
.returning();
return created;
}),
create: protectedProcedure.input(RiskSchemas.createRiskInput).mutation(async ({ ctx, input }) => {
const [created] = await ctx.db
.insert(risk)
.values({
projectId: input.projectId,
kind: input.kind,
title: input.title,
wahrscheinlichkeit: input.wahrscheinlichkeit,
auswirkung: input.auswirkung,
herkunft: input.herkunft ?? null,
showstopper: input.showstopper,
massnahme: input.massnahme ?? null,
strategie: input.strategie ?? null,
ownerUserId: input.ownerUserId ?? null,
status: input.status,
createdBy: ctx.user.id,
})
.returning();
return created;
}),
update: protectedProcedure
.input(RiskSchemas.updateRiskInput)
.mutation(async ({ ctx, input }) => {
const { id, ...patch } = input;
if (Object.keys(patch).length === 0) {
const existing = (await ctx.db.select().from(risk).where(eq(risk.id, id)).limit(1))[0];
if (!existing) throw new TRPCError({ code: 'NOT_FOUND', message: 'Eintrag nicht gefunden.' });
return existing;
}
const [updated] = await ctx.db
.update(risk)
.set({ ...patch, updatedAt: new Date() })
.where(eq(risk.id, id))
.returning();
if (!updated) throw new TRPCError({ code: 'NOT_FOUND', message: 'Eintrag nicht gefunden.' });
return updated;
}),
update: protectedProcedure.input(RiskSchemas.updateRiskInput).mutation(async ({ ctx, input }) => {
const { id, ...patch } = input;
if (Object.keys(patch).length === 0) {
const existing = (await ctx.db.select().from(risk).where(eq(risk.id, id)).limit(1))[0];
if (!existing) throw new TRPCError({ code: 'NOT_FOUND', message: 'Eintrag nicht gefunden.' });
return existing;
}
const [updated] = await ctx.db
.update(risk)
.set({ ...patch, updatedAt: new Date() })
.where(eq(risk.id, id))
.returning();
if (!updated) throw new TRPCError({ code: 'NOT_FOUND', message: 'Eintrag nicht gefunden.' });
return updated;
}),
remove: protectedProcedure
.input(RiskSchemas.removeRiskInput)
.mutation(async ({ ctx, input }) => {
await ctx.db.delete(risk).where(eq(risk.id, input.id));
return { ok: true as const };
}),
remove: protectedProcedure.input(RiskSchemas.removeRiskInput).mutation(async ({ ctx, input }) => {
await ctx.db.delete(risk).where(eq(risk.id, input.id));
return { ok: true as const };
}),
});
+4 -2
View File
@@ -50,7 +50,8 @@ export const stakeholderRouter = router({
const existing = (
await ctx.db.select().from(stakeholder).where(eq(stakeholder.id, id)).limit(1)
)[0];
if (!existing) throw new TRPCError({ code: 'NOT_FOUND', message: 'Stakeholder nicht gefunden.' });
if (!existing)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Stakeholder nicht gefunden.' });
return existing;
}
const [updated] = await ctx.db
@@ -58,7 +59,8 @@ export const stakeholderRouter = router({
.set(patch)
.where(eq(stakeholder.id, id))
.returning();
if (!updated) throw new TRPCError({ code: 'NOT_FOUND', message: 'Stakeholder nicht gefunden.' });
if (!updated)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Stakeholder nicht gefunden.' });
return updated;
}),