OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
594
app/Domain/Reports/Services/Reports.php
Normal file
594
app/Domain/Reports/Services/Reports.php
Normal file
@@ -0,0 +1,594 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Reports\Services;
|
||||
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use Exception;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Promise\PromiseInterface;
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Leantime\Core\Auth\Permissions\RequiresPermission;
|
||||
use Leantime\Core\Configuration\AppSettings as AppSettingCore;
|
||||
use Leantime\Core\Configuration\Environment as EnvironmentCore;
|
||||
use Leantime\Core\Domains\BaseService;
|
||||
use Leantime\Domain\Blueprints\Repositories\Blueprints as BlueprintsRepository;
|
||||
use Leantime\Domain\Clients\Repositories\Clients as ClientRepository;
|
||||
use Leantime\Domain\Comments\Repositories\Comments as CommentRepository;
|
||||
use Leantime\Domain\Ideas\Repositories\Ideas as IdeaRepository;
|
||||
use Leantime\Domain\Projects\Repositories\Projects as ProjectRepository;
|
||||
use Leantime\Domain\Reactions\Repositories\Reactions;
|
||||
use Leantime\Domain\Reports\Permissions\ReportsPermissions;
|
||||
use Leantime\Domain\Reports\Repositories\Reports as ReportRepository;
|
||||
use Leantime\Domain\Setting\Services\Setting as SettingsService;
|
||||
use Leantime\Domain\Sprints\Repositories\Sprints as SprintRepository;
|
||||
use Leantime\Domain\Sprints\Services\Sprints as SprintService;
|
||||
use Leantime\Domain\Tickets\Repositories\Tickets as TicketRepository;
|
||||
use Leantime\Domain\Timesheets\Repositories\Timesheets as TimesheetRepository;
|
||||
use Leantime\Domain\Users\Repositories\Users as UserRepository;
|
||||
|
||||
/**
|
||||
* Reports service: per-project report aggregation (burndown, ticket-status history) plus the
|
||||
* system-level daily-ingestion and telemetry tasks.
|
||||
*
|
||||
* Authorization model: the three by-projectId @api reads carry a dispatch
|
||||
* #[RequiresPermission(reports.view, projectIdParam: 'projectId')] gate, authorized against the
|
||||
* REQUESTED project (closes the cross-project RPC IDOR). The system/cron methods (ingestion,
|
||||
* telemetry) are NOT @api — they run from the scheduler with no session user and must never be
|
||||
* RPC-reachable.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class Reports extends BaseService
|
||||
{
|
||||
private AppSettingCore $appSettings;
|
||||
|
||||
private EnvironmentCore $config;
|
||||
|
||||
private ProjectRepository $projectRepository;
|
||||
|
||||
private SprintRepository $sprintRepository;
|
||||
|
||||
private ReportRepository $reportRepository;
|
||||
|
||||
private SettingsService $settings;
|
||||
|
||||
private TicketRepository $ticketRepository;
|
||||
|
||||
private SprintService $sprintService;
|
||||
|
||||
public function __construct(
|
||||
AppSettingCore $appSettings,
|
||||
EnvironmentCore $config,
|
||||
ProjectRepository $projectRepository,
|
||||
SprintRepository $sprintRepository,
|
||||
ReportRepository $reportRepository,
|
||||
SettingsService $settings,
|
||||
TicketRepository $ticketRepository,
|
||||
SprintService $sprintService
|
||||
) {
|
||||
$this->appSettings = $appSettings;
|
||||
$this->config = $config;
|
||||
$this->projectRepository = $projectRepository;
|
||||
$this->sprintRepository = $sprintRepository;
|
||||
$this->reportRepository = $reportRepository;
|
||||
$this->settings = $settings;
|
||||
$this->ticketRepository = $ticketRepository;
|
||||
$this->sprintService = $sprintService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves which sprint burndown to display on the reports page.
|
||||
*
|
||||
* Mirrors the legacy controller selection order exactly:
|
||||
* 1. An explicitly requested sprint id (from the query string).
|
||||
* 2. Otherwise the project's current sprint.
|
||||
* 3. Otherwise the first available sprint.
|
||||
*
|
||||
* The returned 'currentSprintId' preserves the original behaviour:
|
||||
* when a sprint id is explicitly requested it is echoed back as-is
|
||||
* (even if the sprint cannot be loaded); when falling back to the
|
||||
* current/first sprint the resolved sprint object's id is used.
|
||||
* When the project has no sprints at all, both values are false.
|
||||
*
|
||||
* @param int $projectId Project to resolve the burndown for.
|
||||
* @param int|null $requestedSprintId Sprint id explicitly requested by the user, or null.
|
||||
* @return array{chart: false|array, currentSprintId: int|false} Burndown chart data and the resolved sprint id.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(ReportsPermissions::VIEW, projectIdParam: 'projectId')]
|
||||
public function getSprintBurndownForReport(int $projectId, ?int $requestedSprintId): array
|
||||
{
|
||||
$allSprints = $this->sprintService->getAllSprints($projectId);
|
||||
|
||||
if (count($allSprints) === 0) {
|
||||
return ['chart' => false, 'currentSprintId' => false];
|
||||
}
|
||||
|
||||
$sprintChart = false;
|
||||
|
||||
if ($requestedSprintId !== null) {
|
||||
$sprintObject = $this->sprintService->getSprint($requestedSprintId);
|
||||
if ($sprintObject) {
|
||||
$sprintChart = $this->sprintService->getSprintBurndown($sprintObject);
|
||||
}
|
||||
|
||||
return ['chart' => $sprintChart, 'currentSprintId' => $requestedSprintId];
|
||||
}
|
||||
|
||||
$currentSprint = $this->sprintService->getCurrentSprintId($projectId);
|
||||
|
||||
if ($currentSprint !== false && $currentSprint !== 'all') {
|
||||
$sprintObject = $this->sprintService->getSprint((int) $currentSprint);
|
||||
if ($sprintObject) {
|
||||
$sprintChart = $this->sprintService->getSprintBurndown($sprintObject);
|
||||
|
||||
return ['chart' => $sprintChart, 'currentSprintId' => $sprintObject->id];
|
||||
}
|
||||
|
||||
return ['chart' => $sprintChart, 'currentSprintId' => false];
|
||||
}
|
||||
|
||||
$sprintChart = $this->sprintService->getSprintBurndown($allSprints[0]);
|
||||
|
||||
return ['chart' => $sprintChart, 'currentSprintId' => $allSprints[0]->id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the daily report ingestion for the session's current project.
|
||||
*
|
||||
* Not @api: an internal web-path helper (called by the gated Reports\Controllers\Show after
|
||||
* dispatch enforcement). It reads session('currentProject'), so an RPC caller would have no
|
||||
* meaningful project binding — and it was needlessly RPC-exposed before.
|
||||
*
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function dailyIngestion(): void
|
||||
{
|
||||
$this->runIngestionForProject(session('currentProject'));
|
||||
}
|
||||
|
||||
protected function runIngestionForProject(int $projectId): void
|
||||
{
|
||||
|
||||
if (Cache::has('dailyReports-'.$projectId) === false || Cache::get('dailyReports-'.$projectId) < dtHelper()->dbNow()->endOfDay()) {
|
||||
|
||||
// Check if the dailyingestion cycle was executed already. There should be one entry for backlog and one entry for current sprint (unless there is no current sprint
|
||||
// Get current Sprint Id, if no sprint available, dont run the sprint burndown
|
||||
|
||||
$lastEntries = $this->reportRepository->checkLastReportEntries($projectId);
|
||||
|
||||
// If we receive 2 entries we have a report already. If we have one entry then we ran the backlog one and that means there was no current sprint.
|
||||
if (count($lastEntries) == 0) {
|
||||
$currentSprint = $this->sprintRepository->getCurrentSprint($projectId);
|
||||
|
||||
if ($currentSprint !== false) {
|
||||
$sprintReport = $this->reportRepository->runTicketReport($projectId, $currentSprint->id);
|
||||
if ($sprintReport !== false) {
|
||||
$this->reportRepository->addReport($sprintReport);
|
||||
}
|
||||
}
|
||||
|
||||
$backlogReport = $this->reportRepository->runTicketReport($projectId, '');
|
||||
|
||||
if ($backlogReport !== false) {
|
||||
|
||||
$this->reportRepository->addReport($backlogReport);
|
||||
|
||||
Cache::put('dailyReports-'.$projectId, dtHelper()->dbNow()->endOfDay(), 14400); // 4hours
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public function cronDailyIngestion(): void
|
||||
{
|
||||
$projects = $this->projectRepository->getAll();
|
||||
|
||||
foreach ($projects as $project) {
|
||||
$this->runIngestionForProject($project['id']);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a project's stored report history, authorized against the requested project.
|
||||
*
|
||||
* $projectId is typed int so the param is REQUIRED and non-null: a JSON-RPC caller cannot
|
||||
* pass null to make PermissionEnforcer::resolveProjectId() fall back to the session project
|
||||
* (its isset() check treats explicit null as absent), which would dodge the per-target gate.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(ReportsPermissions::VIEW, projectIdParam: 'projectId')]
|
||||
public function getFullReport(int $projectId): false|array
|
||||
{
|
||||
return $this->reportRepository->getFullReport($projectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a project's current ticket report, authorized against the requested project.
|
||||
* The repository scopes by BOTH projectId and sprint, so a foreign sprint id yields no rows.
|
||||
*
|
||||
* $projectId is typed int for the same reason as getFullReport() — it keeps the dispatch gate
|
||||
* bound to the requested project (no null → session fallback). $sprintId stays mixed because
|
||||
* the empty string is the meaningful "backlog" selector.
|
||||
*
|
||||
* @throws BindingResolutionException
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
#[RequiresPermission(ReportsPermissions::VIEW, projectIdParam: 'projectId')]
|
||||
public function getRealtimeReport(int $projectId, $sprintId): array|bool
|
||||
{
|
||||
return $this->reportRepository->runTicketReport($projectId, $sprintId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the anonymous telemetry payload (instance-wide usage counts + company GUID).
|
||||
*
|
||||
* Not @api: system-level data source for sendAnonymousTelemetry() only. Exposing it over
|
||||
* JSON-RPC let any authenticated user read instance-wide aggregates (user/project/feature
|
||||
* counts) — reconnaissance, not a user capability.
|
||||
*/
|
||||
public function getAnonymousTelemetry(
|
||||
IdeaRepository $ideaRepository,
|
||||
UserRepository $userRepository,
|
||||
ClientRepository $clientRepository,
|
||||
CommentRepository $commentsRepository,
|
||||
TimesheetRepository $timesheetRepo,
|
||||
BlueprintsRepository $blueprintsRepo
|
||||
): array {
|
||||
|
||||
// Get anonymous company guid
|
||||
$companyId = $this->settings->getCompanyId();
|
||||
|
||||
self::dispatch_event('beforeTelemetrySend', ['companyId' => $companyId]);
|
||||
|
||||
$companyLang = $this->settings->getSetting('companysettings.language');
|
||||
if ($companyLang != '' && $companyLang !== false) {
|
||||
$currentLanguage = $companyLang;
|
||||
} else {
|
||||
$currentLanguage = $this->config->language;
|
||||
}
|
||||
|
||||
$projectStatusCount = $this->getProjectStatusReport();
|
||||
|
||||
$taskSentiment = $this->generateTicketReactionsReport();
|
||||
|
||||
$telemetry = [
|
||||
'date' => '',
|
||||
'companyId' => $companyId,
|
||||
'env' => 'oss',
|
||||
'version' => $this->appSettings->appVersion,
|
||||
'language' => $currentLanguage,
|
||||
'numUsers' => $userRepository->getNumberOfUsers(),
|
||||
'lastUserLogin' => $userRepository->getLastLogin(),
|
||||
|
||||
'numProjects' => $this->projectRepository->getNumberOfProjects(null, 'project'),
|
||||
'numProjectsGreen' => $projectStatusCount['green'] ?? 0,
|
||||
'numProjectsYellow' => $projectStatusCount['yellow'] ?? 0,
|
||||
'numProjectsRed' => $projectStatusCount['red'] ?? 0,
|
||||
'numProjectsNone' => $projectStatusCount['none'] ?? 0,
|
||||
|
||||
'numStrategies' => $this->projectRepository->getNumberOfProjects(null, 'strategy'),
|
||||
'numPrograms' => $this->projectRepository->getNumberOfProjects(null, 'program'),
|
||||
'numClients' => $clientRepository->getNumberOfClients(),
|
||||
'numComments' => $commentsRepository->countComments(),
|
||||
'numMilestones' => $this->ticketRepository->getNumberOfMilestones(),
|
||||
'numTickets' => $this->ticketRepository->getNumberOfAllTickets(),
|
||||
|
||||
'numBoards' => $ideaRepository->getNumberOfBoards(),
|
||||
|
||||
'numIdeaItems' => $ideaRepository->getNumberOfIdeas(),
|
||||
'numHoursBooked' => $timesheetRepo->getHoursBooked(),
|
||||
|
||||
'numResearchBoards' => $blueprintsRepo->getNumberOfBoards(null, 'leancanvas'),
|
||||
'numResearchItems' => $blueprintsRepo->getNumberOfCanvasItems(null, 'leancanvas'),
|
||||
|
||||
'numRetroBoards' => $blueprintsRepo->getNumberOfBoards(null, 'retroscanvas'),
|
||||
'numRetroItems' => $blueprintsRepo->getNumberOfCanvasItems(null, 'retroscanvas'),
|
||||
|
||||
'numGoalBoards' => $blueprintsRepo->getNumberOfBoards(null, 'goalcanvas'),
|
||||
'numGoalItems' => $blueprintsRepo->getNumberOfCanvasItems(null, 'goalcanvas'),
|
||||
|
||||
'numValueCanvasBoards' => $blueprintsRepo->getNumberOfBoards(null, 'valuecanvas'),
|
||||
'numValueCanvasItems' => $blueprintsRepo->getNumberOfCanvasItems(null, 'valuecanvas'),
|
||||
|
||||
'numMinEmpathyBoards' => $blueprintsRepo->getNumberOfBoards(null, 'minempathycanvas'),
|
||||
'numMinEmpathyItems' => $blueprintsRepo->getNumberOfCanvasItems(null, 'minempathycanvas'),
|
||||
|
||||
'numOBMBoards' => $blueprintsRepo->getNumberOfBoards(null, 'obmcanvas'),
|
||||
'numOBMItems' => $blueprintsRepo->getNumberOfCanvasItems(null, 'obmcanvas'),
|
||||
|
||||
'numSWOTBoards' => $blueprintsRepo->getNumberOfBoards(null, 'swotcanvas'),
|
||||
'numSWOTItems' => $blueprintsRepo->getNumberOfCanvasItems(null, 'swotcanvas'),
|
||||
|
||||
'numSBBoards' => $blueprintsRepo->getNumberOfBoards(null, 'sbcanvas'),
|
||||
'numSBItems' => $blueprintsRepo->getNumberOfCanvasItems(null, 'sbcanvas'),
|
||||
|
||||
'numRISKSBoards' => $blueprintsRepo->getNumberOfBoards(null, 'riskscanvas'),
|
||||
'numRISKSItems' => $blueprintsRepo->getNumberOfCanvasItems(null, 'riskscanvas'),
|
||||
|
||||
'numEABoards' => $blueprintsRepo->getNumberOfBoards(null, 'eacanvas'),
|
||||
'numEAItems' => $blueprintsRepo->getNumberOfCanvasItems(null, 'eacanvas'),
|
||||
|
||||
'numINSIGHTSBoards' => $blueprintsRepo->getNumberOfBoards(null, 'insightscanvas'),
|
||||
'numINSIGHTSItems' => $blueprintsRepo->getNumberOfCanvasItems(null, 'insightscanvas'),
|
||||
|
||||
'numWikiBoards' => $blueprintsRepo->getNumberOfBoards(null, 'wiki'),
|
||||
'numWikiItems' => $blueprintsRepo->getNumberOfCanvasItems(null, 'wiki'),
|
||||
|
||||
'numTaskSentimentAngry' => $taskSentiment['🤬'] ?? 0,
|
||||
'numTaskSentimentDisgust' => $taskSentiment['🤢'] ?? 0,
|
||||
'numTaskSentimentUnhappy' => $taskSentiment['🙁'] ?? 0,
|
||||
'numTaskSentimentNeutral' => $taskSentiment['😐'] ?? 0,
|
||||
'numTaskSentimentHappy' => $taskSentiment['🙂'] ?? 0,
|
||||
'numTaskSentimentLove' => $taskSentiment['😍'] ?? 0,
|
||||
'numTaskSentimenUnicorn' => $taskSentiment['🦄'] ?? 0,
|
||||
|
||||
'serverSoftware' => $_SERVER['SERVER_SOFTWARE'] ?? 'unknown',
|
||||
'phpUname' => php_uname(),
|
||||
'isDocker' => $this->isRunningInDocker(),
|
||||
'phpSapiName' => php_sapi_name(),
|
||||
'phpOs' => PHP_OS,
|
||||
|
||||
];
|
||||
|
||||
$telemetry = self::dispatch_filter('beforeReturnTelemetry', $telemetry);
|
||||
|
||||
return $telemetry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the daily anonymous telemetry ping (throttled to once a day, opt-out respected).
|
||||
*
|
||||
* Not @api: a system task invoked by the scheduler (register.php cron, no session user) and
|
||||
* the dashboard's lazy trigger — never a user-invokable RPC method (it makes an outbound
|
||||
* HTTP request).
|
||||
*
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function sendAnonymousTelemetry(): bool|PromiseInterface
|
||||
{
|
||||
|
||||
// Only send once a day
|
||||
|
||||
$allowTelemetry = app('config')->allowTelemetry ?? true;
|
||||
|
||||
if ($allowTelemetry === true) {
|
||||
$date_utc = new DateTime('now', new DateTimeZone('UTC'));
|
||||
$today = $date_utc->format('Y-m-d');
|
||||
$lastUpdate = $this->settings->getSetting('companysettings.telemetry.lastUpdate');
|
||||
|
||||
if ($lastUpdate != $today) {
|
||||
$telemetry = app()->call([$this, 'getAnonymousTelemetry']);
|
||||
$telemetry['date'] = $today;
|
||||
|
||||
// Do the curl
|
||||
$httpClient = new Client;
|
||||
|
||||
try {
|
||||
|
||||
$data_string = json_encode($telemetry);
|
||||
|
||||
$promise = $httpClient->postAsync('https://telemetry.leantime.io', [
|
||||
'form_params' => [
|
||||
'telemetry' => $data_string,
|
||||
],
|
||||
// Short connect timeout so an offline/air-gapped server (or a
|
||||
// CI runner with no egress) fails fast instead of blocking the
|
||||
// dashboard's Welcome widget — and saturating PHP-FPM workers —
|
||||
// for minutes. The previous 480s total timeout hung the page
|
||||
// when telemetry was unreachable. (#3372/#3373)
|
||||
'connect_timeout' => 2,
|
||||
'timeout' => 5,
|
||||
])->then(function ($response) use ($today) {
|
||||
$this->settings->saveSetting('companysettings.telemetry.lastUpdate', $today);
|
||||
});
|
||||
|
||||
return $promise;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error($e);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opts the whole instance out of telemetry (writes companysettings.telemetry.active=false).
|
||||
*
|
||||
* Not @api: an instance-wide settings MUTATION. It is invoked internally by the admin-gated
|
||||
* company-settings save (Setting::saveCompanySettings) — over JSON-RPC it previously let ANY
|
||||
* authenticated user flip the company-wide telemetry setting.
|
||||
*
|
||||
* @return false|void
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function optOutTelemetry()
|
||||
{
|
||||
$date_utc = new DateTime('now', new DateTimeZone('UTC'));
|
||||
$today = $date_utc->format('Y-m-d');
|
||||
|
||||
$companyId = $this->settings->getCompanyId();
|
||||
|
||||
$telemetry = [
|
||||
'date' => '',
|
||||
'companyId' => $companyId,
|
||||
'version' => $this->appSettings->appVersion,
|
||||
'language' => '',
|
||||
'numUsers' => 0,
|
||||
'lastUserLogin' => 0,
|
||||
'numProjects' => 0,
|
||||
'numClients' => 0,
|
||||
'numComments' => 0,
|
||||
'numMilestones' => 0,
|
||||
'numTickets' => 0,
|
||||
|
||||
'numBoards' => 0,
|
||||
|
||||
'numIdeaItems' => 0,
|
||||
'numHoursBooked' => 0,
|
||||
|
||||
'numResearchBoards' => 0,
|
||||
'numResearchItems' => 0,
|
||||
|
||||
'numRetroBoards' => 0,
|
||||
'numRetroItems' => 0,
|
||||
|
||||
'numGoalBoards' => 0,
|
||||
'numGoalItems' => 0,
|
||||
|
||||
'numValueCanvasBoards' => 0,
|
||||
'numValueCanvasItems' => 0,
|
||||
|
||||
'numMinEmpathyBoards' => 0,
|
||||
'numMinEmpathyItems' => 0,
|
||||
|
||||
'numOBMBoards' => 0,
|
||||
'numOBMItems' => 0,
|
||||
|
||||
'numSWOTBoards' => 0,
|
||||
'numSWOTItems' => 0,
|
||||
|
||||
'numSBBoards' => 0,
|
||||
'numSBItems' => 0,
|
||||
|
||||
'numRISKSBoards' => 0,
|
||||
'numRISKSItems' => 0,
|
||||
|
||||
'numEABoards' => 0,
|
||||
'numEAItems' => 0,
|
||||
|
||||
'numINSIGHTSBoards' => 0,
|
||||
];
|
||||
|
||||
$telemetry['date'] = $today;
|
||||
|
||||
// Do the curl
|
||||
$httpClient = new Client;
|
||||
|
||||
try {
|
||||
$data_string = json_encode($telemetry);
|
||||
|
||||
$promise = $httpClient->postAsync('https://telemetry.leantime.io', [
|
||||
'form_params' => [
|
||||
'telemetry' => $data_string,
|
||||
],
|
||||
'timeout' => 5,
|
||||
])->then(function ($response) use ($today) {
|
||||
|
||||
$this->settings->saveSetting('companysettings.telemetry.lastUpdate', $today);
|
||||
session(['skipTelemetry' => true]);
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
|
||||
session(['skipTelemetry' => true]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->settings->saveSetting('companysettings.telemetry.active', false);
|
||||
|
||||
session(['skipTelemetry' => true]);
|
||||
|
||||
try {
|
||||
$promise->wait();
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts ALL projects in the instance by status color — a telemetry data source.
|
||||
*
|
||||
* Not @api: instance-wide aggregation with no project scope; over JSON-RPC it disclosed the
|
||||
* whole company's project-health summary to any authenticated user.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getProjectStatusReport()
|
||||
{
|
||||
|
||||
$projectStatus = $this->projectRepository->getAll();
|
||||
|
||||
$statusList = ['green' => 0, 'yellow' => 0, 'red' => 0, 'none' => 0];
|
||||
foreach ($projectStatus as $project) {
|
||||
if (isset($statusList[$project['status']])) {
|
||||
$statusList[$project['status']]++;
|
||||
} else {
|
||||
$statusList['none']++;
|
||||
}
|
||||
}
|
||||
|
||||
return $statusList;
|
||||
}
|
||||
|
||||
public function generateTicketReactionsReport()
|
||||
{
|
||||
$reactionsRepo = app()->make(Reactions::class);
|
||||
$collectedReactions = $reactionsRepo->getReactionsByModule('ticketSentiment');
|
||||
|
||||
$reactions = [
|
||||
'🤬' => 0,
|
||||
'🤢' => 0,
|
||||
'🙁' => 0,
|
||||
'😐' => 0,
|
||||
'🙂' => 0,
|
||||
'😍' => 0,
|
||||
'🦄' => 0,
|
||||
'other' => 0,
|
||||
];
|
||||
|
||||
foreach ($collectedReactions as $reaction) {
|
||||
if (isset($reactions[$reaction['reaction']])) {
|
||||
$reactions[$reaction['reaction']] = $reactions[$reaction['reaction']] + $reaction['reactionCount'];
|
||||
}
|
||||
}
|
||||
|
||||
return $reactions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if Leantime is running in a Docker environment
|
||||
* Uses multiple detection methods and handles errors gracefully
|
||||
*/
|
||||
private function isRunningInDocker(): bool
|
||||
{
|
||||
// Method 1: Check for /.dockerenv file
|
||||
try {
|
||||
if (is_file('/.dockerenv')) {
|
||||
return true;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Silently fail if file access is restricted
|
||||
}
|
||||
|
||||
// Method 2: Check for Docker-specific environment variables
|
||||
if (getenv('DOCKER_CONTAINER') !== false || getenv('IS_DOCKER') !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Method 3: Check cgroup info (works on Linux hosts)
|
||||
try {
|
||||
return strpos(file_get_contents('/proc/1/cgroup'), 'docker') !== false;
|
||||
} catch (\Exception $e) {
|
||||
return false; // Return false if all detection methods fail
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user