all
Some checks failed
Publish Docker Image / build-and-push (push) Failing after 2m13s

This commit is contained in:
Sam Kintop
2026-04-30 09:44:02 -05:00
parent 0a30837e3d
commit bf3870ccca
111 changed files with 8383 additions and 0 deletions

22
web/frontend/404.php Normal file
View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<?php include __DIR__ . '/parts/head.php'; ?>
<title>404 - Page not found</title>
</head>
<body>
<?php include __DIR__ . '/parts/header.php'; ?>
<main>
<div class="error-page">
<div class="error-code">404</div>
<div class="error-message">Page not found</div>
<p class="error-description">The log you're looking for doesn't exist or has expired.</p>
<a href="/" class="btn btn-blue">
<i class="fa-solid fa-home"></i>
Back to Home
</a>
</div>
</main>
<?php include __DIR__ . '/parts/footer.php'; ?>
</body>
</html>

639
web/frontend/api-docs.php Normal file
View File

@@ -0,0 +1,639 @@
<?php
use Aternos\Mclogs\Api\Action\BulkDeleteLogsAction;
use Aternos\Mclogs\Api\Response\ApiError;
use Aternos\Mclogs\Api\Response\ApiResponse;
use Aternos\Mclogs\Api\Response\MultiResponse;
use Aternos\Mclogs\Config\Config;
use Aternos\Mclogs\Config\ConfigKey;
use Aternos\Mclogs\Util\URL;
$config = Config::getInstance();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<?php include __DIR__ . '/parts/head.php'; ?>
<title>API Documentation - <?= htmlspecialchars($config->getName()); ?></title>
<meta name="description" content="API documentation for <?= htmlspecialchars($config->getName()); ?> - Integrate log sharing directly into your server panel or hosting software." />
</head>
<body>
<?php include __DIR__ . '/parts/header.php'; ?>
<main>
<div class="api-docs-header">
<div class="api-docs-header-content">
<h1>API Documentation</h1>
<p>Integrate <strong><?= htmlspecialchars($config->getName()); ?></strong> directly into your server panel, your hosting software or anything else. This platform was built for high performance automation and can easily be integrated into any existing software via our HTTP API.</p>
</div>
</div>
<div class="api-docs-toc">
<h3>Quick Links</h3>
<nav class="api-docs-toc-nav">
<a href="#create-log">Create a log</a>
<a href="#get-log-info">Get log info and content</a>
<a href="#delete-log">Delete a log</a>
</nav>
</div>
<div class="api-docs-section" id="create-log">
<h2>Create a log</h2>
<div class="api-endpoint">
<span class="api-method">POST</span> <span class="api-url"><?= htmlspecialchars(URL::getApi()->withPath("/1/log")->toString()); ?></span> <span class="content-type">application/json</span>
</div>
<div class="api-note">
Posting content with the content type <span class="content-type">application/x-www-form-urlencoded</span> is still supported for backwards compatibility, but does not support setting metadata.
</div>
<table class="api-table">
<tr>
<th>Field</th>
<th>Required</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td class="api-field">content</td>
<td class="api-required required"><i class="fa-solid fa-square-check"></i></td>
<td class="api-type">string</td>
<td class="api-description">
The raw log file content as string.
Limited to <?= number_format($config->get(ConfigKey::STORAGE_LIMIT_BYTES) / 1024 / 1024, 2); ?> MiB and <?= number_format($config->get(ConfigKey::STORAGE_LIMIT_LINES)); ?> lines.
Will be truncated if possible and necessary, but truncating on the client side is recommended.
</td>
</tr>
<tr>
<td class="api-field">source</td>
<td class="api-required"><i class="fa-solid fa-square-xmark"></i></td>
<td class="api-type">string</td>
<td class="api-description">The name of the source, e.g. a domain or software name.</td>
</tr>
<tr>
<td class="api-field">metadata</td>
<td class="api-required"><i class="fa-solid fa-square-xmark"></i></td>
<td class="api-type">array</td>
<td class="api-description">An array of metadata entries.</td>
</tr>
</table>
<h3>Example body <span class="content-type">application/json</span></h3>
<pre class="api-code">{
"content": "[log file content...]",
"source": "example.org"
}</pre>
<h3>Metadata</h3>
<p>
You can send metadata alongside the log content to be displayed on the log page and/or be read by other applications through this API.
This is entirely optional, but can help to provide additional context, e.g. internal server IDs, software versions etc.
</p>
<p>
A metadata entry is an object with the following fields:
</p>
<table class="api-table">
<tr>
<th>Field</th>
<th>Required</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td class="api-field">key</td>
<td class="api-required required"><i class="fa-solid fa-square-check"></i></td>
<td class="api-type">string</td>
<td class="api-description">The metadata key. Can be used to identify the entry in your code later.</td>
</tr>
<tr>
<td class="api-field">value</td>
<td class="api-required required"><i class="fa-solid fa-square-check"></i></td>
<td class="api-type">string|int|float|bool|null</td>
<td class="api-description">The metadata value.</td>
</tr>
<tr>
<td class="api-field">label</td>
<td class="api-required"><i class="fa-solid fa-square-xmark"></i></td>
<td class="api-type">string</td>
<td class="api-description">The display label. If not provided, the key will be used as label.</td>
</tr>
<tr>
<td class="api-field">visible</td>
<td class="api-required"><i class="fa-solid fa-square-xmark"></i></td>
<td class="api-type">bool</td>
<td class="api-description">Whether this metadata should be visible on the log page or is only available through the API. Default is true.</td>
</tr>
</table>
<h3>Example body with metadata <span class="content-type">application/json</span></h3>
<pre class="api-code">{
"content": "[log file content...]",
"source": "example.org",
"metadata": [
{
"key": "server_id",
"value": 12345,
"visible": false
},
{
"key": "software_version",
"value": "1.2.3",
"label": "Software Version",
"visible": true
}
]
}</pre>
<h3>Responses</h3>
<h4>Success <span class="content-type">application/json</span></h4>
<div class="api-note">
The token provided in this response can be used to delete this log later. Store or discard it securely, it will not be shown again.
</div>
<pre class="api-code">{
"success":true,
"id":"WnMMikq",
"source":null,
"created":1769597979,
"expires":1777373979,
"size":157369,
"lines":1201,
"errors":8,
"url": "<?= htmlspecialchars(URL::getBase()->withPath("/WnMMikq")->toString()); ?>",
"raw": "<?= htmlspecialchars(URL::getApi()->withPath("/1/raw/WnMMikq")->toString()); ?>",
"token":"78351fafe495398163fff847f9a26dda440435dcf7b5f92e8e36308f3683d771",
"metadata": [
{
"key": "server_id",
"value": 12345,
"visible": false
},
{
"key": "software_version",
"value": "1.2.3",
"label": "Software Version",
"visible": true
}
]
}</pre>
<h4>Error <span class="content-type">application/json</span></h4>
<pre class="api-code">
{
"success": false,
"error": "Required field 'content' not found."
}</pre>
</div>
<div class="api-docs-section" id="get-log-info">
<h2>Get log info and content</h2>
<div class="api-endpoint">
<span class="api-method">GET</span> <span class="api-url"><?= htmlspecialchars(URL::getApi()->toString()); ?>/1/log/[id]</span>
</div>
<p>
This endpoint only returns the log info and metadata by default (same response as creating a log), you can also get the content in the same request by enabling it in different
formats using GET parameters. You can combine multiple parameters to get multiple content formats in one request, but keep in mind that this will
increase the response size.
</p>
<table class="api-table">
<tr>
<th>GET Parameter</th>
<th>Response field</th>
<th>Description</th>
</tr>
<tr>
<td class="api-field">raw</td>
<td class="api-type">content.raw</td>
<td class="api-description">Includes the raw log content as string in the response.</td>
</tr>
<tr>
<td class="api-field">parsed</td>
<td class="api-type">content.parsed</td>
<td class="api-description">Includes the parsed log content as array/objects in the response.</td>
</tr>
<tr>
<td class="api-field">insights</td>
<td class="api-type">content.insights</td>
<td class="api-description">Includes the automatically detected insights in the response.</td>
</tr>
</table>
<h3>Responses</h3>
<h4>Success <span class="content-type">application/json</span></h4>
<div class="api-note">
All content fields are only included if the corresponding GET parameter is provided.
If no content parameter is provided, the entire content object is omitted from the response.
</div>
<pre class="api-code">{
"success":true,
"id":"WnMMikq",
"source":null,
"created":1769597979,
"expires":1777373979,
"size":157369,
"lines":1201,
"errors":8,
"url": "<?= htmlspecialchars(URL::getBase()->withPath("/WnMMikq")->toString()); ?>",
"raw": "<?= htmlspecialchars(URL::getApi()->withPath("/1/raw/WnMMikq")->toString()); ?>",
"metadata": [
{
"key": "server_id",
"value": 12345,
"visible": false
},
{
"key": "software_version",
"value": "1.2.3",
"label": "Software Version",
"visible": true
}
],
"content": {
"raw": "[log file content...]",
"parsed": [ /* parsed log entries */ ],
"insights": { "problems": [ /* detected problems */ ], "information": [ /* detected information */ ] }
}
}</pre>
<h4>Error <span class="content-type">application/json</span></h4>
<pre class="api-code">
{
"success": false,
"error": "Log not found."
}</pre>
</div>
<div class="api-docs-section" id="delete-log">
<h2>Delete a log</h2>
<div class="api-note">
Deleting a log requires the token that was provided when creating the log.
</div>
<div class="api-endpoint">
<span class="api-method">DELETE</span> <span class="api-url"><?= htmlspecialchars(URL::getApi()->toString()); ?>/1/log/[id]</span>
</div>
<h3>Headers</h3>
<table class="api-table">
<tr>
<th>Header</th>
<th>Example</th>
<th>Description</th>
</tr>
<tr>
<td class="api-field">Authorization</td>
<td class="api-type">Authorization: Bearer 78351fafe495398163f...</td>
<td class="api-description">The type (always "Bearer") and the log token received when creating the log.</td>
</tr>
</table>
<h3>Responses</h3>
<h4>Success <span class="content-type">application/json</span></h4>
<pre class="api-code">{
"success": true
}</pre>
<h4>Error <span class="content-type">application/json</span></h4>
<pre class="api-code">
{
"success": false,
"error": "Invalid token."
}</pre>
</div>
<div class="api-docs-section" id="bulk-delete-log">
<h2>Bulk delete multiple logs</h2>
<div class="api-note">
This method allows deleting up to <?= BulkDeleteLogsAction::MAX_IDS; ?> at once.
Deleting logs requires the tokens that were provided when the logs were created.
</div>
<div class="api-endpoint">
<span class="api-method">POST</span> <span class="api-url"><?= htmlspecialchars(URL::getApi()->toString()); ?>/1/bulk/log/delete</span>
</div>
<h3>Example body <span class="content-type">application/json</span></h3>
<pre class="api-code"><?= json_encode([
[
"id" => "6wexMDE",
"token" => "78351fafe495398163fff847f9a26dda440435dcf7b5f92e8e36308f3683d771"
],
[
"id" => "OahzhMG",
"token" => "6520dd42ec3d5fd0e83f28220974fb83d3bdc0746853f5022373f8e5b062651b"
],
], JSON_PRETTY_PRINT); ?></pre>
<h3>Responses</h3>
<h4>Success <span class="content-type">application/json</span></h4>
<div class="api-note">
The bulk delete request will return a successful result and status code <code>207</code>,
indicating that the request was processed.
Results for the individual operations are included in the response body.
</div>
<pre class="api-code"><?=json_encode(new MultiResponse()
->addResponse("6wexMDE", new ApiResponse())
->addResponse("OahzhMG", new ApiResponse()), JSON_PRETTY_PRINT); ?></pre>
<h4>Partial success <span class="content-type">application/json</span></h4>
<div class="api-note">
If a bulk delete request is valid, but not all logs can be deleted (e.g. due to invalid tokens or non-existing logs),
it will still overall be considered successful, but the response body will include error results for the logs that could not be deleted.
</div>
<pre class="api-code"><?=json_encode(new MultiResponse()
->addResponse("6wexMDE", new ApiResponse())
->addResponse("OahzhMG", new ApiError(404, "Log not found.")), JSON_PRETTY_PRINT); ?></pre>
<h4>Error <span class="content-type">application/json</span></h4>
<div class="api-note">
If a bulk delete request is malformed or invalid, the entire request will be
rejected with an error response and no logs will be deleted.
</div>
<pre class="api-code">
{
"success": false,
"error": "No logs provided."
}</pre>
</div>
<div class="api-docs-section" id="get-raw">
<h2>Get the raw log file content</h2>
<div class="api-note">
Only use this endpoint if you really only need the raw log content. For most use cases, getting the log info and content together from the log endpoint is recommended.
</div>
<div class="api-endpoint">
<span class="api-method">GET</span> <span class="api-url"><?= htmlspecialchars(URL::getApi()->toString()); ?>/1/raw/[id]</span>
</div>
<table class="api-table">
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td class="api-field">[id]</td>
<td class="api-type">string</td>
<td class="api-description">The log file id, received from the paste endpoint or from a URL (<?= htmlspecialchars(URL::getBase()->toString()); ?>/[id]).</td>
</tr>
</table>
<h3>Success <span class="content-type">text/plain</span></h3>
<pre class="api-code">
[18:25:33] [Server thread/INFO]: Starting minecraft server version 1.16.2
[18:25:33] [Server thread/INFO]: Loading properties
[18:25:34] [Server thread/INFO]: Default game type: SURVIVAL
...
</pre>
<h3>Error <span class="content-type">application/json</span></h3>
<pre class="api-code">
{
"success": false,
"error": "Log not found."
}</pre>
</div>
<div class="api-docs-section" id="get-insights">
<h2>Get insights</h2>
<div class="api-note">
This endpoint is mainly kept for backwards compatibility. For new applications, getting the insights together with the log info from the log endpoint is recommended.
</div>
<div class="api-endpoint">
<span class="api-method">GET</span> <span class="api-url"><?= htmlspecialchars(URL::getApi()->toString()); ?>/1/insights/[id]</span>
</div>
<table class="api-table">
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td class="api-field">[id]</td>
<td class="api-type">string</td>
<td class="api-description">The log file id, received from the paste endpoint or from a URL (<?= htmlspecialchars(URL::getBase()->toString()); ?>/[id]).</td>
</tr>
</table>
<h3>Success <span class="content-type">application/json</span></h3>
<pre class="api-code">
{
"id": "name/type",
"name": "Software name, e.g. Vanilla",
"type": "Type name, e.g. Server Log",
"version": "Version, e.g. 1.12.2",
"title": "Combined title, e.g. Vanilla 1.12.2 Server Log",
"analysis": {
"problems": [
{
"message": "A message explaining the problem.",
"counter": 1,
"entry": {
"level": 6,
"time": null,
"prefix": "The prefix of this entry, usually the part containing time and loglevel.",
"lines": [
{
"number": 1,
"content": "The full content of the line."
}
]
},
"solutions": [
{
"message": "A message explaining a possible solution."
}
]
}
],
"information": [
{
"message": "Label: value",
"counter": 1,
"label": "The label of this information, e.g. Minecraft version",
"value": "The value of this information, e.g. 1.12.2",
"entry": {
"level": 6,
"time": null,
"prefix": "The prefix of this entry, usually the part containing time and loglevel.",
"lines": [
{
"number": 6,
"content": "The full content of the line."
}
]
}
}
]
}
}</pre>
<h3>Error <span class="content-type">application/json</span></h3>
<pre class="api-code">
{
"success": false,
"error": "Log not found."
}</pre>
</div>
<div class="api-docs-section" id="analyse">
<h2>Analyse a log without saving it</h2>
<p>
If you only want to use the analysis features of this service without saving the log, you can use this endpoint.
Please do not save logs that you only want to analyse, as this wastes storage space and resources.
</p>
<div class="api-endpoint">
<span class="api-method">POST</span> <span class="api-url"><?= htmlspecialchars(URL::getApi()->withPath("/1/analyse")->toString()); ?></span> <span class="content-type">application/x-www-form-urlencoded</span> <span class="content-type">application/json</span>
</div>
<table class="api-table">
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td class="api-field">content</td>
<td class="api-type">string</td>
<td class="api-description">The raw log file content as string. Maximum length is 10MiB and 25k lines, will be shortened if necessary.</td>
</tr>
</table>
<h3>Success <span class="content-type">application/json</span></h3>
<pre class="api-code">
{
"id": "name/type",
"name": "Software name, e.g. Vanilla",
"type": "Type name, e.g. Server Log",
"version": "Version, e.g. 1.12.2",
"title": "Combined title, e.g. Vanilla 1.12.2 Server Log",
"analysis": {
"problems": [
{
"message": "A message explaining the problem.",
"counter": 1,
"entry": {
"level": 6,
"time": null,
"prefix": "The prefix of this entry, usually the part containing time and loglevel.",
"lines": [
{
"number": 1,
"content": "The full content of the line."
}
]
},
"solutions": [
{
"message": "A message explaining a possible solution."
}
]
}
],
"information": [
{
"message": "Label: value",
"counter": 1,
"label": "The label of this information, e.g. Minecraft version",
"value": "The value of this information, e.g. 1.12.2",
"entry": {
"level": 6,
"time": null,
"prefix": "The prefix of this entry, usually the part containing time and loglevel.",
"lines": [
{
"number": 6,
"content": "The full content of the line."
}
]
}
}
]
}
}</pre>
<h3>Error <span class="content-type">application/json</span></h3>
<pre class="api-code">
{
"success": false,
"error": "Required field 'content' is empty."
}</pre>
</div>
<div class="api-docs-section" id="check-limits">
<h2>Check storage limits</h2>
<div class="api-endpoint">
<span class="api-method">GET</span> <span class="api-url"><?= htmlspecialchars(URL::getApi()->withPath("/1/limits")->toString()); ?></span>
</div>
<h3>Success <span class="content-type">application/json</span></h3>
<pre class="api-code">
{
"storageTime": 7776000,
"maxLength": 10485760,
"maxLines": 25000
}</pre>
<table class="api-table">
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td class="api-field">storageTime</td>
<td class="api-type">integer</td>
<td class="api-description">The duration in seconds that a log is stored for after the last view.</td>
</tr>
<tr>
<td class="api-field">maxLength</td>
<td class="api-type">integer</td>
<td class="api-description">Maximum file length in bytes. Logs over this limit will be truncated to this length.</td>
</tr>
<tr>
<td class="api-field">maxLines</td>
<td class="api-type">integer</td>
<td class="api-description">Maximum number of lines. Additional lines will be removed.</td>
</tr>
</table>
</div>
<div class="api-docs-section" id="check-limits">
<h2>Get filters</h2>
<p>
Filters modify the log content before storing it. They are applied automatically when creating a new log on the server side.
You can get a list of active filters from this endpoint if you want to apply the same filters on the client side before uploading a log.
</p>
<div class="api-endpoint">
<span class="api-method">GET</span> <span class="api-url"><?= htmlspecialchars(URL::getApi()->withPath("/1/filters")->toString()); ?></span>
</div>
<h3>Success <span class="content-type">application/json</span></h3>
<pre class="api-code">
<?=htmlspecialchars(json_encode(\Aternos\Mclogs\Filter\Filter::getAll(), JSON_PRETTY_PRINT)); ?></pre>
<h3>Filter types</h3>
<table class="api-table">
<tr>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td class="api-field">trim</td>
<td class="api-description">
Trim any whitespace characters from the beginning and end of the log content.
</td>
</tr>
<tr>
<td class="api-field">limit-bytes</td>
<td class="api-description">
Limit the log content to a maximum number of bytes (data.limit). Content exceeding this limit will be truncated.
</td>
</tr>
<tr>
<td class="api-field">limit-lines</td>
<td class="api-description">
Limit the log content to a maximum number of lines (data.limit). Additional lines will be removed.
</td>
</tr>
<tr>
<td class="api-field">regex</td>
<td class="api-description">
Apply regular expression replacements to the log content. Each pattern in data.patterns will be applied in order and replaced with the provided replacement, unless the matched string matches one of the exemption patterns in data.exemptions.
</td>
</tr>
</table>
<div class="api-note">
Make sure to handle any filter error, e.g. unknown filter types gracefully, as new filter types may be added in the future.
</div>
</div>
<div class="api-docs-notes">
<div class="api-docs-notes-content">
<h2>Notes</h2>
<p>The API has currently a rate limit of 60 requests per minute per IP address. This is set to ensure the operability of this service. If you have any use case that requires a higher limit, feel free to contact us.</p>
<div class="api-docs-notes-actions">
<a class="btn btn-small" href="mailto:matthias@aternos.org">
<i class="fa-solid fa-envelope"></i> Contact via mail
</a>
</div>
</div>
</div>
</main>
<?php include __DIR__ . '/parts/footer.php'; ?>
</body>
</html>

230
web/frontend/log.php Normal file
View File

@@ -0,0 +1,230 @@
<?php
use Aternos\Mclogs\Frontend\Assets\AssetLoader;
use Aternos\Mclogs\Frontend\Assets\AssetType;
use Aternos\Mclogs\Log;
use Aternos\Mclogs\Config\Config;
use Aternos\Mclogs\Config\ConfigKey;
use Aternos\Mclogs\Frontend\Settings\Setting;
use Aternos\Mclogs\Frontend\Settings\Settings;
use Aternos\Mclogs\Util\TimeInterval;
/** @var Log $log */
$settings = new Settings();
?><!DOCTYPE html>
<html lang="en">
<head>
<?php include __DIR__ . '/parts/head.php'; ?>
<title><?=htmlspecialchars($log->getPageTitle()); ?></title>
<meta name="description" content="<?=htmlspecialchars($log->getPageDescription()); ?>" />
</head>
<body class="log-body<?=$settings->getBodyClassesString(); ?>">
<?php include __DIR__ . '/parts/header.php'; ?>
<main>
<div class="log-header">
<div class="log-header-inner">
<div class="left">
<div class="log-title">
<h1>
<i class="fas fa-file-lines"></i>
<?=htmlspecialchars($log->getCodexLog()->getTitle()); ?>
</h1>
<button class="log-url-btn" data-clipboard="<?=htmlspecialchars($log->getURL()->toString()); ?>" title="Copy log URL to clipboard">
<span class="log-url"><?=htmlspecialchars($log->getDisplayURL()); ?></span>
<i class="fa-solid fa-copy"></i>
</button>
</div>
</div>
<div class="right">
<div class="details">
<div class="log-info-actions">
<?php if($log->hasErrors()): ?>
<div class="btn btn-danger btn-small" id="error-toggle">
<i class="fa fa-exclamation-circle"></i>
<?=htmlspecialchars($log->getErrorsString()); ?>
</div>
<?php endif; ?>
<div class="btn btn-dark btn-small" id="down-button">
<i class="fa fa-arrow-circle-down"></i>
<?=htmlspecialchars($log->getLinesString()); ?>
</div>
<a class="btn btn-dark btn-small" id="raw" target="_blank" title="Raw log" href="<?=$log->getRawURL()->toString(); ?>">
<i class="fa fa-arrow-up-right-from-square"></i>
Raw
</a>
</div>
</div>
</div>
</div>
<?php $information = $log->getAnalysis()->getInformation(); ?>
<?php if(count($log->getVisibleMetadata()) > 0 || count($information) > 0): ?>
<div class="log-info-rows">
<?php if(count($log->getVisibleMetadata()) > 0): ?>
<div class="log-info-row">
<div class="info-row-items">
<div class="info-row-header">
<i class="fa-solid fa-tags"></i>
<span>Metadata</span>
</div>
<?php foreach($log->getVisibleMetadata() as $metadata): ?>
<span class="info-item">
<span class="info-label"><?=htmlspecialchars($metadata->getDisplayLabel()); ?>:</span>
<span class="info-value"><?=htmlspecialchars($metadata->getDisplayValue()); ?></span>
</span>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php if(count($information) > 0): ?>
<div class="log-info-row">
<div class="info-row-items">
<div class="info-row-header">
<i class="fa-solid fa-cube"></i>
<span>Detected</span>
</div>
<?php foreach($information as $info): ?>
<span class="info-item">
<span class="info-label"><?=htmlspecialchars($info->getLabel()); ?>:</span>
<span class="info-value"><?=htmlspecialchars($info->getValue()); ?></span>
</span>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php $problems = $log->getAnalysis()?->getProblems(); ?>
<?php if(count($problems) > 0): ?>
<div class="problems-panel-container">
<div class="problems-panel">
<div class="problems-header">
<span class="problems-count"><?=count($problems); ?></span>
<span class="problems-title"><?=count($problems) === 1 ? 'Problem' : 'Problems'; ?> detected</span>
</div>
<div class="problems-list">
<?php foreach($problems as $problem): ?>
<?php $number = $problem->getEntry()[0]->getNumber(); ?>
<div class="problem-item">
<a href="/<?=htmlspecialchars($log->getId()->get()) . "#L" . $number; ?>" class="problem-entry" onclick="updateLineNumber('#L<?=$number; ?>');">
<span class="problem-label">
<i class="fa-solid fa-triangle-exclamation"></i>
Problem
</span>
<span class="problem-text"><?=htmlspecialchars($problem->getMessage()); ?></span>
<span class="problem-line">Line <?=$number; ?></span>
</a>
<?php if(count($problem->getSolutions()) > 0): ?>
<div class="problem-solutions">
<span class="problem-solutions-label"><?=count($problem->getSolutions()) === 1 ? 'Solution:' : 'Solutions:'; ?></span>
<?php foreach($problem->getSolutions() as $solution): ?>
<div class="problem-solution">
<i class="fa-solid fa-lightbulb"></i>
<span><?=preg_replace("/'([^']+)'/", "'<strong>$1</strong>'", htmlspecialchars($solution->getMessage())); ?></span>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<?php endif; ?>
</div>
</main>
<div class="log-container">
<div class="log">
<?php
echo $log->getPrinter()->print();
?>
</div>
</div>
<div class="log-footer">
<div class="log-bottom">
<div class="btn btn-small btn-dark" id="up-button" title="Scroll to top">
<i class="fa fa-arrow-circle-up"></i>
</div>
<div class="actions">
<?php if ($log->hasValidTokenCookie()): ?>
<div class="delete-wrapper popover-wrapper">
<button class="delete-trigger popover-trigger btn btn-small btn-danger" title="Delete log" popovertarget="delete-overlay">
<i class="fa-solid fa-trash"></i>
Delete
</button>
<div class="delete-overlay popover-content popover-danger" id="delete-overlay" popover>
<span class="delete-message">Delete this log permanently?</span>
<div class="popover-error">
</div>
<div class="delete-actions">
<button class="btn btn-small btn-white" popovertarget="delete-overlay">Cancel</button>
<button class="btn btn-small btn-danger delete-log-button">Delete</button>
</div>
</div>
</div>
<?php endif; ?>
<div class="settings-dropdown popover-wrapper">
<button class="settings-trigger popover-trigger btn btn-small btn-dark" title="Settings" popovertarget="settings-overlay">
<i class="fas fa-cog"></i>
Settings
</button>
<div class="settings-overlay popover-content" id="settings-overlay" popover>
<?php foreach(Setting::cases() as $setting): ?>
<label class="setting" for="setting-<?=$setting->value; ?>">
<span class="setting-label"><?=$setting->getLabel(); ?></span>
<input type="checkbox"
id="setting-<?=$setting->value; ?>"
class="setting-checkbox"
data-body-class="<?=$setting->getBodyClass() ?? ""; ?>"
data-key="<?=$setting->value; ?>"
<?=($settings->get($setting)) ? " checked" : ""; ?>/>
</label>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<div class="log-details">
<?php
$source = $log->getSource();
$created = $log->getCreated()?->toDateTime()->getTimestamp();
?>
<?php if ($source || $created): ?>
<div class="meta-data">
<?php if ($source): ?>
<div class="source" title="Source">
<i class="fa-solid fa-arrow-up-from-bracket"></i>
<?=htmlspecialchars($source); ?>
</div>
<?php endif; ?>
<?php if ($created): ?>
<div class="created-time" title="Created">
<i class="fa-solid fa-clock"></i>
<span class="created" data-time="<?=htmlspecialchars($created); ?>">
</span>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="delete-notice">
This log will be saved for <?= htmlspecialchars(TimeInterval::getInstance()->format(Config::getInstance()->get(ConfigKey::STORAGE_TTL))); ?> from its last view.
</div>
<?php if ($abuseEmail = Config::getInstance()->get(ConfigKey::LEGAL_ABUSE)): ?>
<a href="mailto:<?=htmlspecialchars($abuseEmail); ?>?subject=Report%20<?=htmlspecialchars(rawurlencode(Config::getInstance()->getName())); ?>/<?=htmlspecialchars($log->getId()->get()); ?>" class="report-link">
<i class="fa-solid fa-flag"></i>
Report abuse
</a>
<?php endif; ?>
</div>
</div>
<?php include __DIR__ . '/parts/footer.php'; ?>
<div class="floating-scrollbar-container">
<div class="floating-scrollbar">
<div class="floating-scrollbar-content">
</div>
</div>
</div>
<?= AssetLoader::getInstance()->getHTML(AssetType::JS, "js/log.js"); ?>
</body>
</html>

View File

@@ -0,0 +1,7 @@
<svg width="41" height="42" viewBox="0 0 41 42" fill="<?=htmlspecialchars(\Aternos\Mclogs\Config\Config::getInstance()->get(\Aternos\Mclogs\Config\ConfigKey::FRONTEND_COLOR_ACCENT)); ?>" xmlns="http://www.w3.org/2000/svg">
<rect width="41" height="5" rx="2"/>
<rect y="9.25" width="33" height="5" rx="2"/>
<rect y="18.5" width="19" height="5" rx="2"/>
<rect y="27.75" width="33" height="5" rx="2"/>
<rect y="37" width="41" height="5" rx="2"/>
</svg>

After

Width:  |  Height:  |  Size: 470 B

View File

@@ -0,0 +1,28 @@
<?php
use Aternos\Mclogs\Config\Config;use Aternos\Mclogs\Config\ConfigKey;use Aternos\Mclogs\Util\URL;
$imprintUrl = Config::getInstance()->get(ConfigKey::LEGAL_IMPRINT);
$privacyUrl = Config::getInstance()->get(ConfigKey::LEGAL_PRIVACY);
?>
<footer>
<?php if($imprintUrl || $privacyUrl): ?>
<nav class="legal">
<?php if ($imprintUrl): ?>
<a href="<?=htmlspecialchars($imprintUrl); ?>" class="footer-link" title="Imprint" target="_blank">Imprint</a>
<?php endif; ?>
<?php if ($imprintUrl && $privacyUrl): ?>
<span class="footer-separator"> - </span>
<?php endif; ?>
<?php if ($privacyUrl): ?>
<a href="<?=htmlspecialchars($privacyUrl); ?>" class="footer-link" title="Privacy Policy" target="_blank">Privacy Policy</a>
<?php endif; ?>
</nav>
<?php endif; ?>
<nav class="footer-nav">
<a href="https://github.com/aternosorg/mclogs" title="mclo.gs on Github" target="_blank"><i class="fa-brands fa-github"></i>GitHub</a>
<a href="https://modrinth.com/plugin/mclogs" title="Download mclo.gs Mod/Plugin" target="_blank"><i class="fa-solid fa-cube"></i>Mod/Plugin</a>
<a href="<?=htmlspecialchars(URL::getApi()->toString()); ?>" title="mclo.gs API"><i class="fa-solid fa-code"></i>API</a>
</nav>
<span class="footer-text">developed by <a href="https://aternos.org" target="_blank" title="Aternos website">Aternos</a>
</span>
</footer>

View File

@@ -0,0 +1,44 @@
<?php
use Aternos\Mclogs\Config\Config;
use Aternos\Mclogs\Config\ConfigKey;
use Aternos\Mclogs\Frontend\Assets\AssetLoader;
use Aternos\Mclogs\Frontend\Assets\AssetType;
use Aternos\Mclogs\Util\URL;
?>
<meta charset="utf-8"/>
<base href="/"/>
<?= AssetLoader::getInstance()->getHTML(AssetType::CSS, "vendor/fontawesome/css/fontawesome.min.css"); ?>
<?= AssetLoader::getInstance()->getHTML(AssetType::CSS, "css/mclogs.css"); ?>
<style>
:root {
--bg: <?= htmlspecialchars(Config::getInstance()->get(ConfigKey::FRONTEND_COLOR_BACKGROUND)); ?>;
--text: <?= htmlspecialchars(Config::getInstance()->get(ConfigKey::FRONTEND_COLOR_TEXT)); ?>;
--accent: <?= htmlspecialchars(Config::getInstance()->get(ConfigKey::FRONTEND_COLOR_ACCENT)); ?>;
--error: <?= htmlspecialchars(Config::getInstance()->get(ConfigKey::FRONTEND_COLOR_ERROR)); ?>;
}
</style>
<link rel="shortcut icon" href="img/favicon.ico" type="image/x-icon" sizes="any"/>
<link rel="shortcut icon" href="<?= htmlspecialchars(URL::getBase()->withPath("/favicon.svg")->toString()); ?>" type="image/svg+xml">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<?php if (Config::getInstance()->get(ConfigKey::FRONTEND_ANALYTICS)): ?>
<script>
let _paq = window._paq = window._paq || [];
_paq.push(['disableCookies']);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function () {
_paq.push(['setTrackerUrl', '/data']);
_paq.push(['setSiteId', '5']);
let d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
g.async = true;
g.src = '/data.js';
s.parentNode.insertBefore(g, s);
})();
</script>
<?php endif; ?>

View File

@@ -0,0 +1,49 @@
<header>
<a href="<?=htmlspecialchars(\Aternos\Mclogs\Util\URL::getBase()->toString()); ?>" class="logo">
<svg class="logo-icon" width="41" height="42" viewBox="0 0 41 42" fill="none"
xmlns="http://www.w3.org/2000/svg">
<rect width="41" height="5" rx="2" fill="currentColor"/>
<rect y="9.25" width="33" height="5" rx="2" fill="currentColor"/>
<rect y="18.5" width="19" height="5" rx="2" fill="currentColor"/>
<rect y="27.75" width="33" height="5" rx="2" fill="currentColor"/>
<rect y="37" width="41" height="5" rx="2" fill="currentColor"/>
</svg>
<span class="logo-text"><?= htmlspecialchars(\Aternos\Mclogs\Config\Config::getInstance()->getName()); ?></span>
</a>
<div class="tagline">
<h1 class="tagline-main"><span class="title-verb">Paste</span> your logs.</h1>
<div class="tagline-sub">Built for Minecraft & Hytale</div>
</div>
<script>
const titles = ["Paste", "Share", "Analyse"];
let currentTitle = 0;
let speed = 30;
let pause = 3000;
const titleElement = document.querySelector('.title-verb');
setTimeout(nextTitle, pause);
function nextTitle() {
currentTitle++;
if (typeof (titles[currentTitle]) === "undefined") {
currentTitle = 0;
}
const title = titleElement.innerHTML;
for (let i = 0; i < title.length - 1; i++) {
setTimeout(function () {
titleElement.innerHTML = titleElement.innerHTML.substring(0, titleElement.innerHTML.length - 1);
}, i * speed);
}
const newTitle = titles[currentTitle];
for (let i = 1; i <= newTitle.length; i++) {
setTimeout(function () {
titleElement.innerHTML = newTitle.substring(0, titleElement.innerHTML.length + 1);
}, title.length * speed + i * speed);
}
setTimeout(nextTitle, title.length * speed + newTitle.length * speed + pause);
}
</script>
</header>

37
web/frontend/start.php Normal file
View File

@@ -0,0 +1,37 @@
<?php
use Aternos\Mclogs\Config\Config;
use Aternos\Mclogs\Filter\Filter;
use Aternos\Mclogs\Frontend\Assets\AssetLoader;
use Aternos\Mclogs\Frontend\Assets\AssetType;
?><!DOCTYPE html>
<html lang="en">
<head>
<?php include __DIR__ . '/parts/head.php'; ?>
<title><?= htmlspecialchars(Config::getInstance()->getName()); ?> - Paste, share & analyse your logs</title>
<meta name="description" content="Easily paste your Minecraft & Hytale logs to share and analyse them." />
</head>
<body data-name="<?=htmlspecialchars(Config::getInstance()->getName()); ?>">
<?php include __DIR__ . '/parts/header.php'; ?>
<main>
<div class="paste-area" id="dropzone">
<div class="paste-placeholder">
<i class="fa-solid fa-cloud-arrow-up"></i>
<p>Paste or drop your log here</p>
<div class="paste-hints">
<button type="button" class="btn btn-transparent" title="Paste log" id="paste-clipboard"><i class="fa-solid fa-paste"></i> Paste</button>
<button type="button" class="btn btn-transparent" title="Browse on files" id="paste-select-file"><i class="fa-solid fa-folder-open"></i> Browse</button>
<span><i class="fa-solid fa-file-arrow-up" title="Drop file"></i> Drop</span>
</div>
</div>
<textarea aria-label="Paste or drop your log here" spellcheck="false" data-enable-grammarly="false" id="paste-text"></textarea>
<button type="button" class="btn-save btn paste-save" title="Save log" disabled><i class="fa-solid fa-save"></i> Save</button>
<div class="paste-error" id="paste-error"></div>
</div>
</main>
<?php include __DIR__ . '/parts/footer.php'; ?>
<script>
const FILTERS = <?= json_encode(Filter::getAll()); ?>;
</script>
<?= AssetLoader::getInstance()->getHTML(AssetType::JS, "js/start.js"); ?>
</body>
</html>