adds slightly less-ugly global stylesheet; improves mobile compat

This commit is contained in:
nai-degen
2024-05-21 12:56:25 -05:00
parent 1b68ad7c6f
commit 3012aa651e
11 changed files with 936 additions and 120 deletions
+349
View File
@@ -0,0 +1,349 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}
+231
View File
@@ -0,0 +1,231 @@
/* modified https://github.com/oxalorg/sakura */
html {
font-size: 62.5%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, "Noto Sans", sans-serif;
}
body {
font-size: 1.8rem;
line-height: 1.618;
max-width: 38em;
margin: auto;
color: #c9c9c9;
background-color: #222222;
padding: 13px;
}
@media (max-width: 684px) {
body {
font-size: 1.53rem;
}
}
@media (max-width: 382px) {
body {
font-size: 1.35rem;
}
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, "Noto Sans", sans-serif;
font-weight: 700;
margin-top: 3rem;
margin-bottom: 1.5rem;
overflow-wrap: break-word;
word-wrap: break-word;
-ms-word-break: break-all;
word-break: break-word;
}
h1 {
font-size: 2.35em;
}
h2 {
font-size: 2em;
}
h3 {
font-size: 1.75em;
}
h4 {
font-size: 1.5em;
}
h5 {
font-size: 1.25em;
}
h6 {
font-size: 1em;
}
p {
margin-top: 0px;
margin-bottom: 2.5rem;
}
small,
sub,
sup {
font-size: 75%;
}
hr {
border-color: #ffffff;
}
a {
text-decoration: none;
color: #ffffff;
}
a:visited {
color: #e6e6e6;
}
a:hover {
color: #c9c9c9;
text-decoration: underline;
}
ul {
padding-left: 1.4em;
margin-top: 0px;
margin-bottom: 2.5rem;
}
li {
margin-bottom: 0.4em;
}
blockquote {
margin-left: 0px;
margin-right: 0px;
padding-left: 1em;
padding-top: 0.8em;
padding-bottom: 0.8em;
padding-right: 0.8em;
border-left: 5px solid #ffffff;
margin-bottom: 2.5rem;
background-color: #4a4a4a;
}
blockquote p {
margin-bottom: 0;
}
img,
video {
height: auto;
max-width: 100%;
margin-top: 0px;
margin-bottom: 2.5rem;
}
pre {
background-color: #4a4a4a;
display: block;
padding: 1em;
overflow-x: auto;
margin-top: 0px;
margin-bottom: 2.5rem;
font-size: 0.9em;
}
code,
kbd,
samp {
font-size: 0.9em;
padding: 0 0.5em;
background-color: #4a4a4a;
white-space: pre-wrap;
}
pre > code {
padding: 0;
background-color: transparent;
white-space: pre;
font-size: 1em;
}
table {
text-align: justify;
width: 100%;
border-collapse: collapse;
margin-bottom: 2rem;
}
td,
th {
padding: 0.5em;
border-bottom: 1px solid #4a4a4a;
}
input,
textarea {
border: 1px solid #c9c9c9;
}
input:focus,
textarea:focus {
border: 1px solid #ffffff;
}
textarea {
width: 100%;
}
.button,
button,
input[type="submit"],
input[type="reset"],
input[type="button"],
input[type="file"]::file-selector-button {
display: inline-block;
padding: 5px 10px;
text-align: center;
text-decoration: none;
white-space: nowrap;
background-color: #ffffff;
color: #222222;
border-radius: 1px;
border: 1px solid #ffffff;
cursor: pointer;
box-sizing: border-box;
}
.button[disabled],
button[disabled],
input[type="submit"][disabled],
input[type="reset"][disabled],
input[type="button"][disabled],
input[type="file"][disabled] {
cursor: default;
opacity: 0.5;
}
.button:hover,
button:hover,
input[type="submit"]:hover,
input[type="reset"]:hover,
input[type="button"]:hover,
input[type="file"]::file-selector-button:hover {
background-color: #c9c9c9;
color: #222222;
outline: 0;
}
.button:focus-visible,
button:focus-visible,
input[type="submit"]:focus-visible,
input[type="reset"]:focus-visible,
input[type="button"]:focus-visible,
input[type="file"]::file-selector-button:focus-visible {
outline-style: solid;
outline-width: 2px;
}
textarea,
select,
input {
color: #c9c9c9;
padding: 6px 10px;
margin-bottom: 10px;
background-color: #4a4a4a;
border: 1px solid #4a4a4a;
border-radius: 4px;
box-shadow: none;
box-sizing: border-box;
}
textarea:focus,
select:focus,
input:focus {
border: 1px solid #ffffff;
outline: 0;
}
input[type="checkbox"]:focus {
outline: 1px dotted #ffffff;
}
label,
legend,
fieldset {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
}
+237
View File
@@ -0,0 +1,237 @@
/* modified https://github.com/oxalorg/sakura */
:root {
--accent-color: #4a4a4a;
--accent-color-hover: #5a5a5a;
--link-color: #58739c;
--link-visted-color: #6f5e6f;
}
html {
font-size: 62.5%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, "Noto Sans", sans-serif;
}
body {
font-size: 1.8rem;
line-height: 1.618;
max-width: 38em;
margin: auto;
color: #4a4a4a;
background-color: #f9f9f9;
padding: 13px;
}
@media (max-width: 684px) {
body {
font-size: 1.53rem;
}
}
@media (max-width: 382px) {
body {
font-size: 1.35rem;
}
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, "Noto Sans", sans-serif;
font-weight: 700;
margin-top: 3rem;
margin-bottom: 1.5rem;
overflow-wrap: break-word;
word-wrap: break-word;
-ms-word-break: break-all;
word-break: break-word;
}
h1 {
font-size: 2.35em;
}
h2 {
font-size: 2em;
}
h3 {
font-size: 1.75em;
}
h4 {
font-size: 1.5em;
}
h5 {
font-size: 1.25em;
}
h6 {
font-size: 1em;
}
p {
margin-top: 0;
margin-bottom: 2.5rem;
}
small,
sub,
sup {
font-size: 75%;
}
hr {
border-color: var(--accent-color);
}
a {
text-decoration: none;
color: var(--link-color);
}
a:visited {
color: var(--link-visted-color);
}
a:hover {
color: var(--accent-color-hover);
text-decoration: underline;
}
ul {
padding-left: 1.4em;
margin-top: 0;
margin-bottom: 2.5rem;
}
li {
margin-bottom: 0.4em;
}
blockquote {
margin-left: 0;
margin-right: 0;
padding-left: 1em;
padding-top: 0.8em;
padding-bottom: 0.8em;
padding-right: 0.8em;
border-left: 5px solid var(--accent-color);
margin-bottom: 2.5rem;
background-color: #f1f1f1;
}
blockquote p {
margin-bottom: 0;
}
img,
video {
height: auto;
max-width: 100%;
margin-top: 0;
margin-bottom: 2.5rem;
}
pre {
background-color: #f1f1f1;
display: block;
padding: 1em;
overflow-x: auto;
margin-top: 0;
margin-bottom: 2.5rem;
font-size: 0.9em;
}
code,
kbd,
samp {
font-size: 0.9em;
padding: 0 0.5em;
background-color: #f1f1f1;
white-space: pre-wrap;
}
pre > code {
padding: 0;
background-color: transparent;
white-space: pre;
font-size: 1em;
}
table {
text-align: justify;
width: 100%;
border-collapse: collapse;
margin-bottom: 2rem;
}
td,
th {
padding: 0.5em;
border-bottom: 1px solid #f1f1f1;
}
input,
textarea {
border: 1px solid #4a4a4a;
}
input:focus,
textarea:focus {
border: 1px solid var(--accent-color);
}
textarea {
width: 100%;
}
.button,
button,
input[type="submit"],
input[type="reset"],
input[type="button"],
input[type="file"]::file-selector-button {
display: inline-block;
padding: 5px 10px;
text-align: center;
text-decoration: none;
white-space: nowrap;
background-color: var(--accent-color);
color: #f9f9f9;
border-radius: 2px;
border: 1px solid var(--accent-color);
cursor: pointer;
box-sizing: border-box;
}
.button[disabled],
button[disabled],
input[type="submit"][disabled],
input[type="reset"][disabled],
input[type="button"][disabled],
input[type="file"][disabled] {
cursor: default;
opacity: 0.5;
}
.button:hover,
button:hover,
input[type="submit"]:hover,
input[type="reset"]:hover,
input[type="button"]:hover,
input[type="file"]::file-selector-button:hover {
background-color: var(--accent-color-hover);
color: #f9f9f9;
outline: 0;
}
.button:focus-visible,
button:focus-visible,
input[type="submit"]:focus-visible,
input[type="reset"]:focus-visible,
input[type="button"]:focus-visible,
input[type="file"]::file-selector-button:focus-visible {
outline-style: solid;
outline-width: 2px;
}
textarea,
select,
input {
color: #4a4a4a;
padding: 6px 10px;
margin-bottom: 10px;
background-color: #f1f1f1;
border: 1px solid #f1f1f1;
border-radius: 4px;
box-shadow: none;
box-sizing: border-box;
}
textarea:focus,
select:focus,
input:focus {
border: 1px solid var(--accent-color);
outline: 0;
}
input[type="checkbox"]:focus {
outline: 1px dotted var(--accent-color);
}
label,
legend,
fieldset {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
}
+27 -36
View File
@@ -5,18 +5,6 @@
flex-direction: column; flex-direction: column;
} }
#statsForm div {
display: flex;
flex-direction: row;
margin-bottom: 0.5em;
}
#statsForm div label {
width: 6em;
text-align: right;
margin-right: 1em;
}
#statsForm ul { #statsForm ul {
margin: 0; margin: 0;
padding-left: 2em; padding-left: 2em;
@@ -33,17 +21,17 @@
} }
</style> </style>
<h1>Download Stats</h1> <h1>Download Stats</h1>
<p> <p>Download usage statistics to a Markdown document. You can paste this into a service like Rentry.org to share it.</p>
Download usage statistics to a Markdown document. You can paste this into a service like Rentry.org to share it.
</p>
<div> <div>
<h3>Options</h3> <h3>Options</h3>
<form id="statsForm" action="/admin/manage/generate-stats" method="post" <form
style="display: flex; flex-direction: column;"> id="statsForm"
action="/admin/manage/generate-stats"
method="post"
style="display: flex; flex-direction: column">
<input id="_csrf" type="hidden" name="_csrf" value="<%= csrfToken %>" /> <input id="_csrf" type="hidden" name="_csrf" value="<%= csrfToken %>" />
<div> <div>
<label for="anon">Anonymize</label> <label for="anon"><input id="anon" type="checkbox" name="anon" value="true" /> <span>Anonymize</span></label>
<input id="anon" type="checkbox" name="anon" value="true" />
</div> </div>
<div> <div>
<label for="sort">Sort</label> <label for="sort">Sort</label>
@@ -64,11 +52,12 @@
</select> </select>
</div> </div>
<div> <div>
<label for="format">Custom Format <ul> <label for="format">Custom Format</label>
<li><code>{{header}}</code></li> <ul>
<li><code>{{stats}}</code></li> <li><code>{{header}}</code></li>
<li><code>{{time}}</code></li> <li><code>{{stats}}</code></li>
</ul></label> <li><code>{{time}}</code></li>
</ul>
<textarea id="format" name="format" rows="10" cols="50" placeholder="{{stats}}"> <textarea id="format" name="format" rows="10" cols="50" placeholder="{{stats}}">
# Stats # Stats
{{header}} {{header}}
@@ -115,33 +104,35 @@
loadDefaults(); loadDefaults();
async function fetchAndCopy() { async function fetchAndCopy() {
const form = document.getElementById('statsForm'); const form = document.getElementById("statsForm");
const formData = new FormData(form); const formData = new FormData(form);
const response = await fetch(form.action, { const response = await fetch(form.action, {
method: 'POST', method: "POST",
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { "Content-Type": "application/x-www-form-urlencoded" },
credentials: 'same-origin', credentials: "same-origin",
body: new URLSearchParams(formData), body: new URLSearchParams(formData),
}); });
if (response.ok) { if (response.ok) {
const content = await response.text(); const content = await response.text();
copyToClipboard(content); copyToClipboard(content);
} else { } else {
throw new Error('Failed to fetch generated stats. Try reloading the page.'); throw new Error("Failed to fetch generated stats. Try reloading the page.");
} }
} }
function copyToClipboard(text) { function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => { navigator.clipboard
alert('Copied to clipboard'); .writeText(text)
}).catch(err => { .then(() => {
alert('Failed to copy to clipboard. Try downloading the file instead.'); alert("Copied to clipboard");
}); })
.catch((err) => {
alert("Failed to copy to clipboard. Try downloading the file instead.");
});
} }
document.getElementById('copyButton').addEventListener('click', fetchAndCopy); document.getElementById("copyButton").addEventListener("click", fetchAndCopy);
</script> </script>
<%- include("partials/admin-footer") %> <%- include("partials/admin-footer") %>
+2 -3
View File
@@ -4,9 +4,8 @@
<% if (users.length === 0) { %> <% if (users.length === 0) { %>
<p>No users found.</p> <p>No users found.</p>
<% } else { %> <% } else { %>
<input type="checkbox" id="toggle-nicknames" onchange="toggleNicknames()" /> <label for="toggle-nicknames"><input type="checkbox" id="toggle-nicknames" onchange="toggleNicknames()" /> Show Nicknames</label>
<label for="toggle-nicknames">Show Nicknames</label> <table class="striped full-width">
<table class="striped" style="width: calc(100vw - 3em)">
<thead> <thead>
<tr> <tr>
<th>User</th> <th>User</th>
+18 -12
View File
@@ -63,27 +63,33 @@ export function renderPage(info: ServiceInfo) {
const title = getServerTitle(); const title = getServerTitle();
const headerHtml = buildInfoPageHeader(info); const headerHtml = buildInfoPageHeader(info);
return `<!DOCTYPE html> return `<!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="robots" content="noindex" /> <meta name="robots" content="noindex" />
<title>${title}</title> <title>${title}</title>
<link rel="stylesheet" href="/res/css/reset.css" media="screen" />
<link rel="stylesheet" href="/res/css/sakura.css" media="screen" />
<link rel="stylesheet" href="/res/css/sakura-dark.css" media="screen and (prefers-color-scheme: dark)" />
<style> <style>
body { body {
font-family: sans-serif; font-family: sans-serif;
background-color: #f0f0f0;
padding: 1em; padding: 1em;
max-width: 900px;
margin: 0;
} }
@media (prefers-color-scheme: dark) {
body {
background-color: #222;
color: #eee;
}
a:link, a:visited { .self-service-links {
color: #bbe; display: flex;
} justify-content: center;
margin-bottom: 1em;
padding: 0.5em;
font-size: 0.8em;
}
.self-service-links a {
margin: 0 0.5em;
} }
</style> </style>
</head> </head>
@@ -152,9 +158,9 @@ function getSelfServiceLinks() {
links.unshift(["Request a user token", "/user/captcha"]); links.unshift(["Request a user token", "/user/captcha"]);
} }
return `<div style="font-size: 0.8em;">${links return `<div class="self-service-links"">${links
.map(([text, link]) => `<a target="_blank" href="${link}">${text}</a>`) .map(([text, link]) => `<a target="_blank" href="${link}">${text}</a>`)
.join(" / ")}</div><hr />`; .join(" | ")}</div>`;
} }
function getServerTitle() { function getServerTitle() {
+1 -1
View File
@@ -70,7 +70,7 @@ app.set("views", [
app.use("/user_content", express.static(USER_ASSETS_DIR, { maxAge: "2h" })); app.use("/user_content", express.static(USER_ASSETS_DIR, { maxAge: "2h" }));
app.use( app.use(
"/res", "/res",
express.static(path.join(__dirname, "..", "public"), { etag: true }) express.static(path.join(__dirname, "..", "public"), { maxAge: "2h", etag: false })
); );
app.get("/health", (_req, res) => res.sendStatus(200)); app.get("/health", (_req, res) => res.sendStatus(200));
+29 -38
View File
@@ -1,23 +1,26 @@
<!doctype html> <!doctype html>
<html> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="csrf-token" content="<%= csrfToken %>" /> <meta name="csrf-token" content="<%= csrfToken %>" />
<title><%= title %></title> <title><%= title %></title>
<link rel="stylesheet" href="/res/css/reset.css" media="screen" />
<link rel="stylesheet" href="/res/css/sakura.css" media="screen" />
<link rel="stylesheet" href="/res/css/sakura-dark.css" media="screen and (prefers-color-scheme: dark)" />
<style> <style>
body { body {
font-family: sans-serif; font-family: sans-serif;
background-color: #f0f0f0;
padding: 1em; padding: 1em;
max-width: 800px; max-width: 800px;
} }
a:hover { details summary {
background-color: #e0e6f6; cursor: pointer;
font-weight: bold;
} }
details > *:not(summary) {
a:visited:hover { margin: 0.5em;
background-color: #e7e0f6;
} }
.pagination { .pagination {
@@ -30,6 +33,7 @@
.pagination li a { .pagination li a {
display: block; display: block;
padding: 0.5em 1em; padding: 0.5em 1em;
border-bottom: none;
text-decoration: none; text-decoration: none;
} }
.pagination li.active a { .pagination li.active a {
@@ -37,18 +41,18 @@
color: #fff; color: #fff;
} }
table {
border-collapse: collapse;
border: 1px solid #ccc;
}
table.striped tr:nth-child(even) { table.striped tr:nth-child(even) {
background-color: #eaeaea; background-color: #eaeaea;
} }
table td, table.full-width {
table th { width: calc(100vw - 4em);
border: 1px solid #ccc; position: relative;
padding: 0.25em 0.5em; left: 50%;
right: 50%;
margin-left: calc(-50vw + 2em);
margin-right: calc(-50vw + 2em);
} }
th.active { th.active {
background-color: #e0e6f6; background-color: #e0e6f6;
} }
@@ -73,32 +77,17 @@
padding: 0.5em; padding: 0.5em;
} }
table { table.full-width {
width: 100%; width: 100%;
} position: static;
table td, left: auto;
table th { right: auto;
display: block; margin-left: 0;
width: 100%; margin-right: 0;
} }
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
body {
background-color: #222;
color: #eee;
}
a:link,
a:visited {
color: #bbe;
}
a:link:hover,
a:visited:hover {
background-color: #446;
}
table.striped tr:nth-child(even) { table.striped tr:nth-child(even) {
background-color: #333; background-color: #333;
} }
@@ -111,3 +100,5 @@
</head> </head>
<body> <body>
<%- include("partials/shared_flash", { flashData: flash }) %> <%- include("partials/shared_flash", { flashData: flash }) %>
</body>
</html>
@@ -12,7 +12,7 @@
<script> <script>
function getPageSize() { function getPageSize() {
var match = window.location.search.match(/perPage=(\d+)/); const match = window.location.search.match(/perPage=(\d+)/);
if (match) return parseInt(match[1]); else return document.cookie.match(/perPage=(\d+)/)?.[1] ?? 10; if (match) return parseInt(match[1]); else return document.cookie.match(/perPage=(\d+)/)?.[1] ?? 10;
} }
function setPageSize(size) { function setPageSize(size) {
@@ -1,5 +1,5 @@
<a href="#" id="ip-list-toggle">Show all (<%- user.ip.length %>)</a> <a href="#" id="ip-list-toggle">Show all (<%- user.ip.length %>)</a>
<ol id="ip-list" style="display: none; padding-left: 1em; margin: 0"> <ol id="ip-list" style="display: none">
<% user.ip.forEach((ip) => { %> <% user.ip.forEach((ip) => { %>
<li><code><%- shouldRedact ? redactIp(ip) : ip %></code></li> <li><code><%- shouldRedact ? redactIp(ip) : ip %></code></li>
<% }) %> <% }) %>
@@ -10,15 +10,11 @@
} }
@media (max-width: 1000px) { @media (max-width: 1000px) {
#captcha-container { #captcha-container {
margin: 20px; max-width: unset;
margin: 30px;
} }
} }
#captcha-container p {
padding: 2px;
line-height: 1.5;
}
#captcha-container details { #captcha-container details {
margin: 10px 0; margin: 10px 0;
} }
@@ -44,6 +40,13 @@
padding: 10px 20px; padding: 10px 20px;
} }
#captcha-progress-text {
width: 100%;
height: 18rem;
resize: vertical;
font-family: monospace;
}
#captcha-progress-container { #captcha-progress-container {
margin: 20px 0; margin: 20px 0;
} }
@@ -66,32 +69,39 @@
width: 0; width: 0;
height: 100%; height: 100%;
background-color: #76c7c0; background-color: #76c7c0;
transition: width 0.2s;
} }
</style> </style>
<div style="display: none" id="captcha-container"> <div style="display: none" id="captcha-container">
<p> <p>
Your device needs to perform a verification task before you can receive access. This might take anywhere from a few Your device needs to perform a verification task before you can receive a token. This might take anywhere from a few
seconds to a few minutes, depending on your device and the proxy's security settings. seconds to a few minutes, depending on your device and the proxy's security settings.
</p> </p>
<p>Click the button below to start.</p> <p>Click the button below to start.</p>
<details> <details>
<summary>What is this?</summary> <summary>What is this?</summary>
<p> <p>
This is an anti-abuse measure designed to slow down automated requests. It requires your device's CPU to find a This is a <a href="https://en.wikipedia.org/wiki/Proof_of_work" target="_blank">proof-of-work</a> verification
solution to a cryptographic puzzle, after which a user token will be issued. task designed to slow down automated abuse. It requires your device's CPU to find a solution to a cryptographic
puzzle, after which a user token will be issued.
</p> </p>
</details> </details>
<details> <details>
<summary>How long does verification take?</summary> <summary>How long does verification take?</summary>
<p> <p>
It on the device you're using and the verification task's difficulty level (currently It depends on the device you're using and the current difficulty level (<code><%= difficultyLevel %></code>). The
<strong><%= difficultyLevel %></strong>). It could take anywhere from a few seconds to a few minutes. faster your device, the quicker it will solve the task.
</p>
<p>
An estimate will be displayed once verification starts. Because the task is probabilistic, your device could solve
it more quickly or take longer than the estimate.
</p> </p>
</details> </details>
<details> <details>
<summary>How often do I need to do this?</summary> <summary>How often do I need to do this?</summary>
<p>Once you've earned a user token, you can use it for <%= tokenLifetime %> hours before it expires.</p> <p>
Once you've earned a user token, you can use it for <strong><%= `${tokenLifetime} hours` %></strong> before it
expires.
</p>
<p> <p>
You can refresh an expired token by returning to this page and verifying again. Subsequent verifications will go You can refresh an expired token by returning to this page and verifying again. Subsequent verifications will go
faster than the first one. faster than the first one.
@@ -100,26 +110,28 @@
<details> <details>
<summary>What is the "Workers" setting?</summary> <summary>What is the "Workers" setting?</summary>
<p> <p>
This controls how many CPU cores will be used to solve the verification task. By default, all of your device's This controls how many CPU cores will be used to solve the verification task. If your device gets too hot or slows
cores will be used to solve the task as quickly as possible. down too much during verification, reduce the number of workers.
</p> </p>
<p> <p>
If your device gets too hot or you want to use it for other tasks while verification is in progress, reduce the For fastest verification, set this to the number of physical CPU cores in your device. Setting more workers than
number of workers to lower the CPU load at the cost of slower verification. you have actual cores will generally only slow down verification.
</p> </p>
<p>If you don't understand what this means, leave it at the default setting.</p>
</details> </details>
<details> <details>
<summary>Other important information</summary> <summary>Other important information</summary>
<ul> <ul>
<li>Verification must be submitted from the same device and IP address that started the verification.</li> <li>Don't change your IP address during verification.</li>
<li>Don't close this tab until verification is complete or you will need to start over.</li> <li>Don't close this tab until verification is complete.</li>
<li>You can pause the task, but verification must be finished <strong>within 30 minutes</strong> of issuance.</li>
<li> <li>
Up to <strong><%= tokenMaxIps || "unlimited" %></strong> IP addresses can be associated with a user token at Verification must be finished within <strong><%= `${challengeTimeout} minutes` %></strong>.
once. </li>
<li>Your user token will be registered to your current IP address.</li>
<li>
Up to <strong><%= tokenMaxIps || "unlimited" %></strong> IP addresses total can be registered to your user
token.
</li> </li>
<li>JavaScript is required to complete verification.</li>
<li>If the proxy is restarted, any verification tasks currently in progress will be invalidated.</li>
</ul> </ul>
</details> </details>
<form id="captcha-form" style="display: none"> <form id="captcha-form" style="display: none">
@@ -129,7 +141,7 @@
<div id="captcha-control"> <div id="captcha-control">
<div> <div>
<label for="workers">Workers:</label> <label for="workers">Workers:</label>
<input type="number" id="workers" value="1" min="1" max="16" onchange="spawnWorkers()" /> <input type="number" id="workers" value="1" min="1" max="32" onchange="spawnWorkers()" />
</div> </div>
<button id="worker-control" onclick="toggleWorker()">Start verification</button> <button id="worker-control" onclick="toggleWorker()">Start verification</button>
</div> </div>
@@ -138,7 +150,7 @@
<div id="captcha-progress" class="progress-bar"> <div id="captcha-progress" class="progress-bar">
<div class="progress"></div> <div class="progress"></div>
</div> </div>
<textarea disabled id="captcha-progress-text" style="width: 100%; height: 100px; resize: none"></textarea> <textarea disabled id="captcha-progress-text"></textarea>
</div> </div>
<div id="captcha-result"></div> <div id="captcha-result"></div>
</div> </div>
@@ -245,7 +257,7 @@
workers = []; workers = [];
const selectedWorkers = document.getElementById("workers").value; const selectedWorkers = document.getElementById("workers").value;
const workerCount = Math.min(16, Math.max(1, parseInt(selectedWorkers))); const workerCount = Math.min(32, Math.max(1, parseInt(selectedWorkers)));
for (let i = 0; i < workerCount; i++) { for (let i = 0; i < workerCount; i++) {
const worker = new Worker("/res/js/hash-worker.js"); const worker = new Worker("/res/js/hash-worker.js");
worker.onmessage = handleWorkerMessage; worker.onmessage = handleWorkerMessage;