Add database vacuuming, in/outbox purging, fixes

This commit is contained in:
2025-08-09 01:15:40 +02:00
parent 8dad12806e
commit 44c4cf229e
9 changed files with 127 additions and 14 deletions

View File

@@ -6,7 +6,10 @@
"autoload": {
"psr-4": {
"Casey\\Daemon\\": "src/"
}
},
"files": [
"src/functions.php"
]
},
"authors": [
{

View File

@@ -6,7 +6,10 @@ caseConfirmUrl: 'https://casey.domain.tld/confirm/{code}'
teamInviteReminder: 1d
teamInviteCount: 3
caseConfirmation: true
# Set to false to disable the mail functionality entirely
mailEnabled: true
# Delete entries from the mail spool after period
mailSpoolExpiry: 7d
mailSubjectFormat: '[%reference%] %prefix%: %subject%'
mailSubjectPrefix: 'Support case'
mailSyncTimer: 10

View File

@@ -11,7 +11,8 @@ html {
body {
background: var(--bg-main);
font-family: monospace;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 10pt;
margin: 0px;
padding: 0px;
}
@@ -40,6 +41,26 @@ table.table {
}
}
table.props {
border-collapse: collapse;
border: solid 1px var(--border-color);
width: 100%;
& tr {
border-top: solid 1px var(--border-color);
border-bottom: solid 1px var(--border-color);
}
& th {
font-weight: bold;
text-align: left;
padding-right: 1.5em;
padding: 0.25rem;
background: #ddd;
}
& td {
padding: 0.25rem;
}
}
.w-0 {
width: 0px;
white-space: nowrap;
@@ -57,13 +78,13 @@ table.table {
}
h1,h2,h3,h4,h5,h6 {
margin: 0.25rem 0rem;
margin: 0.40rem 0rem 0.20rem 0rem;
}
.badge {
position: relative;
top: -0.05rem;
padding: 0.2rem 0.1rem 0.1rem 0.1rem;
top: -0.15rem;
padding: 0.05rem 0.1rem 0.10rem 0.1rem;
font-size: 70%;
font-weight: bold;
border-radius: 0.25rem;
@@ -76,6 +97,10 @@ h1,h2,h3,h4,h5,h6 {
background: #c9a61a;
color: white;
}
&.-blue {
background: #2462c8;
color: white;
}
}
a, a:visited {
@@ -119,4 +144,9 @@ button {
.d-flex-row { display: flex; flex-direction: row; }
.d-flex-column { display: flex; flex-direction: column; }
.d-block { display: block; }
.flex-fill { flex-grow: 1; }
.flex-fill { flex-grow: 1; }
.small { font-size:90%; }
.muted { color: #666; }
.fw-bold { font-weight: bold; }

View File

@@ -3,32 +3,51 @@
{% block content %}
<h3>Case {{ case.reference }}: {{ case.subject }}</h3>
<div class="small muted fw-bold">{%
if (case.is_closed) %}Closed case{%
elseif (case.is_accepted) %}Open case{%
elseif (case.is_confirmed) %}Pending case{%
else %}Unconfirmed case{%
endif %}: {{ case.reference }}</div>
{# <div>{{ case|json_encode }}</div> #}
<h1 style="margin-top:0.1rem;">"{{ case.subject }}"</h1>
<div style="display:flex;">
<div style="flex-grow:1;">
<h4>Details</h4>
<table class="props">
{% for prop in [ 'uuid', 'reference', 'created', 'updated', 'subject', 'category', 'tags', 'is_closed', 'resolution' ] %}
<div>{{prop}}: <span style="font-weight:bold">{{case[prop]|json_encode}}</span></div>
<tr>
<th>{{prop}}</th>
<td>{{case[prop]|json_encode}}</td>
</tr>
{% endfor %}
</table>
</div>
<div style="margin-left:1rem; min-width:30%;">
<h4>Participants</h4>
<div style="border: solid 1px var(--border-color); border-top:none;">
{% for participant in case.participants %}
<div>
<div style="border-top: solid 1px var(--border-color); padding:0.25rem;">
<div>
<span style="font-weight:bold;">{{participant.name}}</span>
{% if participant.team %}<span class="badge -green">TEAM</span>{% endif %}
{% if participant.uuid == case.creator.uuid %}<span class="badge -yellow">CREATOR</span>{% endif %}
{% if participant.uuid == case.creator.uuid %}<span class="badge -blue">CREATOR</span>{% endif %}
</div>
<div style="font-size:80%;">{{participant.email}}</div>
</div>
{% endfor %}
</div>
<h4 style="margin-top:1rem;">Metadata</h4>
<table class="props">
{% for key,value in case.metadata %}
<div>{{ key }}: <span style="font-weight:bold;">{{value|json_encode}}</span></div>
<tr>
<th style="width:30%;">{{ key }}</th>
<td>{{value|json_encode}}</td>
</tr>
{% endfor %}
</table>
</div>
</div>

View File

@@ -22,6 +22,7 @@ class Configuration implements EventEmitterInterface
const CONFIG_TEAM_INVITE_REMINDER = 'teamInviteReminder';
const CONFIG_TEAM_INVITE_COUNT = 'teamInviteCount';
const CONFIG_MAIL_ENABLE = 'mailEnable';
const CONFIG_MAIL_SPOOL_EXPIRY = 'mailSpoolExpiry';
const CONFIG_MAIL_SUBJECT_FORMAT = 'mailSubjectFormat';
const CONFIG_MAIL_SUBJECT_PREFIX = 'mailSubjectPrefix';
const CONFIG_MAIL_SYNC_TIMER = 'mailSyncTimer';
@@ -52,6 +53,7 @@ class Configuration implements EventEmitterInterface
self::CONFIG_TEAM_INVITE_COUNT => 3,
self::CONFIG_CASE_CONFIRMATION => false,
self::CONFIG_MAIL_ENABLE => true,
self::CONFIG_MAIL_SPOOL_EXPIRY => "7d",
self::CONFIG_MAIL_SUBJECT_FORMAT => "[%reference%] %prefix%: %subject%",
self::CONFIG_MAIL_SUBJECT_PREFIX => "Case",
self::CONFIG_MAIL_SYNC_TIMER => 60,

View File

@@ -409,7 +409,7 @@ class CaseController
* @param int $participant
* @return PromiseInterface
*/
#[Route(path:"/case/{uuid}/participant/{participant}", methods:["DELETE"])]
#[Route(path:"/case/{uuid}/participant/{participant}", methods:["DELETE"], requires:["@ADMIN"])]
public function deleteCaseParticipant(ServerRequestInterface $request, Deferred $deferred, string $uuid, int $participant): PromiseInterface
{
// TODO delete participant
@@ -451,13 +451,20 @@ class CaseController
$result = $schemaService->validateData("message", $message, [ "from", "message" ]);
$this->logger->debug("Checking message source: {$message->from}");
if (preg_match('|^(.+?) <(.+?)>$|', $message->from, $m)) {
$fromName = $m[1];
$fromMail = $m[2];
} else {
$fromName = $message->from;
$fromMail = $message->from;
}
try {
$personId = $this->database->findPersonIdFromQuery($message->from);
$personId = $this->database->findPersonIdFromQuery($fromMail);
$person = $this->database->getPerson($personId);
$this->logger->debug(" {$message->from} -> {$personId} (existing)");
} catch (\Exception $e) {
// TODO ensure only e-mail addresses
$person = $this->database->createPerson($message->from, $message->from, false);
$person = $this->database->createPerson($fromName, $fromMail, false);
$personId = $person['id'];
$this->logger->debug(" {$message->from} -> {$personId} (created)");
}

View File

@@ -30,6 +30,8 @@ class Database
]);
$this->configureSchema();
printf("Database INIT: %s\n", $this->dataDirectory);
Loop::addPeriodicTimer(3600, fn() => $this->queryOne("VACUUM"));
}
private function configureSchema(): void
@@ -408,6 +410,9 @@ class Database
public function updateCase(array $data, int $id): void
{
$ckey = "case:{$id}";
$this->cache->delete($ckey);
$data['updated'] = date('Y-m-d H:i:s P');
$this->connection->update('cases', $data, [ 'id' => $id ]);
}
@@ -422,6 +427,9 @@ class Database
*/
public function addCaseMessage(int $id, int $personId, array $message): array
{
$ckey = "case:{$id}";
$this->cache->delete($ckey);
$data = [
'case_id' => $id,
'person_id' => $personId,

View File

@@ -175,6 +175,7 @@ class MailerService
$this->syncImapInbox();
$this->sendSpool();
$this->processInbox();
$this->prune();
}
/**
@@ -348,4 +349,13 @@ class MailerService
}
}
private function prune(): void
{
$duration = $this->config->get(Configuration::CONFIG_MAIL_SPOOL_EXPIRY);
$secs = duration($duration);
$cutoff = date("Y-m-d H:i:s P",time()-$secs);
$this->database->queryOne("DELETE FROM inbox WHERE received < :cutoff", [ 'cutoff' => $cutoff ]);
$this->database->queryOne("DELETE FROM outbox WHERE sent < :cutoff", [ 'cutoff' => $cutoff ]);
}
}

31
src/functions.php Normal file
View File

@@ -0,0 +1,31 @@
<?php
/**
* Convert a duration
*
* @param integer|string $duration Duration as string or integer
* @param string $base Base unit to use when duration is an integer
* @return integer
*/
function duration(int|string $duration, string $base='s'): int {
if (ctype_digit($duration)) {
$duration = sprintf("%d%s", $duration, $base);
}
if (preg_match_all("|((\d+)(\w))|", "7d12h", $m)) {
$range = array_combine($m[3], $m[2]);
$result = 0;
foreach ($range as $unit=>$value) {
$result += ($value * match ($unit) {
'w' => 60*60*24*7,
'd' => 60*60*24,
'h' => 60*60,
'm' => 60,
's' => 1,
default => throw new \Error("Invalid base unit passed to duration(): {$base}"),
});
}
return $result;
} else {
return 0;
}
}