Files
schalli de06794e67
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Successful in 38s
Tessera CI/CD / Tests (push) Waiting to run
docs(07): create phase 7 execution plans for DKV fleet module
6 plans covering full pipeline: PDF parsing foundation (Wave 0),
inbox providers + export/SMTP services (Wave 1), pipeline
orchestration + frontend pages + settings UI (Wave 2). Includes
D-06 MailModule DB-config migration and Nyquist validation strategy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 18:56:59 +02:00

26 KiB
Raw Permalink Blame History

Phase 7: DKV Fleet Module - Pattern Map

Mapped: 2026-06-26 Files analyzed: 21 new/modified files Analogs found: 19 / 21


File Classification

New/Modified File Role Data Flow Closest Analog Match Quality
apps/api/src/dkv/dkv.module.ts module — apps/api/src/calendar/calendar.module.ts exact
apps/api/src/dkv/dkv.controller.ts controller request-response apps/api/src/ldap/ldap.controller.ts exact
apps/api/src/dkv/dkv.service.ts service orchestration apps/api/src/calendar/calendar.service.ts role-match
apps/api/src/dkv/dkv-scheduler.service.ts service event-driven apps/api/src/ldap/ldap-sync.scheduler.ts exact
apps/api/src/dkv/dkv-parser.service.ts service transform apps/api/src/calendar/calendar.service.ts partial
apps/api/src/dkv/dkv-export.service.ts service file-I/O — no analog
apps/api/src/dkv/dkv-mail.service.ts service request-response apps/api/src/mail/mail.service.ts role-match
apps/api/src/dkv/providers/inbox-provider.interface.ts interface — apps/api/src/calendar/calendar.service.ts (CalendarProvider) exact
apps/api/src/dkv/providers/imap.provider.ts provider file-I/O apps/api/src/calendar/providers/caldav.provider.ts role-match
apps/api/src/dkv/providers/exchange-inbox.provider.ts provider request-response apps/api/src/calendar/providers/exchange.provider.ts exact
apps/api/src/dkv/dto/dkv-config.dto.ts dto — apps/api/src/calendar/dto/create-calendar-source.dto.ts exact
apps/api/src/dkv/dto/dkv-vehicle.dto.ts dto — apps/api/src/calendar/dto/create-calendar-source.dto.ts exact
apps/api/src/dkv/dto/dkv-history.dto.ts dto — apps/api/src/calendar/dto/create-calendar-source.dto.ts role-match
apps/api/src/settings/settings.module.ts module — apps/api/src/calendar/calendar.module.ts role-match
apps/api/src/settings/settings.controller.ts controller request-response apps/api/src/ldap/ldap.controller.ts exact
apps/api/src/settings/settings.service.ts service CRUD apps/api/src/calendar/calendar.service.ts role-match
apps/api/src/settings/dto/smtp-config.dto.ts dto — apps/api/src/calendar/dto/create-calendar-source.dto.ts role-match
apps/api/src/app.module.ts (modify) module — self —
apps/web/src/app/(portal)/modules/dkv-fleet/page.tsx component request-response apps/web/src/app/(portal)/modules/domaincheck/page.tsx exact
apps/web/src/app/(portal)/modules/dkv-fleet/settings/page.tsx component CRUD apps/web/src/app/(portal)/settings/dashboard/calendar/page.tsx role-match
apps/web/src/app/(portal)/settings/general/smtp/page.tsx component CRUD apps/web/src/app/(portal)/settings/dashboard/calendar/page.tsx role-match

Pattern Assignments

apps/api/src/dkv/dkv.module.ts (module)

Analog: apps/api/src/calendar/calendar.module.ts

Module structure pattern (lines 1–33):

import { Module } from '@nestjs/common';
import { DkvController } from './dkv.controller';
import { DkvService } from './dkv.service';
import { DkvSchedulerService } from './dkv-scheduler.service';
import { DkvParserService } from './dkv-parser.service';
import { DkvExportService } from './dkv-export.service';
import { DkvMailService } from './dkv-mail.service';
import { ImapProvider } from './providers/imap.provider';
import { ExchangeInboxProvider } from './providers/exchange-inbox.provider';
import { CalendarCryptoService } from '../calendar/crypto.service';

@Module({
  controllers: [DkvController],
  providers: [
    DkvService,
    DkvSchedulerService,
    DkvParserService,
    DkvExportService,
    DkvMailService,
    ImapProvider,
    ExchangeInboxProvider,
    CalendarCryptoService,   // imported from CalendarModule — inject directly, not re-declared
  ],
  exports: [DkvService],
})
export class DkvModule {}

Note: CalendarCryptoService must also be exported from CalendarModule (add exports: [CalendarCryptoService] there) so DkvModule can import it.


apps/api/src/dkv/dkv.controller.ts (controller, request-response)

Analog: apps/api/src/ldap/ldap.controller.ts

Imports pattern (lines 1–19):

import {
  BadRequestException,
  Body,
  Controller,
  Delete,
  Get,
  NotFoundException,
  Param,
  Patch,
  Post,
  Req,
  UploadedFile,
  UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { Role } from '@prisma/client';
import { Roles } from '../auth/decorators/roles.decorator';
import { DkvService } from './dkv.service';
import { DkvConfigDto } from './dto/dkv-config.dto';
import { CreateVehicleDto, UpdateVehicleDto } from './dto/dkv-vehicle.dto';

Auth/Guard pattern — ADMIN-only, same as ldap.controller.ts lines 38–40:

@Roles(Role.ADMIN, Role.SUPER_ADMIN)

Apply to every handler. Global JwtAuthGuard already enforces JWT; @Roles adds role check.

Tenant extraction pattern — copy from ldap.controller.ts lines 40–47:

const tenantId = req.tenantId;
if (!tenantId) {
  throw new BadRequestException('No tenant context');
}

Core routes pattern:

@Controller('dkv')
export class DkvController {
  constructor(private readonly dkvService: DkvService) {}

  // GET  /dkv/config          — get module config (no decrypted passwords)
  // PUT  /dkv/config          — save module config
  // POST /dkv/check-now       — manual inbox poll trigger
  // GET  /dkv/history         — processing history (paginated)
  // GET  /dkv/exports/:filename — file download
  // GET  /dkv/vehicles        — list vehicle master
  // POST /dkv/vehicles        — create vehicle
  // PUT  /dkv/vehicles/:id    — update vehicle
  // DELETE /dkv/vehicles/:id  — delete vehicle
  // POST /dkv/vehicles/import — CSV bulk import
}

Error handling pattern — follow ldap.controller.ts:

try {
  const result = await this.dkvService.someMethod(tenantId, dto);
  return result;
} catch (error) {
  if (error instanceof NotFoundException) throw error;
  if (error instanceof BadRequestException) throw error;
  throw error; // let global exception filter handle it
}

apps/api/src/dkv/dkv.service.ts (service, orchestration)

Analog: apps/api/src/calendar/calendar.service.ts

Imports pattern (lines 1–14 of calendar.service.ts):

import {
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CalendarCryptoService } from '../calendar/crypto.service';
// + DkvParserService, DkvExportService, DkvMailService, providers

Safe DB select pattern — never return encrypted credentials (calendar.service.ts lines 50–67):

const CONFIG_SAFE_SELECT = {
  id: true,
  tenantId: true,
  protocol: true,
  host: true,
  port: true,
  encryption: true,
  folder: true,
  senderFilter: true,
  pollIntervalMin: true,
  isActive: true,
  exportRecipient: true,
  vehicleFormatString: true,
  // encryptedInboxCreds: NEVER included
  createdAt: true,
  updatedAt: true,
} as const;

Credential encryption pattern (from RESEARCH.md Code Examples):

// Encrypt at save time
const encryptedInboxCreds = this.crypto.encrypt(
  JSON.stringify({ username: dto.username, password: dto.password })
);

// Decrypt at use time (never log result — T-05-13)
const { username, password } = JSON.parse(
  this.crypto.decrypt(config.encryptedInboxCreds!)
);

Error handling — generic messages (exchange.provider.ts lines 42–48):

} catch (error) {
  this.logger.error(
    `DKV inbox poll failed for tenant ${tenantId}: ${(error as Error).message}`,
  );
  // T-05-13: no credential details in error
  return [];
}

apps/api/src/dkv/dkv-scheduler.service.ts (service, event-driven)

Analog: apps/api/src/ldap/ldap-sync.scheduler.ts

Full structure pattern (ldap-sync.scheduler.ts lines 1–89):

import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
// DkvSchedulerService DIFFERS: uses SchedulerRegistry + dynamic CronJob
// instead of static @Cron — because interval is configurable from DB

import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { SchedulerRegistry } from '@nestjs/schedule';
import { CronJob } from 'cron';

const JOB_NAME = 'dkv-inbox-poll';

@Injectable()
export class DkvSchedulerService implements OnModuleInit {
  private readonly logger = new Logger(DkvSchedulerService.name);

  constructor(
    private readonly schedulerRegistry: SchedulerRegistry,
    private readonly dkvService: DkvService,
  ) {}

  onModuleInit() {
    this.dkvService.loadConfig().then(config => {
      if (config?.isActive) this.setInterval(config.pollIntervalMin);
    }).catch(err => this.logger.error('Failed to init DKV scheduler', err));
  }

  setInterval(intervalMin: number): void {
    try {
      this.schedulerRegistry.getCronJob(JOB_NAME).stop();
      this.schedulerRegistry.deleteCronJob(JOB_NAME);
    } catch { /* not yet registered */ }

    const job = new CronJob(`*/${intervalMin} * * * *`, () => {
      this.dkvService.processInbox().catch(err =>
        this.logger.error('DKV inbox poll failed', err)
      );
    });
    this.schedulerRegistry.addCronJob(JOB_NAME, job);
    job.start();
  }
}

Key difference from LdapSyncScheduler: Static @Cron() decorator cannot change at runtime. Use SchedulerRegistry.addCronJob() pattern instead.


apps/api/src/dkv/dkv-parser.service.ts (service, transform)

No direct analog — unique PDF parsing logic. Use NestJS @Injectable() service wrapper.

Service shell pattern (follows all other services):

import { Injectable, Logger } from '@nestjs/common';

@Injectable()
export class DkvParserService {
  private readonly logger = new Logger(DkvParserService.name);

  async parsePdf(buffer: Buffer): Promise<DkvVehicleBlock[]> {
    // Wave 0: validate pdf-parse v2 API against user-files/invoice.pdf first
    // then implement production regex
  }
}

Critical: pdf-parse v2 API — old pdfParse(buffer) function call does NOT exist in v2:

import { PDFParse } from 'pdf-parse';

async function extractPdfText(buffer: Buffer): Promise<string> {
  const parser = new PDFParse({ data: buffer });
  const result = await parser.getText();
  await parser.destroy();
  return result.text;
}

DKV vehicle block regex (ASSUMED — validate against user-files/invoice.pdf in Wave 0):

const vehicleBlockPattern =
  /VEHICLE:\s+(\S+)\s+CARD NO\.:\s+(\S+)([\s\S]*?)(?=VEHICLE:|$)/g;

German number parsing (mandatory for Kilometerstand/Menge):

parseFloat(raw.replace(/\./g, '').replace(',', '.'))

apps/api/src/dkv/dkv-export.service.ts (service, file-I/O)

No analog — first file-I/O service in codebase. Use NestJS @Injectable() pattern.

SheetJS buffer pattern (RESEARCH.md Pattern 5):

import * as XLSX from 'xlsx';

function buildExcelBuffer(rows: ExportRow[]): Buffer {
  const headers = ['Lieferdatum', 'Fahrzeug', 'Fahrer', 'Ort', 'Kilometerstand'];
  const data = rows.map(r => [r.lieferdatum, r.fahrzeug, r.fahrer, r.ort, r.kilometerstand]);
  const ws = XLSX.utils.aoa_to_sheet([headers, ...data]);
  const wb = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(wb, ws, 'DKV Export');
  return XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' }) as Buffer;
}

Fahrzeug format string resolver (RESEARCH.md Code Examples):

function resolveFahrzeug(vehicle: DkvVehicleMaster, formatString: string): string {
  return formatString
    .replace('{Marke}', vehicle.marke)
    .replace('{Modell}', vehicle.modell)
    .replace('{Kennzeichen}', vehicle.kennzeichen)
    .replace('{Fahrer}', vehicle.fahrer);
}

10-file prune guard — add processing lock to prevent race:

private processing = false;

async processInbox(): Promise<void> {
  if (this.processing) return; // Pitfall 7: race condition guard
  this.processing = true;
  try { /* ... */ } finally { this.processing = false; }
}

apps/api/src/dkv/dkv-mail.service.ts (service, request-response)

Analog: apps/api/src/mail/mail.service.ts

Key difference: Do NOT use MailerService from @nestjs-modules/mailer. Use nodemailer.createTransport() directly — transport created at send time from DB config (Pitfall 3).

Imports pattern (mail.service.ts lines 1–4, adapted):

import { Injectable, Logger } from '@nestjs/common';
import * as nodemailer from 'nodemailer';
import { PrismaService } from '../prisma/prisma.service';
import { CalendarCryptoService } from '../calendar/crypto.service';

Error handling pattern (mail.service.ts lines 69–78):

try {
  await transport.sendMail({ ... });
  this.logger.log(`DKV export sent to ${recipient}`);
} catch (error) {
  // Log but surface to caller for retry logic (unlike MailService which swallows)
  this.logger.error(
    `Failed to send DKV export to ${recipient}`,
    error instanceof Error ? error.stack : String(error),
  );
  throw error; // DkvService handles retries with exponential backoff
}

Dynamic SMTP transport pattern (RESEARCH.md Pattern 6):

const transport = nodemailer.createTransport({
  host: smtpConfig.host,
  port: smtpConfig.port,
  secure: smtpConfig.encryption === 'ssl-tls',
  requireTLS: smtpConfig.encryption === 'starttls',
  auth: smtpConfig.username
    ? { user: smtpConfig.username, pass: decryptedPassword }
    : undefined,
});

apps/api/src/dkv/providers/inbox-provider.interface.ts (interface)

Analog: apps/api/src/calendar/calendar.service.ts — CalendarProvider interface (lines 33–44)

// Mirror of CalendarProvider pattern — same structure, different method names
export interface InboxEmail {
  uid: number | string;
  messageId: string;
  subject: string;
  from: string;
  date: Date;
  attachments: InboxAttachment[];
}

export interface InboxAttachment {
  filename: string;
  contentType: string;
  buffer: Buffer;
}

export interface InboxProvider {
  fetchPdfAttachments(config: InboxConfig): Promise<InboxEmail[]>;
  testConnection(config: InboxConfig): Promise<boolean>;
}

apps/api/src/dkv/providers/exchange-inbox.provider.ts (provider, request-response)

Analog: apps/api/src/calendar/providers/exchange.provider.ts (full file, 228 lines)

Class shell + Logger pattern (exchange.provider.ts lines 1–48):

import { Injectable, Logger } from '@nestjs/common';
import { InboxEmail, InboxProvider } from './inbox-provider.interface';

@Injectable()
export class ExchangeInboxProvider implements InboxProvider {
  private readonly logger = new Logger(ExchangeInboxProvider.name);

  async fetchPdfAttachments(config: InboxConfig): Promise<InboxEmail[]> {
    try {
      return await this.fetchViaEws(config);
    } catch (error) {
      this.logger.error(
        `EWS inbox fetch failed: ${(error as Error).message}`,
        // T-05-13: never include credentials in error
      );
      return [];
    }
  }
}

EWS dynamic import pattern (exchange.provider.ts lines 154–155):

const ews: any = await import('ews-javascript-api');
const service = new ews.ExchangeService(ews.ExchangeVersion.Exchange2013);
service.Url = new ews.Uri(config.host);
service.Credentials = new ews.WebCredentials(config.username, config.password);

Critical difference from ExchangeProvider: Use ews.WellKnownFolderName.Inbox with service.FindItems() and ews.EmailMessage — NOT FindAppointments / CalendarView (Pitfall 6).


apps/api/src/dkv/providers/imap.provider.ts (provider, file-I/O)

Analog: apps/api/src/calendar/providers/caldav.provider.ts (role-match — async provider shell)

Class shell:

import { Injectable, Logger } from '@nestjs/common';
import { ImapFlow } from 'imapflow';
import { InboxEmail, InboxProvider } from './inbox-provider.interface';

@Injectable()
export class ImapProvider implements InboxProvider {
  private readonly logger = new Logger(ImapProvider.name);
  // ...
}

Critical imapflow pattern (RESEARCH.md Pattern 2 + Pitfall 1):

// NEVER call download() inside fetch() async iterator — causes IMAP deadlock
// Always: fetchAll() first → loop → download()
const messages = await client.fetchAll(
  uids.join(','),
  { envelope: true, bodyStructure: true },
  { uid: true },
);
for (const msg of messages) {
  const { content } = await client.download(String(msg.uid), partId, { uid: true });
}

Logger suppression (security — never log IMAP credentials):

const client = new ImapFlow({
  // ...
  logger: false, // suppress verbose imapflow logs (contain credentials)
});

apps/api/src/dkv/dto/dkv-config.dto.ts (dto)

Analog: apps/api/src/calendar/dto/create-calendar-source.dto.ts

Validation decorator pattern (lines 1–47):

import {
  IsBoolean,
  IsEmail,
  IsIn,
  IsInt,
  IsNotEmpty,
  IsOptional,
  IsString,
  Max,
  Min,
} from 'class-validator';

export class DkvConfigDto {
  @IsIn(['imap', 'exchange'])
  protocol!: string;

  @IsOptional()
  @IsString()
  host?: string;

  @IsOptional()
  @IsInt()
  @Min(1)
  @Max(65535)
  port?: number;

  @IsIn(['none', 'starttls', 'ssl-tls'])
  encryption!: string;

  @IsOptional()
  @IsString()
  folder?: string;

  @IsOptional()
  @IsEmail()
  senderFilter?: string;  // IsEmail() validator — Pitfall: credential injection

  @IsOptional()
  @IsEmail()
  exportRecipient?: string;

  @IsOptional()
  @IsInt()
  @Min(1)
  pollIntervalMin?: number;

  @IsOptional()
  @IsBoolean()
  isActive?: boolean;
}

apps/api/src/settings/settings.controller.ts (controller, request-response)

Analog: apps/api/src/ldap/ldap.controller.ts

Pattern: Same admin-only guard, same tenant extraction. Routes: GET /settings/smtp and PUT /settings/smtp.

@Controller('settings')
export class SettingsController {
  constructor(private readonly settingsService: SettingsService) {}

  @Get('smtp')
  @Roles(Role.ADMIN, Role.SUPER_ADMIN)
  async getSmtpConfig(@Req() req: any) {
    const tenantId = req.tenantId;
    if (!tenantId) throw new BadRequestException('No tenant context');
    const config = await this.settingsService.getSmtpConfig(tenantId);
    // Never return decryptedPassword — return hasPassword boolean (T-05-09 pattern)
    return config ? { ...config, encryptedPassword: undefined, hasPassword: !!config.encryptedPassword } : null;
  }

  @Put('smtp')
  @Roles(Role.ADMIN, Role.SUPER_ADMIN)
  async saveSmtpConfig(@Req() req: any, @Body() dto: SmtpConfigDto) {
    const tenantId = req.tenantId;
    if (!tenantId) throw new BadRequestException('No tenant context');
    return this.settingsService.saveSmtpConfig(tenantId, dto);
  }
}

apps/web/src/app/(portal)/modules/dkv-fleet/page.tsx (component, request-response)

Analog: apps/web/src/app/(portal)/modules/domaincheck/page.tsx

Full component pattern (domaincheck/page.tsx lines 1–64):

'use client';

import { useTranslations } from 'next-intl';
import { useState } from 'react';

export default function DkvFleetPage() {
  const t = useTranslations('dkvFleet');
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  return (
    <div className="mx-auto max-w-5xl space-y-6 p-6">
      <div>
        <h1 className="text-2xl font-bold tracking-tight">{t('title')}</h1>
        <p className="text-sm text-muted-foreground mt-1">{t('description')}</p>
      </div>

      <div className="rounded-lg border border-border bg-card p-6 shadow-sm space-y-4">
        {/* InvoiceHistoryTable + manual trigger button */}
      </div>
    </div>
  );
}

Layout: mx-auto max-w-5xl space-y-6 p-6 — wider than domaincheck (27 vehicles × 5 columns needs more space).


apps/web/src/app/(portal)/modules/dkv-fleet/settings/page.tsx (component, CRUD)

Analog: apps/web/src/app/(portal)/settings/dashboard/calendar/page.tsx

Pattern (calendar/page.tsx lines 1–21):

'use client';

import { useTranslations } from 'next-intl';
import { InboxConfigForm } from './components/InboxConfigForm';
import { VehicleTable } from './components/VehicleTable';
import { CsvImportButton } from './components/CsvImportButton';

export default function DkvFleetSettingsPage() {
  const t = useTranslations('dkvFleet');

  return (
    <div>
      <h1 className="mb-6 text-lg font-semibold text-foreground">
        {t('settingsTitle')}
      </h1>
      <InboxConfigForm />
      <VehicleTable />
    </div>
  );
}

apps/web/src/app/(portal)/settings/general/smtp/page.tsx (component, CRUD)

Analog: apps/web/src/app/(portal)/settings/dashboard/calendar/page.tsx

This page lives in the settings section — uses SettingsLayout (already wraps the route).

'use client';

import { useTranslations } from 'next-intl';

export default function SmtpSettingsPage() {
  const t = useTranslations('settings');

  return (
    <div>
      <h1 className="mb-6 text-lg font-semibold text-foreground">
        {t('categorySmtp')}
      </h1>
      {/* SmtpConfigForm component */}
    </div>
  );
}

Settings sidebar extension — add to apps/web/src/components/settings/settings-sidebar.tsx:

// Extend items array — add new "Allgemein" category above "Dashboard"
const generalItems = [
  { label: t('categorySmtp'), href: '/settings/general/smtp' },
];

Shared Patterns

Authentication / Authorization

Source: apps/api/src/ldap/ldap.controller.ts lines 38–40
Apply to: All DKV and Settings controller handlers

@Roles(Role.ADMIN, Role.SUPER_ADMIN)

Global JwtAuthGuard is already applied in main.ts. @Roles decorator adds role check on top.


Credential Encryption (AES-256-GCM)

Source: apps/api/src/calendar/crypto.service.ts (full file, 75 lines)
Apply to: dkv.service.ts (inbox creds), settings.service.ts (SMTP password)

// Inject CalendarCryptoService — it handles the key lifecycle
constructor(private readonly crypto: CalendarCryptoService) {}

// Encrypt at save time
const encrypted = this.crypto.encrypt(JSON.stringify({ username, password }));

// Decrypt at use time — NEVER log result (T-05-13)
const creds = JSON.parse(this.crypto.decrypt(encrypted));

Tenant Context Extraction

Source: apps/api/src/ldap/ldap.controller.ts lines 40–47
Apply to: All DKV and Settings controller handlers

const tenantId = req.tenantId;
if (!tenantId) {
  throw new BadRequestException('No tenant context');
}

Generic Error Logging (no credential exposure)

Source: apps/api/src/calendar/providers/exchange.provider.ts lines 42–48
Apply to: dkv-mail.service.ts, imap.provider.ts, exchange-inbox.provider.ts, dkv-scheduler.service.ts

this.logger.error(
  `Operation failed for tenant ${tenantId}: ${(error as Error).message}`,
  // T-05-13: message only — never include stack with potential credential details
);

NestJS Logger

Source: apps/api/src/calendar/providers/exchange.provider.ts line 15
Apply to: All new @Injectable() services and providers

private readonly logger = new Logger(ClassName.name);

Safe API Response (no encrypted fields)

Source: apps/api/src/calendar/calendar.service.ts lines 50–67
Apply to: dkv.service.ts (config), settings.service.ts (SMTP config)

Define a const X_SAFE_SELECT Prisma select object that explicitly excludes encryptedInboxCreds / encryptedPassword. Never return these fields to the frontend.


Frontend 'use client' + useTranslations shell

Source: apps/web/src/app/(portal)/modules/domaincheck/page.tsx lines 1–7
Apply to: All new Next.js page and component files

'use client';

import { useTranslations } from 'next-intl';
import { useState } from 'react';

Frontend Card Layout

Source: apps/web/src/app/(portal)/modules/domaincheck/page.tsx lines 40–64
Apply to: dkv-fleet/page.tsx, settings form components

<div className="rounded-lg border border-border bg-card p-6 shadow-sm space-y-4">
  {/* content */}
</div>

No Analog Found

File Role Data Flow Reason
apps/api/src/dkv/dkv-export.service.ts service file-I/O No file-writing service exists in codebase (first file-I/O service)
apps/api/src/dkv/dkv-parser.service.ts service transform No binary-parsing / regex transform service exists (novel pattern)

For these files, use RESEARCH.md Patterns 3–5 (pdf-parse v2, DKV regex, SheetJS) as primary reference. Wrap in standard @Injectable() NestJS service shell from the shared pattern above.


AppModule Modification

File: apps/api/src/app.module.ts
Required change: Add ScheduleModule.forRoot() (NOT imported yet — confirmed by codebase grep) and DkvModule:

import { ScheduleModule } from '@nestjs/schedule';
import { DkvModule } from './dkv/dkv.module';
import { SettingsModule } from './settings/settings.module';

@Module({
  imports: [
    // ... existing imports ...
    ScheduleModule.forRoot(),  // prerequisite for DkvSchedulerService (Pitfall 4)
    DkvModule,
    SettingsModule,
  ],
})

Metadata

Analog search scope: apps/api/src/, apps/web/src/app/(portal)/ Files scanned: 21 existing source files Pattern extraction date: 2026-06-26