opsdash-app/opsdash/lib/Service/DeckSeedService.php
2026-02-14 15:54:00 +07:00

383 lines
14 KiB
PHP

<?php
declare(strict_types=1);
namespace OCA\Opsdash\Service;
use DateTimeImmutable;
use DateTimeZone;
use OCP\App\IAppManager;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Server;
class DeckSeedException extends \RuntimeException {}
class DeckSeedService {
public function __construct(
private IUserManager $userManager,
private IUserSession $userSession,
private IAppManager $appManager,
) {
}
/**
* @param array{
* userId?: string,
* boardTitle?: string,
* boardColor?: string,
* resetStacks?: bool
* } $options
*/
public function seed(array $options = []): array {
$userId = isset($options['userId']) ? (string)$options['userId'] : 'qa';
$boardTitle = isset($options['boardTitle']) ? (string)$options['boardTitle'] : 'Opsdash Deck QA';
$boardColor = isset($options['boardColor']) ? (string)$options['boardColor'] : '#2563EB';
$resetStacks = array_key_exists('resetStacks', $options) ? (bool)$options['resetStacks'] : true;
$otherUserId = isset($options['otherUserId']) ? (string)$options['otherUserId'] : (getenv('QA_OTHER_USER') ?: 'admin');
$user = $this->userManager->get($userId);
if (!$user) {
throw new DeckSeedException("User \"{$userId}\" does not exist.");
}
// Ensure Deck sees a logged-in user for activity entries.
try {
if (method_exists($this->userSession, 'setUser')) {
$this->userSession->setUser($user);
}
} catch (\Throwable $e) {
// Non-fatal: continue if session binding fails
}
$this->ensureDeckAvailable();
/** @var \OCA\Deck\Service\PermissionService $permissionService */
$permissionService = Server::get('OCA\\Deck\\Service\\PermissionService');
$permissionService->setUserId($userId);
/** @var \OCA\Deck\Service\BoardService $boardService */
$boardService = Server::get('OCA\\Deck\\Service\\BoardService');
$boardService->setUserId($userId);
/** @var \OCA\Deck\Service\StackService $stackService */
$stackService = Server::get('OCA\\Deck\\Service\\StackService');
/** @var \OCA\Deck\Service\CardService $cardService */
$cardService = Server::get('OCA\\Deck\\Service\\CardService');
/** @var \OCA\Deck\Service\AssignmentService $assignmentService */
$assignmentService = Server::get('OCA\\Deck\\Service\\AssignmentService');
/** @var \OCA\Deck\Service\LabelService $labelService */
$labelService = Server::get('OCA\\Deck\\Service\\LabelService');
$otherUser = $this->userManager->get($otherUserId);
$board = $this->findBoardByTitle($boardService, $boardTitle);
$created = false;
if ($board === null) {
$board = $boardService->create($boardTitle, $userId, $this->normalizeHex($boardColor));
$created = true;
} elseif ($resetStacks) {
$this->resetBoardStacks($stackService, (int)$board->getId());
$board = $boardService->find((int)$board->getId(), true);
} else {
$board = $boardService->find((int)$board->getId(), true);
}
if (!$board) {
throw new DeckSeedException('Unable to create or load Deck board.');
}
$stackPlan = [
['title' => 'Inbox', 'order' => 10],
['title' => 'In Progress', 'order' => 20],
['title' => 'Done', 'order' => 30],
];
$stackMap = $this->seedStacks($stackService, (int)$board->getId(), $stackPlan);
$labelPlan = [
['title' => 'Ops', 'color' => '#F97316'],
['title' => 'Blocked', 'color' => '#F43F5E'],
['title' => 'Reporting', 'color' => '#0EA5E9'],
];
$labelMap = $this->ensureLabels($labelService, $boardService, (int)$board->getId(), $labelPlan);
$cardsSeeded = $this->seedCards(
$cardService,
$assignmentService,
$stackMap,
$labelMap,
$userId,
$otherUser ? (string)$otherUser->getUID() : null
);
return [
'boardId' => (int)$board->getId(),
'boardTitle' => (string)$board->getTitle(),
'created' => $created,
'stacks' => array_keys($stackMap),
'labels' => array_keys($labelMap),
'cardsSeeded' => $cardsSeeded,
];
}
private function ensureDeckAvailable(): void {
if (!class_exists('OCA\\Deck\\AppInfo\\Application')) {
throw new DeckSeedException('Deck app is not installed. Enable Deck before seeding.');
}
if (!$this->appManager->isInstalled('deck') || !$this->appManager->isEnabledForUser('deck')) {
throw new DeckSeedException('Deck app is disabled. Enable Deck before seeding.');
}
$this->appManager->loadApp('deck');
}
/**
* @param \OCA\Deck\Service\BoardService $boardService
* @param string $title
* @return \OCA\Deck\Db\Board|null
*/
private function findBoardByTitle($boardService, string $title) {
$boards = $boardService->findAll(0, true);
foreach ($boards as $board) {
if (strcasecmp((string)$board->getTitle(), $title) === 0) {
return $board;
}
}
return null;
}
private function normalizeHex(string $color): string {
$c = ltrim(trim($color), '#');
if ($c === '') {
return '2563EB';
}
// Deck expects 6-char hex without leading '#'
return strtoupper(substr($c, 0, 6));
}
private function resetBoardStacks($stackService, int $boardId): void {
try {
$stacks = $stackService->findAll($boardId);
foreach ($stacks as $stack) {
$stackService->delete((int)$stack->getId());
}
} catch (\Throwable $e) {
throw new DeckSeedException('Failed to reset Deck board stacks: ' . $e->getMessage(), 0, $e);
}
}
/**
* @param \OCA\Deck\Service\StackService $stackService
* @param int $boardId
* @param array<int, array{title: string, order: int}> $plan
* @return array<string, int>
*/
private function seedStacks($stackService, int $boardId, array $plan): array {
$map = [];
foreach ($plan as $entry) {
$stack = $stackService->create($entry['title'], $boardId, $entry['order']);
$map[$entry['title']] = (int)$stack->getId();
}
return $map;
}
/**
* @param \OCA\Deck\Service\LabelService $labelService
* @param \OCA\Deck\Service\BoardService $boardService
* @param int $boardId
* @param array<int, array{title: string, color: string}> $plan
* @return array<string, int>
*/
private function ensureLabels($labelService, $boardService, int $boardId, array $plan): array {
$board = $boardService->find($boardId, true);
$existing = [];
foreach ($board->getLabels() ?? [] as $label) {
$existing[(string)$label->getTitle()] = (int)$label->getId();
}
$map = [];
foreach ($plan as $entry) {
$title = $entry['title'];
if (isset($existing[$title])) {
$map[$title] = $existing[$title];
continue;
}
$label = $labelService->create($title, ltrim($entry['color'], '#'), $boardId);
$map[$title] = (int)$label->getId();
}
return $map;
}
/**
* @param array<string, int> $stackMap
* @param array<string, int> $labelMap
*/
private function seedCards($cardService, $assignmentService, array $stackMap, array $labelMap, string $userId, ?string $otherUserId): int {
$tz = new DateTimeZone('UTC');
$weekStart = (new DateTimeImmutable('monday this week', $tz))->setTime(9, 0);
$monthAnchor = (new DateTimeImmutable('first day of this month', $tz))->setTime(10, 0);
$entries = [
[
'title' => 'Review overnight alerts',
'stack' => 'Inbox',
'description' => 'Check incidents and flag anything that blocks delivery.',
'due' => $weekStart->modify('+1 day'),
'labels' => ['Ops'],
'assign' => [$userId],
'done' => false,
'archived' => false,
],
[
'title' => 'Escalate blocked migration tasks',
'stack' => 'Inbox',
'description' => 'Unblock owner approvals for cards stuck in review.',
'due' => $weekStart->modify('+3 days'),
'labels' => ['Blocked'],
'assign' => [$otherUserId ?: $userId],
'done' => false,
'archived' => false,
],
[
'title' => 'QA follow-up checks',
'stack' => 'Inbox',
'description' => 'Verify retests for issues marked ready-for-verify.',
'due' => $weekStart->modify('+2 days'),
'labels' => ['Ops'],
'assign' => [$userId],
'done' => false,
'archived' => false,
],
[
'title' => 'Publish weekly KPI digest',
'stack' => 'Done',
'description' => 'Post the weekly metrics digest for product and support.',
'due' => $weekStart->modify('+3 days'),
'labels' => ['Reporting'],
'assign' => [$userId],
'done' => true,
'archived' => false,
],
[
'title' => 'Close resolved support escalations',
'stack' => 'Done',
'description' => 'Confirm fix rollout and close escalations from this sprint.',
'due' => $weekStart->modify('+1 day'),
'labels' => ['Ops'],
'assign' => [$otherUserId ?: $userId],
'done' => true,
'archived' => false,
],
[
'title' => 'Document automation runbook updates',
'stack' => 'Done',
'description' => 'Capture lessons from the latest workflow automation updates.',
'due' => $weekStart->modify('+1 day'),
'labels' => ['Ops'],
'assign' => [$userId],
'done' => true,
'archived' => false,
],
[
'title' => 'Tag cards for monthly reporting',
'stack' => 'Done',
'description' => 'Apply reporting labels so charts stay clean and consistent.',
'due' => $weekStart->modify('+2 days'),
'labels' => ['Reporting'],
'assign' => [$otherUserId ?: $userId],
'done' => true,
'archived' => false,
],
[
'title' => 'Investigate sync latency spike',
'stack' => 'In Progress',
'description' => 'Analyze slow sync jobs and prepare mitigations.',
'due' => $weekStart->modify('+4 days'),
'labels' => ['Blocked'],
'assign' => [$otherUserId ?: $userId],
'done' => false,
'archived' => false,
],
[
'title' => 'Plan next month reliability themes',
'stack' => 'Inbox',
'description' => 'Draft monthly priorities for reliability and support quality.',
'due' => $monthAnchor->modify('+20 days'),
'labels' => ['Ops'],
'assign' => [$userId],
'done' => false,
'archived' => false,
],
];
$count = 0;
$order = 1;
foreach ($entries as $entry) {
$stackTitle = $entry['stack'];
if (!isset($stackMap[$stackTitle])) {
continue;
}
$card = $cardService->create(
$entry['title'],
$stackMap[$stackTitle],
'plain',
$order++,
$userId,
$entry['description'],
($entry['due'] instanceof DateTimeImmutable) ? $entry['due']->format('c') : null
);
foreach ($entry['labels'] as $labelTitle) {
if (!isset($labelMap[$labelTitle])) {
continue;
}
try {
$cardService->assignLabel((int)$card->getId(), $labelMap[$labelTitle]);
} catch (\Throwable $e) {
// continue even if label assignment fails
}
}
foreach ($entry['assign'] as $assignee) {
if (!$assignee) {
continue;
}
try {
$assignmentService->assignUser((int)$card->getId(), $assignee);
} catch (\Throwable $e) {
// continue
}
}
if (!empty($entry['done'])) {
try {
$cardService->done((int)$card->getId());
} catch (\Throwable $e) {
// ignore
}
}
if (!empty($entry['archived'])) {
try {
$cardService->archive((int)$card->getId());
} catch (\Throwable $e) {
// ignore
}
}
$count++;
}
$doneStackId = $stackMap['Done'] ?? null;
if ($doneStackId) {
$card = $cardService->create(
'Archive shipped initiatives',
$doneStackId,
'plain',
$order + 5,
$userId,
'Keep the board tidy by archiving work shipped this week.',
$weekStart->modify('-1 day')->format('c')
);
$cardService->done((int)$card->getId());
$cardService->archive((int)$card->getId());
$count++;
}
return $count;
}
}