Add Timepoll availability polling application and image
Build custom container images / build (map[base_image:php:8-fpm-alpine build_args:PHP_VERSION=8 context:php8-pgsql fingerprint_command:{ apk info -v | LC_ALL=C sort; find /usr/local/lib/php/extensions /usr/local/etc/php/conf.d -type f -exec sha256sum {} + | LC_ALL=C sort; } name:ph… (push) Successful in 49s
Build custom container images / build (map[base_image:postgres:18 build_args:PG_VERSION=18 POSTGIS_VERSION=3 VCHORD_VERSION=0.5.3 context:postgres fingerprint_command:{ dpkg-query -W -f='${binary:Package}=${Version}\n' | LC_ALL=C sort; find /usr/lib/postgresql -type f -exec sha256su… (push) Successful in 1m42s
Build custom container images / build (map[base_image:python:3 build_args:PYTHON_VERSION=3 context:python-tools fingerprint_command:{ dpkg-query -W -f='${binary:Package}=${Version}\n' | LC_ALL=C sort; pip freeze | LC_ALL=C sort; } name:python-tools oci_labels:org.opencontainers.ima… (push) Successful in 54s
Build custom container images / build (map[base_image:python:3.12-slim build_args:PYTHON_VERSION=3.12-slim context:linkki-tiedotus fingerprint_command:{ dpkg-query -W -f='${binary:Package}=${Version}\n' | LC_ALL=C sort; pip freeze | LC_ALL=C sort; } name:linkki-tiedotus oci_labels:… (push) Successful in 29s
Build custom container images / build (map[base_image:python:3.12-slim build_args:PYTHON_VERSION=3.12-slim context:timepoll fingerprint_command:{ dpkg-query -W -f='${binary:Package}=${Version}\n' | LC_ALL=C sort; pip freeze | LC_ALL=C sort; } name:timepoll oci_labels:org.opencontai… (push) Successful in 44s

This commit is contained in:
ajp_anton
2026-09-09 19:39:40 +00:00
parent cb80d420c0
commit 4cc2e8fc80
21 changed files with 3519 additions and 2 deletions
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 562 B

+23
View File
@@ -0,0 +1,23 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light dark" />
<title>Timepoll</title>
<link rel="icon" href="/static/icon.png" type="image/png" />
<link rel="stylesheet" href="/static/style.css" />
<script src="/static/vendor/lucide.min.js" defer></script>
<script src="/static/app.js" defer></script>
</head>
<body>
<header class="site-header">
<a href="/" class="brand"
><img src="/static/icon.png" width="30" height="30" alt="" />Timepoll</a
><span id="connection"></span>
</header>
<main id="app"><p class="muted">Loading...</p></main>
<div id="notice" role="alert" hidden></div>
<div id="popover" role="dialog" aria-label="Slot availability" hidden></div>
</body>
</html>
+859
View File
@@ -0,0 +1,859 @@
:root {
color-scheme: light dark;
--bg: #fafcfb;
--surface: #fff;
--ink: #182a24;
--muted: #63736d;
--line: #d5dfd9;
--soft: #eef3ef;
--green: #166548;
--blue: #1254ad;
--selected: #dcece4;
--shadow: 0 8px 30px #18342625;
--slot-empty: #f1f4f2;
--stripe: #13241d75;
--gray: #68717c;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #131817;
--surface: #1b2320;
--ink: #ebf1ed;
--muted: #a4b3ac;
--line: #3a4841;
--soft: #222e27;
--green: #82d5ac;
--blue: #91c5ff;
--selected: #294237;
--shadow: 0 8px 30px #0007;
--slot-empty: #202c25;
--stripe: #e9fff39c;
--gray: #afb9c1;
}
}
* {
box-sizing: border-box;
letter-spacing: 0;
}
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font:
15px/1.5 system-ui,
sans-serif;
}
button,
input,
select {
font: inherit;
}
button,
a,
input,
select {
touch-action: manipulation;
}
a {
color: var(--blue);
text-underline-offset: 3px;
}
button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
min-height: 40px;
padding: 7px 13px;
border: 1px solid var(--line);
border-radius: 6px;
color: var(--ink);
background: var(--surface);
cursor: pointer;
}
button:hover {
background: var(--soft);
}
button:disabled {
cursor: default;
opacity: 0.5;
}
.primary {
background: #196b4c;
color: #fff;
border-color: #196b4c;
}
.primary:hover {
background: #13583e;
}
.button-link {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 7px 13px;
min-height: 40px;
border-radius: 6px;
text-decoration: none;
}
.danger {
color: #ba3838;
}
.icon-button {
width: 40px;
padding: 8px;
flex: none;
}
svg.lucide {
width: 18px;
height: 18px;
flex: none;
}
input,
select {
min-height: 40px;
max-width: 100%;
padding: 7px 10px;
border: 1px solid var(--line);
border-radius: 5px;
background: var(--surface);
color: var(--ink);
}
input[type="checkbox"] {
min-height: auto;
accent-color: var(--green);
width: 17px;
height: 17px;
}
input[type="time"],
input[type="date"] {
width: auto;
}
input:focus,
select:focus,
button:focus-visible,
a:focus-visible {
outline: 2px solid var(--blue);
outline-offset: 3px;
}
label {
max-width: 100%;
min-width: 0;
display: flex;
flex-direction: column;
gap: 5px;
font-size: 14px;
font-weight: 550;
}
.check {
flex-direction: row;
align-items: center;
gap: 9px;
font-weight: 400;
}
.site-header {
height: 66px;
border-bottom: 1px solid var(--line);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 28px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 10px;
font-size: 21px;
font-weight: 700;
text-decoration: none;
color: var(--ink);
}
.brand img {
border-radius: 6px;
}
#connection {
color: var(--muted);
font-size: 13px;
}
main {
max-width: 1420px;
margin: auto;
padding: 26px 28px 60px;
}
h1 {
font-size: 28px;
line-height: 1.25;
margin: 0 0 12px;
overflow-wrap: anywhere;
}
h2 {
font-size: 19px;
line-height: 1.35;
margin: 0 0 14px;
}
h3 {
font-size: 16px;
margin: 0 0 10px;
}
p {
margin: 8px 0 16px;
}
.muted {
color: var(--muted);
}
.home {
max-width: 800px;
margin: 40px auto;
}
.home h1 {
font-size: 36px;
}
.home-actions {
display: grid;
grid-template-columns: 1fr 1fr;
margin-top: 32px;
gap: 40px;
border-top: 1px solid var(--line);
padding-top: 28px;
}
.home-actions section + section {
border-left: 1px solid var(--line);
padding-left: 40px;
}
.home-actions form {
display: flex;
gap: 8px;
align-items: end;
flex-wrap: wrap;
}
.home-actions input {
width: 170px;
}
.heading {
display: flex;
justify-content: space-between;
align-items: start;
gap: 16px;
margin-bottom: 22px;
}
.heading h1 {
margin-bottom: 6px;
}
.row {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
.between {
justify-content: space-between;
}
.toolbar {
display: flex;
gap: 12px;
align-items: end;
flex-wrap: wrap;
margin: 18px 0;
}
.toolbar label {
font-weight: 400;
}
.fields {
display: flex;
flex-wrap: wrap;
gap: 18px;
align-items: end;
}
.fields .wide {
width: 320px;
max-width: 100%;
}
.editor {
max-width: 1150px;
margin: auto;
}
.band {
border-top: 1px solid var(--line);
padding: 22px 0;
}
.dates {
display: flex;
flex-wrap: wrap;
gap: 6px;
max-height: 150px;
overflow: auto;
margin-top: 16px;
}
.date-chip {
min-height: 32px;
padding: 3px 9px;
font-size: 13px;
}
.date-chip svg {
width: 13px;
height: 13px;
}
.date-entry {
display: flex;
align-items: end;
gap: 18px 30px;
flex-wrap: wrap;
}
.date-range-entry,
.single-date-entry {
display: flex;
flex-wrap: wrap;
align-items: end;
gap: 8px;
}
.date-bucket {
margin: 22px 0 0;
padding: 12px 14px 16px;
border: 1px solid var(--line);
border-radius: 5px;
min-width: 0;
background: var(--soft);
}
.date-bucket legend {
padding: 0 7px;
font-weight: 600;
}
.date-bucket .dates {
min-height: 38px;
margin: 0;
align-items: center;
}
#date-count {
margin-left: 8px;
font-size: 13px;
font-weight: 400;
}
.clipboard-buffer {
position: fixed;
left: -9999px;
top: 0;
}
.code {
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
white-space: nowrap;
font-size: 16px;
font-weight: 650;
}
.code .digit {
color: var(--blue);
}
.links {
max-width: 820px;
margin: auto;
}
.link-row {
display: flex;
gap: 8px;
align-items: center;
margin: 12px 0;
}
.link-row input {
flex: 1;
min-width: 0;
}
.links section {
padding: 22px 0;
border-top: 1px solid var(--line);
}
.links a.primary {
display: inline-flex;
border-radius: 6px;
padding: 10px 16px;
text-decoration: none;
}
.people {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 7px;
margin: 16px 0;
}
.person {
max-width: 100%;
overflow-wrap: anywhere;
min-height: 34px;
font-size: 14px;
}
.person.active {
background: var(--selected);
border-color: var(--green);
}
.person.filtered {
border-color: var(--gray);
background: var(--soft);
}
.new-person {
display: flex;
gap: 8px;
align-items: end;
flex-wrap: wrap;
}
.new-person input {
width: 240px;
}
.editbar {
display: flex;
align-items: center;
gap: 15px;
flex-wrap: wrap;
padding: 13px 0;
border-top: 1px solid var(--line);
border-bottom: 1px solid var(--line);
margin: 16px 0;
}
.editbar .save-actions {
margin-left: auto;
display: flex;
gap: 8px;
}
.editbar strong {
max-width: 100%;
overflow-wrap: anywhere;
}
#vote-actions {
margin-bottom: 18px;
}
.segmented {
display: inline-flex;
max-width: 100%;
flex-wrap: wrap;
}
.segmented button {
border-radius: 0;
margin-left: -1px;
}
.segmented button:first-child {
border-radius: 6px 0 0 6px;
margin-left: 0;
}
.segmented button:last-child {
border-radius: 0 6px 6px 0;
}
.segmented button[aria-pressed="true"] {
background: var(--selected);
border-color: var(--green);
position: relative;
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 16px;
font-size: 13px;
color: var(--muted);
align-items: center;
}
.swatch {
display: inline-block;
width: 13px;
height: 13px;
margin-right: 5px;
border-radius: 2px;
vertical-align: -2px;
background: #399368;
}
.swatch.blue {
background: #478aca;
}
.swatch.maybe {
background:
repeating-linear-gradient(135deg, transparent 0 3px, #405e5070 3px 4px),
#8ac5a8;
}
.swatch.gray {
background: #949fa4;
}
.grid-scroll {
overflow: auto;
max-height: 72vh;
border: 1px solid var(--line);
border-radius: 5px;
background: var(--surface);
overscroll-behavior: contain;
position: relative;
scrollbar-gutter: stable;
}
.grid {
--day-width: 112px;
--time-width: 83px;
border-spacing: 0;
table-layout: fixed;
min-width: calc(var(--time-width) + var(--columns) * var(--day-width));
width: 100%;
border-collapse: separate;
}
.grid .time-column {
width: var(--time-width);
}
.grid th {
position: sticky;
top: 0;
z-index: 2;
background: var(--surface);
font-size: 13px;
text-align: center;
padding: 10px 8px;
border-bottom: 1px solid var(--line);
min-width: 112px;
}
.grid th small {
display: block;
color: var(--muted);
font-weight: 400;
}
.grid .time {
left: 0;
z-index: 1;
position: sticky;
background: var(--surface);
width: 83px;
min-width: 83px;
text-align: right;
padding: 3px 10px 3px 6px;
font-size: 12px;
color: var(--muted);
font-weight: 400;
border-right: 1px solid var(--line);
}
.grid thead .time {
z-index: 3;
top: 0;
}
.grid td {
padding: 0;
min-width: 112px;
height: 32px;
border-right: 1px solid var(--surface);
border-bottom: 1px solid var(--line);
}
.grid td.empty {
background: var(--bg);
}
.grid button.slot {
width: 100%;
height: 32px;
min-height: 32px;
padding: 0 4px;
border: 0;
border-radius: 0;
font-size: 12px;
background-color: var(--cell, var(--slot-empty));
color: var(--ink);
display: block;
position: relative;
user-select: none;
}
.grid button.slot:hover {
box-shadow: inset 0 0 0 2px var(--blue);
}
.grid button.slot.stripes {
background-image: repeating-linear-gradient(
135deg,
transparent 0 calc(var(--spacing) - 1px),
var(--stripe) calc(var(--spacing) - 1px) var(--spacing)
);
}
.grid button.slot.blocked {
background: var(--soft);
color: var(--muted);
opacity: 0.8;
}
.grid button.slot.boundary {
border-top: 2px solid var(--ink);
}
.grid .hour-mark > .time {
font-weight: 700;
color: var(--ink);
background: var(--soft);
border-top: 1px solid var(--muted);
}
.grid .hour-mark > td {
border-top: 1px solid var(--muted);
}
.grid button.slot.peer-boundary {
border-top: 2px solid var(--green);
}
.vote-mark {
position: relative;
z-index: 1;
}
.grid button.draft-slot {
background-color: var(--slot-empty);
background-image: none;
}
.draft-slot::before,
.draft-slot::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
}
.draft-slot::before {
background-color: var(--peer-cell, transparent);
opacity: 0.75;
}
.draft-slot.peer-stripes::before {
background-image: repeating-linear-gradient(
135deg,
transparent 0 calc(var(--peer-spacing) - 1px),
var(--stripe) calc(var(--peer-spacing) - 1px) var(--peer-spacing)
);
}
.draft-slot::after {
background-color: var(--cell, transparent);
opacity: 0.35;
}
.draft-slot.stripes::after {
background-image: repeating-linear-gradient(
135deg,
transparent 0 calc(var(--spacing) - 1px),
var(--stripe) calc(var(--spacing) - 1px) var(--spacing)
);
}
.grid .gap td,
.grid .gap th {
height: 11px;
min-height: 11px;
background: var(--bg);
padding: 0;
}
.grid.editing .slot {
touch-action: none;
}
.slot svg {
width: 14px;
height: 14px;
margin: auto;
}
.grid-foot {
padding: 8px 0;
display: flex;
gap: 16px;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
font-size: 13px;
color: var(--muted);
}
.summary-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 35px;
padding-top: 26px;
}
.summary-list {
padding: 0;
margin: 0;
list-style: none;
}
.summary-list > li {
border-top: 1px solid var(--line);
padding: 13px 0;
}
.summary-list strong {
display: block;
}
.summary-list .muted {
font-size: 13px;
}
.summary-list details {
margin-top: 6px;
font-size: 13px;
}
.summary-list details summary {
cursor: pointer;
color: var(--blue);
}
#popover {
position: fixed;
z-index: 20;
width: 290px;
max-width: calc(100vw - 24px);
max-height: 65vh;
overflow: auto;
padding: 14px;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--surface);
box-shadow: var(--shadow);
}
#popover h3 {
font-size: 14px;
padding-right: 28px;
}
#popover .close {
position: absolute;
right: 5px;
top: 5px;
min-height: 28px;
width: 28px;
padding: 4px;
}
#popover .voter {
display: flex;
width: 100%;
justify-content: space-between;
text-align: left;
border: 0;
min-height: 35px;
background: transparent;
overflow-wrap: anywhere;
}
#popover .voter.absent .name {
text-decoration: line-through;
color: var(--muted);
}
#popover .voter small {
color: var(--muted);
margin-left: 10px;
}
#notice {
position: fixed;
bottom: 18px;
left: 50%;
transform: translateX(-50%);
max-width: calc(100vw - 28px);
width: max-content;
background: var(--ink);
color: var(--bg);
padding: 14px 18px;
border-radius: 6px;
box-shadow: var(--shadow);
z-index: 40;
font-size: 14px;
}
#notice button {
margin-left: 12px;
color: inherit;
background: transparent;
border: 0;
min-height: 22px;
padding: 0;
}
.busy svg {
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
[hidden] {
display: none !important;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
}
.grid.compact td,
.grid.compact button.slot {
height: 20px;
min-height: 20px;
line-height: 16px;
}
.grid.compact tbody .time {
padding-top: 0;
padding-bottom: 0;
line-height: 16px;
}
.grid.compact .gap td {
height: 11px;
min-height: 11px;
}
@media (prefers-reduced-motion: reduce) {
.busy svg {
animation: none;
}
}
@media (max-width: 700px) {
.segmented button {
font-size: 14px;
padding: 7px 8px;
gap: 5px;
}
.grid {
--day-width: 95px;
--time-width: 69px;
}
.site-header {
padding: 0 16px;
height: 58px;
}
main {
padding: 20px 14px 45px;
}
h1 {
font-size: 25px;
}
.home {
margin: 18px auto;
}
.home h1 {
font-size: 30px;
}
.home-actions {
grid-template-columns: 1fr;
gap: 28px;
}
.home-actions section + section {
padding: 24px 0 0;
border-left: 0;
border-top: 1px solid var(--line);
}
.heading {
flex-wrap: wrap;
}
.summary-grid {
grid-template-columns: 1fr;
gap: 18px;
}
.fields {
gap: 12px;
}
.fields .wide {
width: 100%;
}
.editbar {
gap: 12px;
}
.editbar .save-actions {
margin-left: 0;
}
.grid th,
.grid td {
min-width: 95px;
}
.grid .time {
min-width: 69px;
width: 69px;
padding-right: 7px;
}
.grid-scroll {
max-height: 68vh;
}
.date-entry {
gap: 6px;
}
.date-entry input {
font-size: 14px;
}
#connection {
font-size: 12px;
}
.grid button.slot,
.grid td {
height: 36px;
}
.grid button.slot {
min-height: 36px;
}
.new-person input {
width: 190px;
}
}
+9
View File
@@ -0,0 +1,9 @@
# Bundled Assets
- `lucide.min.js`: Lucide 0.468.0, ISC license in `lucide.LICENSE`.
Source: https://unpkg.com/lucide@0.468.0/dist/umd/lucide.min.js
- `../icon.png`: Spiral Calendar (U+1F5D3), Twitter Twemoji 14.0.2.
Graphics licensed under CC BY 4.0: https://creativecommons.org/licenses/by/4.0/
Source: https://github.com/twitter/twemoji/tree/v14.0.2/assets/72x72
Assets are served locally; the browser does not contact any third-party service.
+15
View File
@@ -0,0 +1,15 @@
ISC License
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
File diff suppressed because one or more lines are too long