Skip to content

Commit a9171b7

Browse files
fix(contact): add contact and sponsor page in the website
1 parent 8c928d9 commit a9171b7

6 files changed

Lines changed: 379 additions & 66 deletions

File tree

api/duck.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import asyncio
99
import contextlib
1010
import datetime as dt
11+
import logging
1112
import os
1213
import queue
1314
import threading
@@ -23,6 +24,7 @@
2324
from osmsg.history import fetch_manifest
2425
from osmsg.query import Sources
2526

27+
_log = logging.getLogger("osmsg.api.duck")
2628
_PG_ATTACH = "pg"
2729

2830
# Warm connections (extensions + Postgres attached) reused across requests so cold-start is paid once;
@@ -127,8 +129,8 @@ def _warm(fn, hashtag, kwargs, key) -> None:
127129
con = _connect()
128130
try:
129131
fn(con, hashtag, _sources(), **kwargs)
130-
except duckdb.Error:
131-
pass
132+
except duckdb.Error as e:
133+
_log.warning("cache warm rerun of %s for %r failed: %s", fn.__name__, hashtag, e)
132134
finally:
133135
con.close()
134136
with _warm_lock:
@@ -148,6 +150,31 @@ def _enqueue_warm(fn, hashtag, kwargs) -> None:
148150
_warm_pool.submit(_warm, fn, hashtag, kwargs, key)
149151

150152

153+
def _warm_all_time_job(hashtag, key) -> None:
154+
con = _connect()
155+
try:
156+
query.warm_all_time(con, hashtag, _sources())
157+
except duckdb.Error as e:
158+
_log.warning("all-time warm for %r failed: %s", hashtag, e)
159+
finally:
160+
con.close()
161+
with _warm_lock:
162+
_warm_inflight.discard(key)
163+
164+
165+
def _maybe_warm_all_time(hashtag) -> None:
166+
"""Fire a background warm of the all-time history caches (leaderboard per-user tags, trending
167+
co-occurring) for this hashtag when any is still cold, deduped per hashtag. No-op when caching is off."""
168+
if _QUERY_CACHE_DIR is None or not query.all_time_warm_pending(_sources(), hashtag):
169+
return
170+
key = ("warm_all_time", repr(hashtag))
171+
with _warm_lock:
172+
if key in _warm_inflight:
173+
return
174+
_warm_inflight.add(key)
175+
_warm_pool.submit(_warm_all_time_job, hashtag, key)
176+
177+
151178
def _run(fn, hashtag, **kwargs):
152179
"""Borrow a warm pooled connection (429 past _POOL_WAIT caps concurrency), run under a watchdog that
153180
interrupts a query past _QUERY_TIMEOUT, and return it (replaced if it errored or was interrupted)."""
@@ -202,7 +229,7 @@ async def leaderboard(
202229
start: dt.datetime | None = None,
203230
end: dt.datetime | None = None,
204231
):
205-
return await asyncio.to_thread(
232+
res = await asyncio.to_thread(
206233
_run,
207234
query.leaderboard,
208235
hashtag,
@@ -214,6 +241,9 @@ async def leaderboard(
214241
start=start,
215242
end=end,
216243
)
244+
if start is None and end is None:
245+
_maybe_warm_all_time(hashtag)
246+
return res
217247

218248

219249
async def tags(
@@ -229,7 +259,10 @@ async def editors(hashtag: str | list[str], *, start: dt.datetime | None = None,
229259
async def hashtags(
230260
hashtag: str | list[str], *, limit: int = 15, start: dt.datetime | None = None, end: dt.datetime | None = None
231261
):
232-
return await asyncio.to_thread(_run, query.hashtags, hashtag, limit=limit, start=start, end=end)
262+
res = await asyncio.to_thread(_run, query.hashtags, hashtag, limit=limit, start=start, end=end)
263+
if start is None and end is None:
264+
_maybe_warm_all_time(hashtag)
265+
return res
233266

234267

235268
async def trends(

frontend/app.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1192,7 +1192,7 @@ function openUserModal(username) {
11921192
.filter(Boolean)
11931193
.map((h) => "#" + String(h).replace(/^#/, ""));
11941194
const hashtagLine = userHashtags.length
1195-
? `<div class="modal-hashtags">${userHashtags.map((h) => `<span class="mh-chip">${escapeHtml(h)}</span>`).join("")}</div>`
1195+
? `<div class="modal-field"><span class="modal-label">Recent hashtags</span><div class="modal-hashtags">${userHashtags.map((h) => `<span class="mh-chip">${escapeHtml(h)}</span>`).join("")}</div></div>`
11961196
: "";
11971197
const modalEditors = (r.editors || []).filter(Boolean);
11981198
const editorText = modalEditors.length ? groupEditorsText(modalEditors) : "Unknown";

frontend/index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,18 @@ <h3><i data-lucide="database"></i> Where the data comes from</h3>
281281
</div>
282282

283283
<footer>
284+
<div class="support">
285+
<div class="support-lead">
286+
<span class="support-title"><i data-lucide="heart-handshake"></i> Contact &amp; Support</span>
287+
<p>OSMSG is a free, community-driven project built and maintained by volunteers. If it is useful to
288+
you, help sustain it: contribute, sponsor, or partner with us.</p>
289+
</div>
290+
<div class="support-actions">
291+
<a class="support-btn" href="mailto:info@osgeonepal.org"><i data-lucide="mail"></i> info@osgeonepal.org</a>
292+
<a class="support-btn primary" href="https://osgeonepal.org" target="_blank" rel="noopener"><i
293+
data-lucide="heart"></i> Support / Donate</a>
294+
</div>
295+
</div>
284296
<div class="footer-inner">
285297
<div>
286298
<div class="text-ink-2 font-medium mb-1">OSMSG · OSM Stats Generator</div>

frontend/style.css

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,6 +1099,42 @@ span.flatpickr-weekday {
10991099
color: #fff;
11001100
}
11011101

1102+
/* Today can coincide with a selected or range-end day; flatpickr's .today rule otherwise repaints the number
1103+
a low-contrast green over the green fill, so keep the selected styling winning here. */
1104+
.flatpickr-day.today.selected,
1105+
.flatpickr-day.today.startRange,
1106+
.flatpickr-day.today.endRange {
1107+
background: var(--green);
1108+
border-color: var(--green);
1109+
color: #fff;
1110+
}
1111+
1112+
.flatpickr-day.today.inRange {
1113+
background: var(--green-pale);
1114+
border-color: var(--green-pale);
1115+
color: var(--green-deep);
1116+
}
1117+
1118+
/* The dark-mode green is light, so white numbers on a filled day read weakly; use dark ink instead. */
1119+
:root[data-theme="dark"] .flatpickr-day.selected,
1120+
:root[data-theme="dark"] .flatpickr-day.startRange,
1121+
:root[data-theme="dark"] .flatpickr-day.endRange,
1122+
:root[data-theme="dark"] .flatpickr-day.today.selected,
1123+
:root[data-theme="dark"] .flatpickr-day.today.startRange,
1124+
:root[data-theme="dark"] .flatpickr-day.today.endRange {
1125+
color: #10231a;
1126+
}
1127+
1128+
/* Selecting the hour/minute field otherwise shows the browser's default blue highlight in a green picker. */
1129+
.flatpickr-calendar input::selection {
1130+
background: var(--green-pale);
1131+
color: var(--green-deep);
1132+
}
1133+
1134+
.flatpickr-time input:focus {
1135+
background: var(--green-tint);
1136+
}
1137+
11021138
.flatpickr-day.week.selected {
11031139
background: var(--green);
11041140
border-color: var(--green);
@@ -1598,6 +1634,19 @@ button.ht-tag-add:hover {
15981634
word-break: break-word;
15991635
}
16001636

1637+
.modal-field {
1638+
display: flex;
1639+
flex-direction: column;
1640+
gap: 3px;
1641+
}
1642+
1643+
.modal-label {
1644+
font-size: 10.5px;
1645+
text-transform: uppercase;
1646+
letter-spacing: 0.04em;
1647+
color: var(--muted);
1648+
}
1649+
16011650
.modal-hashtags {
16021651
display: flex;
16031652
flex-wrap: wrap;
@@ -2866,6 +2915,13 @@ button.ht-tag-add:hover {
28662915
color: var(--muted);
28672916
}
28682917

2918+
/* With only the placeholder or error row, the header columns would force a horizontal scroll and push the
2919+
message off-centre; drop the header until real rows size the table. */
2920+
.table-scroll:has(.empty) thead,
2921+
.table-scroll:has(.errbox) thead {
2922+
display: none;
2923+
}
2924+
28692925
.empty [data-lucide] {
28702926
width: 40px;
28712927
height: 40px;
@@ -2929,6 +2985,81 @@ footer {
29292985
color: var(--muted);
29302986
}
29312987

2988+
.support {
2989+
display: flex;
2990+
justify-content: space-between;
2991+
align-items: center;
2992+
gap: 24px;
2993+
flex-wrap: wrap;
2994+
padding-bottom: 24px;
2995+
margin-bottom: 24px;
2996+
border-bottom: 1px solid var(--bd);
2997+
}
2998+
2999+
.support-lead {
3000+
max-width: 540px;
3001+
}
3002+
3003+
.support-title {
3004+
display: inline-flex;
3005+
align-items: center;
3006+
gap: 8px;
3007+
color: var(--ink-2);
3008+
font-weight: 600;
3009+
font-size: 14px;
3010+
margin-bottom: 6px;
3011+
}
3012+
3013+
.support-title [data-lucide] {
3014+
width: 16px;
3015+
height: 16px;
3016+
color: var(--green);
3017+
}
3018+
3019+
.support-lead p {
3020+
margin: 0;
3021+
line-height: 1.6;
3022+
}
3023+
3024+
.support-actions {
3025+
display: flex;
3026+
gap: 10px;
3027+
flex-wrap: wrap;
3028+
}
3029+
3030+
.support-btn {
3031+
display: inline-flex;
3032+
align-items: center;
3033+
gap: 6px;
3034+
padding: 7px 12px;
3035+
border: 1px solid var(--bd);
3036+
border-radius: 8px;
3037+
color: var(--ink-2);
3038+
background: var(--surface);
3039+
white-space: nowrap;
3040+
}
3041+
3042+
.support-btn [data-lucide] {
3043+
width: 15px;
3044+
height: 15px;
3045+
}
3046+
3047+
.support-btn:hover {
3048+
border-color: var(--green-soft);
3049+
color: var(--green);
3050+
}
3051+
3052+
.support-btn.primary {
3053+
background: var(--green);
3054+
color: #fff;
3055+
border-color: var(--green);
3056+
}
3057+
3058+
.support-btn.primary:hover {
3059+
background: var(--green-deep);
3060+
color: #fff;
3061+
}
3062+
29323063
.footer-inner {
29333064
display: flex;
29343065
justify-content: space-between;

0 commit comments

Comments
 (0)