This commit is contained in:
dgtlmoon
2025-04-29 16:32:32 +02:00
parent f53be7c7fb
commit 63efe8f556
8 changed files with 387 additions and 288 deletions

View File

@@ -102,7 +102,7 @@
{% set is_unviewed = watch.newest_history_key| int > watch.last_viewed and watch.history_n>=2 %}
{% set checking_now = is_checking_now(watch) %}
<tr id="{{ watch.uuid }}" data-watch-uuid="{{ watch.uuid }}"
<tr id="{{ watch.uuid }}" data-watch-uuid="{{ watch.uuid }}" data-history-n="{{ watch.history_n }}"
class="{{ loop.cycle('pure-table-odd', 'pure-table-even') }} processor-{{ watch['processor'] }}
{% if watch.last_error is defined and watch.last_error != False %}error{% endif %}
{% if watch.last_notification_error is defined and watch.last_notification_error != False %}error{% endif %}
@@ -111,6 +111,7 @@
{% if watch.has_restock_info %} has-restock-info {% if watch['restock']['in_stock'] %}in-stock{% else %}not-in-stock{% endif %} {% else %}no-restock-info{% endif %}
{% if watch.uuid in queued_uuids %}queued{% endif %}
{% if checking_now %}checking-now{% endif %}
{% if watch.history_n >=2 %}has-history{% endif %}
">
<td class="inline checkbox-uuid" ><input name="uuids" type="checkbox" value="{{ watch.uuid}} " > <span>{{ loop.index+pagination.skip }}</span></td>
<td class="inline watch-controls">
@@ -194,35 +195,22 @@
{% if checking_now %}
<span class="spinner"></span><span> Checking now</span>
{% else %}
{{watch|format_last_checked_time|safe}}</td>
{% endif %}
<td class="last-changed" data-timestamp="{{ watch.last_changed }}">{% if watch.history_n >=2 and watch.last_changed >0 %}
{{watch.last_changed|format_timestamp_timeago}}
{% else %}
Not yet
<span class="timeago">{{watch|format_last_checked_time|safe}} <!-- default --></span>
{% endif %}
</td>
<td class="last-changed" data-timestamp="{{ watch.last_changed }}">
<span class="timeago">{% if watch.history_n >=2 and watch.last_changed >0 %}{{watch|format_last_checked_time|safe}}{% else %}Not yet{% endif %}<!-- default --></span>
</td>
<td>
<a {% if watch.uuid in queued_uuids %}disabled="true"{% endif %} href="{{ url_for('ui.form_watch_checknow', uuid=watch.uuid, tag=request.args.get('tag')) }}"
class="recheck pure-button pure-button-primary">{% if watch.uuid in queued_uuids %}Queued{% else %}Recheck{% endif %}</a>
<a href="{{ url_for('ui.ui_edit.edit_page', uuid=watch.uuid, tag=active_tag_uuid)}}#general" class="pure-button pure-button-primary">Edit</a>
{% if watch.history_n >= 2 %}
<a href="{{ url_for('ui.form_watch_checknow', uuid=watch.uuid) }}" class="recheck pure-button pure-button-primary">Recheck</a>
<a href="" disabled=disabled class="queue pure-button pure-button-primary" style="display: none;">Queued</a>
<a href="{{ url_for('ui.ui_edit.edit_page', uuid=watch.uuid)}}#general" class="edit pure-button pure-button-primary">Edit</a>
{% set open_diff_in_new_tab = datastore.data['settings']['application']['ui'].get('open_diff_in_new_tab') %}
{% set target_attr = ' target="' ~ watch.uuid ~ '"' if open_diff_in_new_tab else '' %}
{% set open_diff_in_new_tab = datastore.data['settings']['application']['ui'].get('open_diff_in_new_tab') %}
{% set target_attr = ' target="' ~ watch.uuid ~ '"' if open_diff_in_new_tab else '' %}
{% if is_unviewed %}
<a href="{{ url_for('ui.ui_views.diff_history_page', uuid=watch.uuid, from_version=watch.get_from_version_based_on_last_viewed) }}" {{target_attr}} class="pure-button pure-button-primary diff-link">History</a>
{% else %}
<a href="{{ url_for('ui.ui_views.diff_history_page', uuid=watch.uuid)}}" {{target_attr}} class="pure-button pure-button-primary diff-link">History</a>
{% endif %}
{% else %}
{% if watch.history_n == 1 or (watch.history_n ==0 and watch.error_text_ctime )%}
<a href="{{ url_for('ui.ui_views.preview_page', uuid=watch.uuid)}}" {{target_attr}} class="pure-button pure-button-primary">Preview</a>
{% endif %}
{% endif %}
<a href="{{ url_for('ui.ui_views.diff_history_page', uuid=watch.uuid)}}" {{target_attr}} class="history pure-button pure-button-primary diff-link" >History</a>
<a href="{{ url_for('ui.ui_views.preview_page', uuid=watch.uuid)}}" {{target_attr}} class="preview pure-button pure-button-primary" style="display: none;">Preview</a>
</td>
</tr>
{% endfor %}

View File

@@ -67,7 +67,7 @@ class ChangeDetectionSocketIO:
def background_task(self):
"""Background task that emits watch status periodically"""
check_interval = 3 # seconds between updates
check_interval = 4 # seconds between updates
try:
with self.main_app.app_context():
@@ -93,10 +93,10 @@ class ChangeDetectionSocketIO:
'uuid': uuid,
'url': watch.get('url', ''),
'title': watch.get('title', ''),
'last_checked': watch.get('last_checked', 0),
'last_changed': watch.get('newest_history_key', 0),
'last_checked': int(watch.get('last_checked', 0)),
'last_changed': int(watch.get('newest_history_key', 0)),
'history_n': watch.history_n if hasattr(watch, 'history_n') else 0,
'viewed': watch.get('viewed', True),
'unviewed_history': int(watch.get('newest_history_key', 0)) > int(watch.get('last_viewed', 0)) and watch.history_n >=2,
'paused': watch.get('paused', False),
'checking': uuid in currently_checking
}

View File

@@ -35,19 +35,66 @@ $(document).ready(function() {
function updateWatchRow($row, data) {
// Update the last-checked time
const $lastChecked = $row.find('.last-checked');
if ($lastChecked.length && data.last_checked) {
// Format as timeago if we have the timeago library available
if (typeof timeago !== 'undefined') {
$lastChecked.text(timeago.format(data.last_checked, Date.now()/1000));
if ($lastChecked.length) {
// Update data-timestamp attribute
$lastChecked.attr('data-timestamp', data.last_checked);
// Only show timeago if not currently checking
if (!data.checking) {
let $timeagoSpan = $lastChecked.find('.timeago');
// If there's no timeago span yet, create one
if (!$timeagoSpan.length) {
$lastChecked.html('<span class="timeago"></span>');
$timeagoSpan = $lastChecked.find('.timeago');
}
if (data.last_checked > 0) {
// Format as timeago if we have the timeago library available
if (typeof timeago !== 'undefined') {
$timeagoSpan.text(timeago.format(data.last_checked * 1000));
} else {
// Simple fallback if timeago isn't available
const date = new Date(data.last_checked * 1000);
$timeagoSpan.text(date.toLocaleString());
}
} else {
$lastChecked.text('Not yet');
}
}
}
// Update the last-changed time
const $lastChanged = $row.find('.last-changed');
if ($lastChanged.length && data.last_changed) {
// Update data-timestamp attribute
$lastChanged.attr('data-timestamp', data.last_changed);
// Only update the text if we have history
if (data.history_n >= 2 && data.last_changed > 0) {
let $timeagoSpan = $lastChanged.find('.timeago');
// If there's no timeago span yet, create one
if (!$timeagoSpan.length) {
$lastChanged.html('<span class="timeago"></span>');
$timeagoSpan = $lastChanged.find('.timeago');
}
// Format as timeago
if (typeof timeago !== 'undefined') {
$timeagoSpan.text(timeago.format(data.last_changed * 1000));
} else {
// Simple fallback if timeago isn't available
const date = new Date(data.last_changed * 1000);
$timeagoSpan.text(date.toLocaleString());
}
} else {
// Simple fallback if timeago isn't available
const date = new Date(data.last_checked * 1000);
$lastChecked.text(date.toLocaleString());
$lastChanged.text('Not yet');
}
}
// Toggle the unviewed class based on viewed status
$row.toggleClass('unviewed', data.viewed === false);
$row.toggleClass('unviewed', data.unviewed_history === false);
// If the watch is currently being checked
$row.toggleClass('checking-now', data.checking === true);

View File

@@ -88,7 +88,9 @@
--color-icon-github: var(--color-black);
--color-icon-github-hover: var(--color-grey-300);
--color-watch-table-error: var(--color-dark-red);
--color-watch-table-row-text: var(--color-grey-100); }
--color-watch-table-row-text: var(--color-grey-100);
--color-change-highlight-rgb: 255, 215, 0;
/* Gold color for socket.io highlight */ }
html[data-darkmode="true"] {
--color-link: #59bdfb;

View File

@@ -0,0 +1,183 @@
/* table related */
.watch-table {
width: 100%;
font-size: 80%;
tr {
&.unviewed {
font-weight: bold;
}
&.has-history {
a.preview {
display: none !important;
}
&.history {
display: inline-block !important;
}
}
&.error {
color: var(--color-watch-table-error);
}
&.queued {
a.queue {
display: inline-block !important;
}
a.recheck {
display: none !important;
}
}
color: var(--color-watch-table-row-text);
}
td {
white-space: nowrap;
&.title-col {
word-break: break-all;
white-space: normal;
}
}
th {
white-space: nowrap;
a {
font-weight: normal;
&.active {
font-weight: bolder;
}
&.inactive {
.arrow {
display: none;
}
}
}
}
.title-col a[target="_blank"]::after,
.current-diff-url::after {
content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAQElEQVR42qXKwQkAIAxDUUdxtO6/RBQkQZvSi8I/pL4BoGw/XPkh4XigPmsUgh0626AjRsgxHTkUThsG2T/sIlzdTsp52kSS1wAAAABJRU5ErkJggg==);
margin: 0 3px 0 5px;
}
}
@media only screen and (max-width: 760px),
(min-device-width: 768px) and (max-device-width: 800px) {
/*
Max width before this PARTICULAR table gets nasty
This query will take effect for any screen smaller than 760px
and also iPads specifically.
*/
.watch-table {
/* make headings work on mobile */
thead {
display: block;
tr {
th {
display: inline-block;
// Hide the "Last" text for smaller screens
@media (max-width: 768px) {
.hide-on-mobile {
display: none;
}
}
}
}
.empty-cell {
display: none;
}
}
/* Force table to not be like tables anymore */
tbody {
td,
tr {
display: block;
}
}
tbody {
tr {
display: flex;
flex-wrap: wrap;
// The third child of each row will take up the remaining space
// This is useful for the URL column, which should expand to fill the remaining space
:nth-child(3) {
flex-grow: 1;
}
// The last three children (from the end) of each row will take up the full width
// This is useful for the "Last Checked", "Last Changed", and the action buttons columns, which should each take up the full width
:nth-last-child(-n+3) {
flex-basis: 100%;
}
}
}
.last-checked {
> span {
vertical-align: middle;
}
}
.last-checked::before {
color: var(--color-last-checked);
content: "Last Checked ";
}
.last-changed::before {
color: var(--color-last-checked);
content: "Last Changed ";
}
/* Force table to not be like tables anymore */
td.inline {
display: inline-block;
}
.pure-table td,
.pure-table th {
border: none;
}
td {
/* Behave like a "row" */
border: none;
border-bottom: 1px solid var(--color-border-watch-table-cell);
vertical-align: middle;
&:before {
/* Top/left values mimic padding */
top: 6px;
left: 6px;
width: 45%;
padding-right: 10px;
white-space: nowrap;
}
}
&.pure-table-striped {
tr {
background-color: var(--color-table-background);
}
tr:nth-child(2n-1) {
background-color: var(--color-table-stripe);
}
tr:nth-child(2n-1) td {
background-color: inherit;
}
}
}
}

View File

@@ -6,6 +6,7 @@
@import "parts/_browser-steps";
@import "parts/_extra_proxies";
@import "parts/_extra_browsers";
@import "parts/_watch_table";
@import "parts/_pagination";
@import "parts/_spinners";
@import "parts/_variables";
@@ -169,56 +170,6 @@ code {
color: var(--color-text);
}
/* table related */
.watch-table {
width: 100%;
font-size: 80%;
tr {
&.unviewed {
font-weight: bold;
}
&.error {
color: var(--color-watch-table-error);
}
color: var(--color-watch-table-row-text);
}
td {
white-space: nowrap;
&.title-col {
word-break: break-all;
white-space: normal;
}
}
th {
white-space: nowrap;
a {
font-weight: normal;
&.active {
font-weight: bolder;
}
&.inactive {
.arrow {
display: none;
}
}
}
}
.title-col a[target="_blank"]::after,
.current-diff-url::after {
content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAQElEQVR42qXKwQkAIAxDUUdxtO6/RBQkQZvSi8I/pL4BoGw/XPkh4XigPmsUgh0626AjRsgxHTkUThsG2T/sIlzdTsp52kSS1wAAAABJRU5ErkJggg==);
margin: 0 3px 0 5px;
}
}
.inline-tag {
white-space: nowrap;
border-radius: 5px;
@@ -706,115 +657,6 @@ footer {
input[type='text'] {
width: 100%;
}
/*
Max width before this PARTICULAR table gets nasty
This query will take effect for any screen smaller than 760px
and also iPads specifically.
*/
.watch-table {
/* make headings work on mobile */
thead {
display: block;
tr {
th {
display: inline-block;
// Hide the "Last" text for smaller screens
@media (max-width: 768px) {
.hide-on-mobile {
display: none;
}
}
}
}
.empty-cell {
display: none;
}
}
/* Force table to not be like tables anymore */
tbody {
td,
tr {
display: block;
}
}
tbody {
tr {
display: flex;
flex-wrap: wrap;
// The third child of each row will take up the remaining space
// This is useful for the URL column, which should expand to fill the remaining space
:nth-child(3) {
flex-grow: 1;
}
// The last three children (from the end) of each row will take up the full width
// This is useful for the "Last Checked", "Last Changed", and the action buttons columns, which should each take up the full width
:nth-last-child(-n+3) {
flex-basis: 100%;
}
}
}
.last-checked {
>span {
vertical-align: middle;
}
}
.last-checked::before {
color: var(--color-last-checked);
content: "Last Checked ";
}
.last-changed::before {
color: var(--color-last-checked);
content: "Last Changed ";
}
/* Force table to not be like tables anymore */
td.inline {
display: inline-block;
}
.pure-table td,
.pure-table th {
border: none;
}
td {
/* Behave like a "row" */
border: none;
border-bottom: 1px solid var(--color-border-watch-table-cell);
vertical-align: middle;
&:before {
/* Top/left values mimic padding */
top: 6px;
left: 6px;
width: 45%;
padding-right: 10px;
white-space: nowrap;
}
}
&.pure-table-striped {
tr {
background-color: var(--color-table-background);
}
tr:nth-child(2n-1) {
background-color: var(--color-table-stripe);
}
tr:nth-child(2n-1) td {
background-color: inherit;
}
}
}
}
.pure-table {

View File

@@ -184,6 +184,105 @@ ul#requests-extra_browsers {
margin: 1em;
padding: 1em; }
/* table related */
.watch-table {
width: 100%;
font-size: 80%; }
.watch-table tr {
color: var(--color-watch-table-row-text); }
.watch-table tr.unviewed {
font-weight: bold; }
.watch-table tr.has-history a.preview {
display: none !important; }
.watch-table tr.has-history.history {
display: inline-block !important; }
.watch-table tr.error {
color: var(--color-watch-table-error); }
.watch-table tr.queued a.queue {
display: inline-block !important; }
.watch-table tr.queued a.recheck {
display: none !important; }
.watch-table td {
white-space: nowrap; }
.watch-table td.title-col {
word-break: break-all;
white-space: normal; }
.watch-table th {
white-space: nowrap; }
.watch-table th a {
font-weight: normal; }
.watch-table th a.active {
font-weight: bolder; }
.watch-table th a.inactive .arrow {
display: none; }
.watch-table .title-col a[target="_blank"]::after,
.watch-table .current-diff-url::after {
content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAQElEQVR42qXKwQkAIAxDUUdxtO6/RBQkQZvSi8I/pL4BoGw/XPkh4XigPmsUgh0626AjRsgxHTkUThsG2T/sIlzdTsp52kSS1wAAAABJRU5ErkJggg==);
margin: 0 3px 0 5px; }
@media only screen and (max-width: 760px), (min-device-width: 768px) and (max-device-width: 800px) {
/*
Max width before this PARTICULAR table gets nasty
This query will take effect for any screen smaller than 760px
and also iPads specifically.
*/
.watch-table {
/* make headings work on mobile */
/* Force table to not be like tables anymore */
/* Force table to not be like tables anymore */ }
.watch-table thead {
display: block; }
.watch-table thead tr th {
display: inline-block; } }
@media only screen and (max-width: 760px) and (max-width: 768px), (min-device-width: 768px) and (max-device-width: 800px) and (max-width: 768px) {
.watch-table thead tr th .hide-on-mobile {
display: none; } }
@media only screen and (max-width: 760px), (min-device-width: 768px) and (max-device-width: 800px) {
.watch-table thead .empty-cell {
display: none; }
.watch-table tbody td,
.watch-table tbody tr {
display: block; }
.watch-table tbody tr {
display: flex;
flex-wrap: wrap; }
.watch-table tbody tr :nth-child(3) {
flex-grow: 1; }
.watch-table tbody tr :nth-last-child(-n+3) {
flex-basis: 100%; }
.watch-table .last-checked > span {
vertical-align: middle; }
.watch-table .last-checked::before {
color: var(--color-last-checked);
content: "Last Checked "; }
.watch-table .last-changed::before {
color: var(--color-last-checked);
content: "Last Changed "; }
.watch-table td.inline {
display: inline-block; }
.watch-table .pure-table td,
.watch-table .pure-table th {
border: none; }
.watch-table td {
/* Behave like a "row" */
border: none;
border-bottom: 1px solid var(--color-border-watch-table-cell);
vertical-align: middle; }
.watch-table td:before {
/* Top/left values mimic padding */
top: 6px;
left: 6px;
width: 45%;
padding-right: 10px;
white-space: nowrap; }
.watch-table.pure-table-striped tr {
background-color: var(--color-table-background); }
.watch-table.pure-table-striped tr:nth-child(2n-1) {
background-color: var(--color-table-stripe); }
.watch-table.pure-table-striped tr:nth-child(2n-1) td {
background-color: inherit; } }
.pagination-page-info {
color: #fff;
font-size: 0.85rem;
@@ -339,7 +438,9 @@ ul#requests-extra_browsers {
--color-icon-github: var(--color-black);
--color-icon-github-hover: var(--color-grey-300);
--color-watch-table-error: var(--color-dark-red);
--color-watch-table-row-text: var(--color-grey-100); }
--color-watch-table-row-text: var(--color-grey-100);
--color-change-highlight-rgb: 255, 215, 0;
/* Gold color for socket.io highlight */ }
html[data-darkmode="true"] {
--color-link: #59bdfb;
@@ -735,34 +836,6 @@ code {
background: var(--color-background-code);
color: var(--color-text); }
/* table related */
.watch-table {
width: 100%;
font-size: 80%; }
.watch-table tr {
color: var(--color-watch-table-row-text); }
.watch-table tr.unviewed {
font-weight: bold; }
.watch-table tr.error {
color: var(--color-watch-table-error); }
.watch-table td {
white-space: nowrap; }
.watch-table td.title-col {
word-break: break-all;
white-space: normal; }
.watch-table th {
white-space: nowrap; }
.watch-table th a {
font-weight: normal; }
.watch-table th a.active {
font-weight: bolder; }
.watch-table th a.inactive .arrow {
display: none; }
.watch-table .title-col a[target="_blank"]::after,
.watch-table .current-diff-url::after {
content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAQElEQVR42qXKwQkAIAxDUUdxtO6/RBQkQZvSi8I/pL4BoGw/XPkh4XigPmsUgh0626AjRsgxHTkUThsG2T/sIlzdTsp52kSS1wAAAABJRU5ErkJggg==);
margin: 0 3px 0 5px; }
.inline-tag, .watch-tag-list, .tracking-ldjson-price-data, .restock-label {
white-space: nowrap;
border-radius: 5px;
@@ -1097,68 +1170,7 @@ footer {
border-radius: 0px;
margin-right: 0px; }
input[type='text'] {
width: 100%; }
/*
Max width before this PARTICULAR table gets nasty
This query will take effect for any screen smaller than 760px
and also iPads specifically.
*/
.watch-table {
/* make headings work on mobile */
/* Force table to not be like tables anymore */
/* Force table to not be like tables anymore */ }
.watch-table thead {
display: block; }
.watch-table thead tr th {
display: inline-block; } }
@media only screen and (max-width: 760px) and (max-width: 768px), (min-device-width: 768px) and (max-device-width: 800px) and (max-width: 768px) {
.watch-table thead tr th .hide-on-mobile {
display: none; } }
@media only screen and (max-width: 760px), (min-device-width: 768px) and (max-device-width: 800px) {
.watch-table thead .empty-cell {
display: none; }
.watch-table tbody td,
.watch-table tbody tr {
display: block; }
.watch-table tbody tr {
display: flex;
flex-wrap: wrap; }
.watch-table tbody tr :nth-child(3) {
flex-grow: 1; }
.watch-table tbody tr :nth-last-child(-n+3) {
flex-basis: 100%; }
.watch-table .last-checked > span {
vertical-align: middle; }
.watch-table .last-checked::before {
color: var(--color-last-checked);
content: "Last Checked "; }
.watch-table .last-changed::before {
color: var(--color-last-checked);
content: "Last Changed "; }
.watch-table td.inline {
display: inline-block; }
.watch-table .pure-table td,
.watch-table .pure-table th {
border: none; }
.watch-table td {
/* Behave like a "row" */
border: none;
border-bottom: 1px solid var(--color-border-watch-table-cell);
vertical-align: middle; }
.watch-table td:before {
/* Top/left values mimic padding */
top: 6px;
left: 6px;
width: 45%;
padding-right: 10px;
white-space: nowrap; }
.watch-table.pure-table-striped tr {
background-color: var(--color-table-background); }
.watch-table.pure-table-striped tr:nth-child(2n-1) {
background-color: var(--color-table-stripe); }
.watch-table.pure-table-striped tr:nth-child(2n-1) td {
background-color: inherit; } }
width: 100%; } }
.pure-table {
border-color: var(--color-border-table-cell); }
@@ -1360,6 +1372,29 @@ ul {
#selector-current-xpath {
font-size: 80%; }
@keyframes socket-highlight-flash {
0% {
background-color: rgba(var(--color-change-highlight-rgb), 0); }
50% {
background-color: rgba(var(--color-change-highlight-rgb), 0.4); }
100% {
background-color: rgba(var(--color-change-highlight-rgb), 0); } }
.socket-highlight {
animation: socket-highlight-flash 2s ease-in-out; }
@keyframes checking-progress {
0% {
background-size: 0% 100%; }
100% {
background-size: 100% 100%; } }
tr.checking-now .last-checked {
background-image: linear-gradient(to right, rgba(0, 120, 255, 0.2) 0%, rgba(0, 120, 255, 0.1) 100%);
background-size: 100% 100%;
background-repeat: no-repeat;
animation: checking-progress 10s linear; }
#webdriver_delay {
width: 5em; }

View File

@@ -32,7 +32,9 @@
<script src="{{url_for('static_content', group='js', filename='jquery-3.6.0.min.js')}}"></script>
<script src="{{url_for('static_content', group='js', filename='csrf.js')}}" defer></script>
<script src="https://cdn.socket.io/4.6.0/socket.io.min.js" integrity="sha384-c79GN5VsunZvi+Q/WObgk2in0CbZsHnjEqvFxC5DxHn9lTfNce2WW6h2pH6u/kF+" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/timeago.js/4.0.2/timeago.min.js" integrity="sha512-SVDh1zH5N9ChofSlNAK43lcNS7lWze6DTVx1JCXH1Tmno+0/1jMpdbR8YDgDUfcUrPp1xyE53G42GFrcM0CMVg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="{{url_for('static_content', group='js', filename='socket.js')}}" defer></script>
<script src="{{url_for('static_content', group='js', filename='timeago-init.js')}}" defer></script>
</head>
<body class="">