35 Commits
Author SHA1 Message Date
Justin 4260f9350c adding vsphere to vmare vsphere 2026-10-18 15:42:41 -05:00
Justin b7367f7e35 change the name of the file 2026-10-18 11:50:58 -05:00
Justin 46987b967e working on a adding vsphere 2026-10-18 11:48:06 -05:00
Justin 64020603e1 changed the file chmod to remove -x 2026-10-18 10:33:20 -05:00
JustinandClaude Opus 5.5 3d9c5c4b2b Remove RAW_DATA fallback and show a log-unavailable message
- Drop the embedded 20-contact RAW_DATA sample; the logbook now comes
  only from data/k5bss-log.json
- Show a banner and table message when the log fails to load, and
  display "—" for the header stats
- Skip building the dashboard when there is no data (it crashed on an
  empty log) and guard the activity chart redraw on resize
- Compute the QSO count from the log instead of hardcoding it

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 10:04:44 -05:00
Justin 38f38c89fe moved the javascrpit to its own file and moved the json to its own file 2026-10-18 09:45:40 -05:00
Justin ac08a5b7e5 movied css inline to its own file 2026-10-18 09:33:00 -05:00
Justin a60a44aa82 working on the homelab 2026-09-06 20:36:36 -05:00
Justin 2d96cd70a0 working on the clean up 2026-09-05 14:52:04 -05:00
Justin 5b35c52d58 Resolve unresolved merge conflict markers left in main
The merge of claude/nifty-bhaskara-fb3ec2 left three-way conflict
markers committed straight into index.html, _headers, and
.claude/launch.json instead of being resolved. Two branches had
independently fixed the same duplicate-CSP bug; keep the more
thorough fix (single CSP source of truth in _headers, no meta tag
in index.html, local photo path) and drop the now-unneeded
'unsafe-inline'/ki5bhv.com allowances.
2026-08-31 19:37:44 -05:00
Justin c31ba229af Merge branch 'claude/nifty-bhaskara-fb3ec2' of https://git.ki5bhv.com/justin/k5bss.com
# Conflicts:
#	.claude/launch.json
#	_headers
#	index.html
2026-08-31 19:26:10 -05:00
Justin 49856bdefc Merge branch 'claude/scan-report-issues-93c6e9' of https://git.ki5bhv.com/justin/k5bss.com
# Conflicts:
#	_headers
#	index.html
2026-08-31 14:40:01 -05:00
JustinandClaude Sonnet 5 f8b0a8b070 Add local static-server launch config for browser preview
Lets the Browser pane preview the site locally (python -m http.server)
without needing a full deploy to check CSP/console behavior.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-31 14:26:21 -05:00
JustinandClaude Sonnet 5 9dd8fae16e Fix CSP violations and font 404 from Cloudflare Radar scan
Two conflicting <meta> CSP tags in index.html (plus a separate _headers
policy) meant the browser enforced their intersection, which blocked
the Cloudflare Insights beacon script and its inline bootstrap. k5bss.html
had the same gap in its own CSP. Consolidated to one consistent policy per
surface that allows static.cloudflareinsights.com / cloudflareinsights.com,
and moved index.html's inline script and onerror handler into js/main.js
so script-src no longer needs 'unsafe-inline' there.

Also added the missing fonts/ibm-plex-mono-400.woff2 that style.css has
referenced since it was wired up, which was 404ing.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-31 14:25:10 -05:00
JustinandClaude Sonnet 5 6cea0312d2 Add local static-server launch config for browser preview
Lets the site be served on localhost for testing (e.g. verifying
font/CSP fixes) without needing a separate dev setup.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-31 14:14:15 -05:00
JustinandClaude Sonnet 5 f45dfaa232 Fix CSP blocking Cloudflare Insights beacon and inline scripts
index.html carried two Content-Security-Policy meta tags that were
never reconciled, so browsers enforced their intersection: the
Cloudflare beacon script/connect endpoints, the profile photo from
ki5bhv.com, and the page's own inline script/onerror handler all got
silently blocked depending on which policy line was checked.

Collapse index.html down to a single correct policy, and add the
same static.cloudflareinsights.com / cloudflareinsights.com
allowances to _headers (the actual Cloudflare Pages response header)
and to k5bss.html's policy, since Cloudflare injects that beacon
site-wide.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-31 14:13:19 -05:00
JustinandClaude Sonnet 5 435f56e0cc Fix 404 on self-hosted IBM Plex Mono font
Download the actual woff2 file so it exists at the path style.css
already references, instead of relying on the unused Google Fonts
preconnect hints.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-31 14:09:38 -05:00
Justin 8c665ba239 update 2026-08-31 13:57:34 -05:00
Justin bdcc6c1a8f fixed a picture issues 2026-08-30 16:35:48 -05:00
JustinandClaude Sonnet 5 c7a10ae719 Fix CSP violations from Cloudflare Radar scan
Two duplicate <meta> CSP tags in index.html conflicted with the
_headers HTTP header, and browsers enforce the intersection of all
active policies. That blocked the profile photo, the inline nav-
highlight script, and the Cloudflare Insights beacon.

Consolidate to a single CSP in _headers, allow the actual external
resources the page needs (ki5bhv.com image, cloudflareinsights.com
beacon), and move the inline script/onerror handler into js/main.js
so script-src no longer needs 'unsafe-inline'.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-30 16:28:33 -05:00
Justin 5d6eae87bf fixing issues with cloudflare 2026-08-30 16:17:44 -05:00
Justin a3b77de5aa added font to css 2026-08-30 16:06:07 -05:00
Justin ae21ab1be9 added the font into the html 2026-08-30 15:44:27 -05:00
Justin 1d9ec33761 added the font 2026-08-30 15:40:56 -05:00
Justin c74589555b header 2026-08-30 15:11:28 -05:00
Justin 2f244ea46d added PDQ Connent to job 2026-08-30 09:15:18 -05:00
Justin d89065ca4d added skilles that was was missing 2026-08-30 09:05:20 -05:00
Justin af7db894b3 fixed some spelling issues 2026-08-29 18:54:46 -05:00
Justin 617838a1e1 test 2026-08-29 09:57:26 -05:00
Justin Frasier 89063938d7 test 2026-08-29 09:52:07 -05:00
Justin 5ad5fd720d fix title change 2026-08-27 20:48:25 -05:00
Justin 24ad1a1097 change the title 2026-08-27 20:45:45 -05:00
Justin 1927c1e85f moved the date and local 2026-08-27 20:27:14 -05:00
Justin 5029d3ca76 move the dates 2026-08-27 20:23:29 -05:00
Justin 8ff376324c added Infrastructure Admin job 2026-08-27 20:12:01 -05:00
23 changed files with 3152 additions and 906 deletions

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "static-site",
"runtimeExecutable": "python",
"runtimeArgs": ["-m", "http.server", "8123"],
"port": 8123
}
]
}
Executable → Regular
+1
View File
@@ -1 +1,2 @@
./vs ./vs
./.claude
Executable → Regular
View File
File mode changed.
Executable → Regular
View File
File mode changed.
+9
View File
@@ -0,0 +1,9 @@
/*
Content-Security-Policy: default-src 'self'; script-src 'self' https://static.cloudflareinsights.com; style-src 'self' https://fonts.googleapis.com 'unsafe-inline'; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self' https://cloudflareinsights.com; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'; upgrade-insecure-requests
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin
X-Permitted-Cross-Domain-Policies: none
+576
View File
@@ -0,0 +1,576 @@
:root {
--bg: #0a0c10;
--surface: #111418;
--surface2: #181c22;
--border: #1e2530;
--green: #00ff88;
--green2: #00cc6a;
--amber: #ffaa00;
--red: #ff4455;
--blue: #4499ff;
--cyan: #00ddff;
--text: #c8d8e8;
--muted: #5a7080;
--font-mono: 'Share Tech Mono', monospace;
--font-display: 'Orbitron', sans-serif;
--font-body: 'Barlow', sans-serif;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--font-body);
font-weight: 300;
min-height: 100vh;
overflow-x: hidden;
}
/* Scanline overlay */
body::before {
content: '';
position: fixed;
inset: 0;
background: repeating-linear-gradient(
to bottom,
transparent 0px,
transparent 2px,
rgba(0,0,0,0.07) 2px,
rgba(0,0,0,0.07) 4px
);
pointer-events: none;
z-index: 9999;
}
/* ─── HEADER ─── */
header {
position: relative;
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 0 2rem;
overflow: hidden;
}
header::after {
content: '';
position: absolute;
bottom: 0; left: 0; right: 0;
height: 2px;
background: linear-gradient(90deg, transparent, var(--green), var(--cyan), transparent);
animation: scanH 4s linear infinite;
}
@keyframes scanH {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.header-inner {
max-width: 1400px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.5rem 0;
gap: 2rem;
flex-wrap: wrap;
}
.callsign-block {
display: flex;
align-items: baseline;
gap: 1rem;
}
.callsign {
font-family: var(--font-display);
font-size: clamp(2.5rem, 5vw, 4rem);
font-weight: 900;
color: var(--green);
text-shadow: 0 0 30px rgba(0,255,136,0.5), 0 0 60px rgba(0,255,136,0.2);
letter-spacing: 0.05em;
line-height: 1;
}
.op-name {
font-family: var(--font-mono);
font-size: 0.85rem;
color: var(--muted);
letter-spacing: 0.15em;
}
.header-status {
display: flex;
gap: 2rem;
flex-wrap: wrap;
}
.status-item {
text-align: center;
}
.status-val {
font-family: var(--font-display);
font-size: 1.6rem;
font-weight: 700;
color: var(--amber);
text-shadow: 0 0 20px rgba(255,170,0,0.4);
display: block;
}
.status-lbl {
font-family: var(--font-mono);
font-size: 0.65rem;
color: var(--muted);
letter-spacing: 0.2em;
text-transform: uppercase;
}
.online-badge {
display: flex;
align-items: center;
gap: 0.5rem;
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--green);
padding: 0.4rem 1rem;
border: 1px solid var(--green);
border-radius: 2px;
background: rgba(0,255,136,0.05);
letter-spacing: 0.1em;
}
.pulse-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: var(--green);
box-shadow: 0 0 8px var(--green);
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(0.7); }
}
/* ─── NAV ─── */
nav {
background: var(--surface2);
border-bottom: 1px solid var(--border);
}
.nav-inner {
max-width: 1400px;
margin: 0 auto;
display: flex;
gap: 0;
padding: 0 2rem;
}
.nav-btn {
font-family: var(--font-mono);
font-size: 0.8rem;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--muted);
padding: 0.9rem 1.5rem;
background: none;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.nav-btn:hover { color: var(--text); }
.nav-btn.active { color: var(--green); border-bottom-color: var(--green); }
/* ─── MAIN ─── */
main {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.log-error {
background: var(--surface);
border: 1px solid var(--red);
border-radius: 4px;
color: var(--red);
font-family: var(--font-mono);
font-size: 0.85rem;
letter-spacing: 0.1em;
padding: 1rem 1.5rem;
margin-bottom: 1.5rem;
}
.log-error[hidden] { display: none; }
.section { display: none; }
.section.active { display: block; }
/* ─── DASHBOARD ─── */
.dash-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 4px;
padding: 1.5rem;
position: relative;
overflow: hidden;
}
.card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
}
.card.green::before { background: linear-gradient(90deg, var(--green), transparent); }
.card.amber::before { background: linear-gradient(90deg, var(--amber), transparent); }
.card.blue::before { background: linear-gradient(90deg, var(--blue), transparent); }
.card.cyan::before { background: linear-gradient(90deg, var(--cyan), transparent); }
.card.red::before { background: linear-gradient(90deg, var(--red), transparent); }
.card-title {
font-family: var(--font-mono);
font-size: 0.7rem;
letter-spacing: 0.2em;
color: var(--muted);
text-transform: uppercase;
margin-bottom: 1rem;
}
.bar-chart { display: flex; flex-direction: column; gap: 0.5rem; }
.bar-row {
display: grid;
grid-template-columns: 80px 1fr 40px;
align-items: center;
gap: 0.5rem;
}
.bar-label {
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--text);
text-align: right;
}
.bar-track {
height: 16px;
background: var(--surface2);
border-radius: 2px;
overflow: hidden;
position: relative;
}
.bar-fill {
height: 100%;
border-radius: 2px;
transition: width 1s cubic-bezier(0.4,0,0.2,1);
}
.bar-count {
font-family: var(--font-mono);
font-size: 0.7rem;
color: var(--muted);
}
.country-list {
display: flex;
flex-direction: column;
gap: 0.4rem;
max-height: 220px;
overflow-y: auto;
}
.country-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.3rem 0;
border-bottom: 1px solid var(--border);
font-family: var(--font-mono);
font-size: 0.75rem;
}
.country-name { color: var(--text); }
.country-count {
color: var(--amber);
background: rgba(255,170,0,0.1);
padding: 0.1rem 0.4rem;
border-radius: 2px;
font-size: 0.7rem;
}
/* ─── ACTIVITY CHART ─── */
.activity-card { grid-column: 1 / -1; }
#activityCanvas {
width: 100%;
height: 120px;
display: block;
}
/* ─── LOGBOOK TABLE ─── */
.log-controls {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
flex-wrap: wrap;
align-items: center;
}
.search-input {
font-family: var(--font-mono);
font-size: 0.85rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 2px;
color: var(--text);
padding: 0.5rem 1rem;
flex: 1;
min-width: 200px;
outline: none;
transition: border-color 0.2s;
}
.search-input:focus { border-color: var(--green); }
.search-input::placeholder { color: var(--muted); }
.filter-select {
font-family: var(--font-mono);
font-size: 0.8rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 2px;
color: var(--text);
padding: 0.5rem 0.75rem;
outline: none;
cursor: pointer;
}
.filter-select option { background: var(--surface2); }
.log-meta {
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--muted);
margin-left: auto;
white-space: nowrap;
}
.table-wrap {
overflow-x: auto;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 4px;
}
table {
width: 100%;
border-collapse: collapse;
font-family: var(--font-mono);
font-size: 0.8rem;
}
thead th {
background: var(--surface2);
padding: 0.75rem 1rem;
text-align: left;
color: var(--muted);
letter-spacing: 0.15em;
font-size: 0.7rem;
text-transform: uppercase;
border-bottom: 1px solid var(--border);
white-space: nowrap;
cursor: pointer;
user-select: none;
transition: color 0.2s;
}
thead th:hover { color: var(--green); }
thead th.sort-asc::after { content: ' ▲'; font-size: 0.6rem; }
thead th.sort-desc::after { content: ' ▼'; font-size: 0.6rem; }
tbody tr {
border-bottom: 1px solid rgba(30,37,48,0.5);
transition: background 0.15s;
}
tbody tr:hover { background: rgba(0,255,136,0.03); }
tbody td {
padding: 0.6rem 1rem;
color: var(--text);
white-space: nowrap;
}
.call-cell {
color: var(--cyan);
font-weight: 700;
font-size: 0.9rem;
letter-spacing: 0.05em;
}
.band-badge {
display: inline-block;
padding: 0.15rem 0.5rem;
border-radius: 2px;
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.05em;
}
.band-20M, .band-20m { background: rgba(68,153,255,0.15); color: var(--blue); }
.band-40M, .band-40m { background: rgba(0,255,136,0.12); color: var(--green); }
.band-80M, .band-80m { background: rgba(255,68,85,0.12); color: var(--red); }
.band-10M, .band-10m { background: rgba(0,221,255,0.12); color: var(--cyan); }
.band-default { background: rgba(90,112,128,0.2); color: var(--muted); }
.mode-cell { color: var(--amber); font-size: 0.75rem; letter-spacing: 0.1em; }
.lotw-y { color: var(--green); }
.lotw-n { color: var(--muted); }
.rst-cell { color: var(--text); font-size: 0.75rem; }
.dist-cell { color: var(--muted); font-size: 0.75rem; }
/* Pagination */
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin-top: 1.5rem;
font-family: var(--font-mono);
font-size: 0.8rem;
flex-wrap: wrap;
}
.page-btn {
background: var(--surface);
border: 1px solid var(--border);
color: var(--text);
padding: 0.4rem 0.8rem;
border-radius: 2px;
cursor: pointer;
transition: all 0.2s;
}
.page-btn:hover { border-color: var(--green); color: var(--green); }
.page-btn.active { background: rgba(0,255,136,0.1); border-color: var(--green); color: var(--green); }
.page-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.page-info { color: var(--muted); padding: 0.4rem 0.5rem; }
/* ─── ABOUT ─── */
.about-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
@media (max-width: 768px) {
.about-grid { grid-template-columns: 1fr; }
}
.info-block { margin-bottom: 1.5rem; }
.info-label {
font-family: var(--font-mono);
font-size: 0.65rem;
letter-spacing: 0.2em;
color: var(--muted);
text-transform: uppercase;
margin-bottom: 0.3rem;
}
.info-value {
font-family: var(--font-body);
font-size: 1.1rem;
color: var(--text);
font-weight: 600;
}
.info-value a { color: var(--cyan); text-decoration: none; }
.info-value a:hover { text-decoration: underline; }
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.5rem;
}
.tag {
font-family: var(--font-mono);
font-size: 0.7rem;
letter-spacing: 0.1em;
padding: 0.3rem 0.7rem;
border: 1px solid var(--border);
border-radius: 2px;
color: var(--text);
background: var(--surface2);
}
/* ─── SCROLLBAR ─── */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: var(--surface2); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--muted); }
/* ─── FOOTER ─── */
footer {
text-align: center;
padding: 2rem;
font-family: var(--font-mono);
font-size: 0.7rem;
color: var(--muted);
border-top: 1px solid var(--border);
margin-top: 3rem;
letter-spacing: 0.15em;
}
footer span { color: var(--green); }
/* ─── LOADING ─── */
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 3rem;
gap: 0.5rem;
font-family: var(--font-mono);
font-size: 0.8rem;
color: var(--muted);
}
.spinner {
width: 16px; height: 16px;
border: 2px solid var(--border);
border-top-color: var(--green);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (max-width: 600px) {
.header-inner { padding: 1rem 0; }
.callsign { font-size: 2rem; }
.status-val { font-size: 1.2rem; }
main { padding: 1rem; }
.log-controls { gap: 0.5rem; }
.nav-btn { padding: 0.75rem 1rem; font-size: 0.7rem; }
}
Executable → Regular
+8
View File
@@ -1,5 +1,13 @@
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/ibm-plex-mono-400.woff2') format('woff2');
}
:root { :root {
--bg: #0a0c10; --bg: #0a0c10;
--bg2: #0f1218; --bg2: #0f1218;
+2114
View File
File diff suppressed because it is too large. Load diff
Executable → Regular
View File
File mode changed.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Executable → Regular
View File
File mode changed.

Before

Width:  |  Height:  |  Size: 473 B

After

Width:  |  Height:  |  Size: 473 B

Executable → Regular
View File
File mode changed.

Before

Width:  |  Height:  |  Size: 826 B

After

Width:  |  Height:  |  Size: 826 B

Executable → Regular
View File
File mode changed.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Executable → Regular
View File
File mode changed.

Before

Width:  |  Height:  |  Size: 370 B

After

Width:  |  Height:  |  Size: 370 B

@@ -0,0 +1,52 @@
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(https://fonts.gstatic.com/s/ibmplexmono/v20/-F6qfjptAgt5VM-kVkqdyU8n3oQI8lc.ttf) format('truetype');
}
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/ibmplexmono/v20/-F63fjptAgt5VM-kVkqdyU8n5ig.ttf) format('truetype');
}
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(https://fonts.gstatic.com/s/ibmplexmono/v20/-F6qfjptAgt5VM-kVkqdyU8n3twJ8lc.ttf) format('truetype');
}
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/ibmplexmono/v20/-F6qfjptAgt5VM-kVkqdyU8n3vAO8lc.ttf) format('truetype');
}
@font-face {
font-family: 'IBM Plex Sans';
font-style: normal;
font-weight: 300;
font-stretch: normal;
font-display: swap;
src: url(https://fonts.gstatic.com/s/ibmplexsans/v23/zYXGKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1swZSAXcomDVmadSDtFlzAA.ttf) format('truetype');
}
@font-face {
font-family: 'IBM Plex Sans';
font-style: normal;
font-weight: 400;
font-stretch: normal;
font-display: swap;
src: url(https://fonts.gstatic.com/s/ibmplexsans/v23/zYXGKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1swZSAXcomDVmadSD6llzAA.ttf) format('truetype');
}
@font-face {
font-family: 'IBM Plex Sans';
font-style: normal;
font-weight: 500;
font-stretch: normal;
font-display: swap;
src: url(https://fonts.gstatic.com/s/ibmplexsans/v23/zYXGKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1swZSAXcomDVmadSD2FlzAA.ttf) format('truetype');
}
Binary file not shown.
Executable → Regular
+28 -39
View File
@@ -6,8 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="referrer" content="strict-origin-when-cross-origin" /> <meta name="referrer" content="strict-origin-when-cross-origin" />
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' https://ki5bhv.com data:; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'none'; upgrade-insecure-requests" /> <title>Justin Frasier | Infrastructure Administrator</title>
<title>Justin Frasier | IT Support Analyst</title>
<link rel="sitemap" type="application/xml" href="/sitemap.xml" /> <link rel="sitemap" type="application/xml" href="/sitemap.xml" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='4' fill='%230f1218'/%3E%3Ctext x='3' y='11' font-family='IBM Plex Mono,Courier New,monospace' font-size='6' fill='%234a5568'%3E~$%3C/text%3E%3Ctext x='2' y='27' font-family='IBM Plex Mono,Courier New,monospace' font-size='19' font-weight='600' fill='%2322c55e'%3Ejf%3C/text%3E%3C/svg%3E" /> <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='4' fill='%230f1218'/%3E%3Ctext x='3' y='11' font-family='IBM Plex Mono,Courier New,monospace' font-size='6' fill='%234a5568'%3E~$%3C/text%3E%3Ctext x='2' y='27' font-family='IBM Plex Mono,Courier New,monospace' font-size='19' font-weight='600' fill='%2322c55e'%3Ejf%3C/text%3E%3C/svg%3E" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" /> <link rel="icon" type="image/x-icon" href="/favicon.ico" />
@@ -16,9 +15,6 @@
<link rel="icon" type="image/png" sizes="256x256" href="/favicon-256x256.png" /> <link rel="icon" type="image/png" sizes="256x256" href="/favicon-256x256.png" />
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;600&family=IBM+Plex+Sans:wght@300;400;500&display=swap"
rel="stylesheet">
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
</head> </head>
@@ -28,10 +24,9 @@
<!-- ── Sidebar ── --> <!-- ── Sidebar ── -->
<aside> <aside>
<img class="profile-photo" src="https://ki5bhv.com/photo/justin1.jpg" alt="Justin Frasier" <img class="profile-photo" src="./photo/justin1.jpg" alt="Justin Frasier">
onerror="this.style.display='none'">
<div class="profile-name">Justin Frasier</div> <div class="profile-name">Justin Frasier</div>
<div class="profile-title">IT Support Analyst</div> <div class="profile-title">Infrastructure Administrator</div>
<div class="profile-loc">📍 Bossier City, LA</div> <div class="profile-loc">📍 Bossier City, LA</div>
<hr class="divider"> <hr class="divider">
@@ -65,7 +60,7 @@
<div class="prompt"><span>~/k5bss</span> $ whoami</div> <div class="prompt"><span>~/k5bss</span> $ whoami</div>
<h1>Justin<br>Frasier<span class="cursor"></span></h1> <h1>Justin<br>Frasier<span class="cursor"></span></h1>
<p class="hero-sub"> <p class="hero-sub">
<strong>IT Support Analyst</strong> with a background in computer systems, infrastructure, and security. <strong>Infrastructure Administrator</strong> with a background in computer systems, infrastructure, and security.
CompTIA A+ certified with hands-on experience in <strong>Azure Active Directory</strong>, CompTIA A+ certified with hands-on experience in <strong>Azure Active Directory</strong>,
Windows Server, networking, and self-hosted services. Windows Server, networking, and self-hosted services.
Amateur radio operator <strong>(K5BSS — General Class)</strong>. Amateur radio operator <strong>(K5BSS — General Class)</strong>.
@@ -76,6 +71,7 @@
<span class="tag blue">Azure AD</span> <span class="tag blue">Azure AD</span>
<span class="tag blue">Docker</span> <span class="tag blue">Docker</span>
<span class="tag blue">Proxmox VE</span> <span class="tag blue">Proxmox VE</span>
<span class="tag blue">VMware VSphere</span>
<span class="tag blue">Web Design</span> <span class="tag blue">Web Design</span>
<span class="tag">Self-hosted Infrastructure</span> <span class="tag">Self-hosted Infrastructure</span>
<span class="tag">FCC K5BSS</span> <span class="tag">FCC K5BSS</span>
@@ -87,9 +83,12 @@
<h2><span class="hash">#</span>experience</h2> <h2><span class="hash">#</span>experience</h2>
<div class="exp-item"> <div class="exp-item">
<div class="exp-role">Infrastructure Administrator</div>
<div class="exp-meta">Aug 2026 — Present</div>
<div class="exp-role">IT Support Analyst</div> <div class="exp-role">IT Support Analyst</div>
<div class="exp-meta">Mar 2023 — Aug 2026</div>
<div class="exp-company">Margaritaville Resort &amp; Casino</div> <div class="exp-company">Margaritaville Resort &amp; Casino</div>
<div class="exp-meta">Mar 2023 — Present &nbsp;·&nbsp; Bossier City, LA</div> <div class="exp-meta">Bossier City, LA</div>
<ul class="exp-list"> <ul class="exp-list">
<li>Provide comprehensive Tier 1 and Tier 2 technical support across a large-scale resort and casino environment, diagnosing and resolving hardware, software, and network issues to minimize downtime and maintain seamless business operations.</li> <li>Provide comprehensive Tier 1 and Tier 2 technical support across a large-scale resort and casino environment, diagnosing and resolving hardware, software, and network issues to minimize downtime and maintain seamless business operations.</li>
<li>Deploy, configure, and maintain Windows 10 and Windows 11 workstations, managing domain enrollment through Active Directory to uphold a secure, well-organized network infrastructure.</li> <li>Deploy, configure, and maintain Windows 10 and Windows 11 workstations, managing domain enrollment through Active Directory to uphold a secure, well-organized network infrastructure.</li>
@@ -102,6 +101,7 @@
<li>Manage the full help desk ticket lifecycle — logging, prioritizing, tracking, and resolving requests — ensuring timely support delivery and adherence to service-level expectations.</li> <li>Manage the full help desk ticket lifecycle — logging, prioritizing, tracking, and resolving requests — ensuring timely support delivery and adherence to service-level expectations.</li>
<li>Utilize PDQ Inventory to maintain up-to-date hardware and software asset tracking across all endpoints, enabling informed decision-making for procurement, audits, and lifecycle management.</li> <li>Utilize PDQ Inventory to maintain up-to-date hardware and software asset tracking across all endpoints, enabling informed decision-making for procurement, audits, and lifecycle management.</li>
<li>Leverage PDQ Deploy to automate software deployments and updates across the organization, reducing manual effort and ensuring consistent, timely rollouts enterprise wide.</li> <li>Leverage PDQ Deploy to automate software deployments and updates across the organization, reducing manual effort and ensuring consistent, timely rollouts enterprise wide.</li>
<li>Converting PDQ Deploy & Invertory to PDQ Connent. Using PDQ Connent to manage and push packages and update to workstations and laptopps.</li>
<li>Administer VMware vSphere environments, managing virtual machine provisioning, configuration, and maintenance to support reliable and scalable infrastructure operations.</li> <li>Administer VMware vSphere environments, managing virtual machine provisioning, configuration, and maintenance to support reliable and scalable infrastructure operations.</li>
<li>Manage and support the Cisco phone system, handling configuration, provisioning, and troubleshooting of IP phones to ensure reliable communication across the organization.</li> <li>Manage and support the Cisco phone system, handling configuration, provisioning, and troubleshooting of IP phones to ensure reliable communication across the organization.</li>
<li>Utilize TeamViewer to provide remote technical support, enabling efficient diagnosis and resolution of end-user issues without requiring an on-site visit.</li> <li>Utilize TeamViewer to provide remote technical support, enabling efficient diagnosis and resolution of end-user issues without requiring an on-site visit.</li>
@@ -199,13 +199,18 @@
<div class="skill-cell"><span class="skill-icon">▸</span>Help Desk Ticketing Systems</div> <div class="skill-cell"><span class="skill-icon">▸</span>Help Desk Ticketing Systems</div>
<div class="skill-cell"><span class="skill-icon">▸</span>PDQ Deploy</div> <div class="skill-cell"><span class="skill-icon">▸</span>PDQ Deploy</div>
<div class="skill-cell"><span class="skill-icon">▸</span>PDQ Inventory</div> <div class="skill-cell"><span class="skill-icon">▸</span>PDQ Inventory</div>
<div class="skill-cell"><span class="skill-icon">▸</span>Cisco Phone Systems</div> <div class="skill-cell"><span class="skill-icon">▸</span>PDQ Connent</div>
<div class="skill-cell"><span class="skill-icon">▸</span>Cisco Systems</div>
<div class="skill-cell"><span class="skill-icon">▸</span>TeamViewer</div> <div class="skill-cell"><span class="skill-icon">▸</span>TeamViewer</div>
<div class="skill-cell"><span class="skill-icon">▸</span>PRTG Network Monitor</div> <div class="skill-cell"><span class="skill-icon">▸</span>PRTG Network Monitor</div>
<div class="skill-cell"><span class="skill-icon">▸</span>Web Hosting</div> <div class="skill-cell"><span class="skill-icon">▸</span>Web Hosting</div>
<div class="skill-cell"><span class="skill-icon">▸</span>Docker & Container Management</div>
<div class="skill-cell"><span class="skill-icon">▸</span>Ansible</div> <div class="skill-cell"><span class="skill-icon">▸</span>Ansible</div>
<div class="skill-cell"><span class="skill-icon">▸</span>Proxmox VE</div> <div class="skill-cell"><span class="skill-icon">▸</span>Rubrik Backup</div>
<div class="skill-cell"><span class="skill-icon">▸</span>Proxmox Backup Server</div>
<div class="skill-cell"><span class="skill-icon">▸</span>Nginx Proxy Manager</div>
<div class="skill-cell"><span class="skill-icon">▸</span>Nginx Web Server</div>
<div class="skill-cell"><span class="skill-icon">▸</span>Windows server DNS</div>
<div class="skill-cell"><span class="skill-icon">▸</span>Windows server DHCP</div>
</div> </div>
<div class="skills-subheading">Development IDEs</div> <div class="skills-subheading">Development IDEs</div>
@@ -251,8 +256,14 @@
<div class="skill-cell"><span class="skill-icon">▸</span>Crisis Management</div> <div class="skill-cell"><span class="skill-icon">▸</span>Crisis Management</div>
</div> </div>
<!-- <div class="skills-subheading">Home Lab</div>
<div class="skills-grid">
<div class="skill-cell"><span class="skill-icon">▸</span>Docker</div>
<div class="skill-cell"><span class="skill-icon">▸</span></div>
<div class="skill-cell"><span class="skill-icon">▸</span>Corporate Security</div>
<div class="skill-cell"><span class="skill-icon">▸</span>Corporate Security</div>
</section> </section>
-->
<!-- Education --> <!-- Education -->
<section id="education"> <section id="education">
<h2><span class="hash">#</span>education</h2> <h2><span class="hash">#</span>education</h2>
@@ -353,33 +364,11 @@
</div> </div>
</div> </div>
</section> </section>
<footer> <footer>
<span style="color:var(--accent)">$</span> site built by Justin Frasier &nbsp;·&nbsp; k5bss.com &nbsp;·&nbsp; <span style="color:var(--accent)">$</span> site built by Justin Frasier &nbsp;·&nbsp; k5bss.com &nbsp;·&nbsp; 2026
2026
</footer> </footer>
</main> </main>
</div> </div>
<script src="js/main.js" defer></script>
<script>
// Highlight active nav link on scroll
const sections = document.querySelectorAll('section[id]');
const links = document.querySelectorAll('nav a');
const observer = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting) {
links.forEach(l => l.classList.remove('active'));
const active = document.querySelector(`nav a[href="#${e.target.id}"]`);
if (active) active.classList.add('active');
}
});
}, { threshold: 0.3 });
sections.forEach(s => observer.observe(s));
</script>
</body> </body>
</html>
</html>
+318
View File
@@ -0,0 +1,318 @@
// ─── DATA ───────────────────────────────────────────────────
let ALL_DATA = [];
let filteredData = [];
let sortKey = 'date';
let sortDir = -1; // -1 = desc
let currentPage = 1;
const PAGE_SIZE = 50;
let logUnavailable = false;
// ─── FULL LOG ────────────────────────────────────────────────
const FULL_LOG_URL = 'data/k5bss-log.json';
async function loadFullLog() {
try {
const res = await fetch(FULL_LOG_URL);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.warn('Could not load full log:', err);
return null;
}
}
async function init() {
const log = await loadFullLog();
logUnavailable = !log;
ALL_DATA = log || [];
filteredData = [...ALL_DATA];
if (logUnavailable) {
document.getElementById('log-error').hidden = false;
renderTable();
return;
}
// Update stats
const countries = new Set(ALL_DATA.map(r => r.country).filter(Boolean));
const states = new Set(ALL_DATA.filter(r => r.country === 'United States').map(r => r.state).filter(Boolean));
document.getElementById('stat-qso').textContent = ALL_DATA.length.toLocaleString();
document.getElementById('stat-dxcc').textContent = countries.size;
document.getElementById('stat-states').textContent = states.size;
buildDashboard();
populateFilters();
renderTable();
}
// ─── DASHBOARD ───────────────────────────────────────────────
function buildDashboard() {
// Modes
const modes = countBy(ALL_DATA, 'mode');
const topModes = Object.entries(modes).sort((a,b)=>b[1]-a[1]).slice(0,7);
const maxMode = topModes[0][1];
const modesEl = document.getElementById('modes-chart');
const modeColors = { FT8:'#ffaa00', FT4:'#ffcc44', DIG:'#ff8800', USB:'#4499ff', LSB:'#66aaff', SSB:'#88bbff', RTTY:'#ff4455' };
modesEl.innerHTML = topModes.map(([m, c]) => `
<div class="bar-row">
<div class="bar-label">${m}</div>
<div class="bar-track"><div class="bar-fill" style="width:0%;background:${modeColors[m]||'#888'}" data-target="${(c/maxMode*100).toFixed(1)}"></div></div>
<div class="bar-count">${c}</div>
</div>`).join('');
// Bands
const bands = countBy(ALL_DATA, 'band');
const topBands = Object.entries(bands).sort((a,b)=>b[1]-a[1]).slice(0,8);
const maxBand = topBands[0][1];
const bandEl = document.getElementById('bands-chart');
const bandColors = { '20M':'#4499ff','40M':'#00ff88','80M':'#ff4455','10M':'#00ddff','30M':'#88aaff','17M':'#aaddff','15M':'#ffaaff','12M':'#ffdd88' };
bandEl.innerHTML = topBands.map(([b, c]) => `
<div class="bar-row">
<div class="bar-label">${b}</div>
<div class="bar-track"><div class="bar-fill" style="width:0%;background:${bandColors[b]||'#888'}" data-target="${(c/maxBand*100).toFixed(1)}"></div></div>
<div class="bar-count">${c}</div>
</div>`).join('');
// Countries
const countries = countBy(ALL_DATA, 'country');
const topCountries = Object.entries(countries).sort((a,b)=>b[1]-a[1]).slice(0,20);
const ctEl = document.getElementById('countries-list');
ctEl.innerHTML = topCountries.map(([c,n]) => `
<div class="country-row">
<span class="country-name">${c||'Unknown'}</span>
<span class="country-count">${n}</span>
</div>`).join('');
// Animate bars after a tick
requestAnimationFrame(() => {
setTimeout(() => {
document.querySelectorAll('.bar-fill').forEach(el => {
el.style.width = el.dataset.target + '%';
});
}, 100);
});
// Activity chart
drawActivityChart();
}
function drawActivityChart() {
if (!ALL_DATA.length) return;
const canvas = document.getElementById('activityCanvas');
const dpr = window.devicePixelRatio || 1;
const W = canvas.parentElement.offsetWidth - 48;
const H = 120;
canvas.width = W * dpr;
canvas.height = H * dpr;
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
// Group by year-month
const monthly = {};
ALL_DATA.forEach(r => {
if (!r.date) return;
const key = r.date.slice(0,7);
monthly[key] = (monthly[key]||0) + 1;
});
const keys = Object.keys(monthly).sort();
const vals = keys.map(k => monthly[k]);
const max = Math.max(...vals);
const pad = { l: 10, r: 10, t: 15, b: 30 };
const chartW = W - pad.l - pad.r;
const chartH = H - pad.t - pad.b;
const barW = Math.max(2, (chartW / keys.length) - 1);
ctx.clearRect(0,0,W,H);
// Grid line
ctx.strokeStyle = 'rgba(30,37,48,0.8)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(pad.l, pad.t + chartH);
ctx.lineTo(W - pad.r, pad.t + chartH);
ctx.stroke();
// Bars
keys.forEach((k, i) => {
const x = pad.l + i * (chartW / keys.length);
const h = (vals[i] / max) * chartH;
const y = pad.t + chartH - h;
const grad = ctx.createLinearGradient(0, y, 0, y + h);
grad.addColorStop(0, 'rgba(0,255,136,0.9)');
grad.addColorStop(1, 'rgba(0,255,136,0.15)');
ctx.fillStyle = grad;
ctx.fillRect(x, y, barW, h);
});
// X labels (every 6 months)
ctx.fillStyle = 'rgba(90,112,128,0.9)';
ctx.font = `${9 * dpr / dpr}px 'Share Tech Mono', monospace`;
ctx.textAlign = 'center';
keys.forEach((k, i) => {
if (i % 6 === 0) {
const x = pad.l + i * (chartW / keys.length) + barW/2;
ctx.fillText(k, x, H - 8);
}
});
}
// ─── TABLE ───────────────────────────────────────────────────
function populateFilters() {
const bands = [...new Set(ALL_DATA.map(r=>r.band).filter(Boolean))].sort();
const modes = [...new Set(ALL_DATA.map(r=>r.mode).filter(Boolean))].sort();
const bandSel = document.getElementById('filter-band');
const modeSel = document.getElementById('filter-mode');
bands.forEach(b => { const o = document.createElement('option'); o.value=b; o.text=b; bandSel.appendChild(o); });
modes.forEach(m => { const o = document.createElement('option'); o.value=m; o.text=m; modeSel.appendChild(o); });
}
function applyFilters() {
const q = document.getElementById('search-input').value.trim().toLowerCase();
const band = document.getElementById('filter-band').value;
const mode = document.getElementById('filter-mode').value;
const lotw = document.getElementById('filter-lotw').value;
filteredData = ALL_DATA.filter(r => {
if (band && r.band !== band) return false;
if (mode && r.mode !== mode) return false;
if (lotw && r.lotw !== lotw) return false;
if (q) {
const hay = [r.call, r.name, r.country, r.qth, r.state, r.grid].join(' ').toLowerCase();
if (!hay.includes(q)) return false;
}
return true;
});
currentPage = 1;
renderTable();
}
function sortTable(key) {
if (sortKey === key) sortDir *= -1;
else { sortKey = key; sortDir = key === 'date' || key === 'time' ? -1 : 1; }
filteredData.sort((a,b) => {
let av = a[key]||'', bv = b[key]||'';
if (key === 'dist') { av = parseInt(av)||0; bv = parseInt(bv)||0; }
if (av < bv) return -sortDir;
if (av > bv) return sortDir;
return 0;
});
// Update header indicators
document.querySelectorAll('thead th').forEach(th => {
th.classList.remove('sort-asc','sort-desc');
});
const headers = ['call','name','date','time','band','mode','rst_sent','country','dist','lotw'];
const idx = headers.indexOf(key);
if (idx >= 0) {
const ths = document.querySelectorAll('thead th');
ths[idx].classList.add(sortDir === 1 ? 'sort-asc' : 'sort-desc');
}
renderTable();
}
function renderTable() {
const tbody = document.getElementById('log-tbody');
const total = filteredData.length;
const pages = Math.ceil(total / PAGE_SIZE);
const start = (currentPage - 1) * PAGE_SIZE;
const slice = filteredData.slice(start, start + PAGE_SIZE);
document.getElementById('log-meta').textContent =
`${total.toLocaleString()} QSOs · Page ${currentPage} of ${Math.max(1,pages)}`;
if (slice.length === 0) {
const msg = logUnavailable ? 'LOG UNAVAILABLE — TRY AGAIN LATER' : 'NO CONTACTS FOUND';
tbody.innerHTML = `<tr><td colspan="10" style="text-align:center;padding:2rem;color:var(--muted);font-family:var(--font-mono);font-size:0.8rem;">${msg}</td></tr>`;
} else {
tbody.innerHTML = slice.map(r => {
const timeStr = r.time ? r.time.slice(0,2)+':'+r.time.slice(2,4) : '';
const bandClass = `band-${r.band}`;
const lotwClass = r.lotw === 'Y' ? 'lotw-y' : 'lotw-n';
const lotwSym = r.lotw === 'Y' ? '✓' : '·';
const rst = r.rst_sent && r.rst_rcvd ? `${r.rst_sent} / ${r.rst_rcvd}` : r.rst_sent || '';
return `<tr>
<td class="call-cell">${esc(r.call)}</td>
<td>${esc(r.name ? titleCase(r.name) : '')}</td>
<td>${esc(r.date)}</td>
<td>${timeStr}Z</td>
<td><span class="band-badge ${bandClass}">${esc(r.band)}</span></td>
<td class="mode-cell">${esc(r.mode)}</td>
<td class="rst-cell">${esc(rst)}</td>
<td>${esc(r.country||'')}${r.state ? ' <span style="color:var(--muted)">'+esc(r.state)+'</span>' : ''}</td>
<td class="dist-cell">${r.dist ? Number(r.dist).toLocaleString() : ''}</td>
<td class="${lotwClass}" style="text-align:center">${lotwSym}</td>
</tr>`;
}).join('');
}
renderPagination(pages);
}
function renderPagination(pages) {
const el = document.getElementById('pagination');
if (pages <= 1) { el.innerHTML = ''; return; }
let html = `<button class="page-btn" onclick="goPage(${currentPage-1})" ${currentPage===1?'disabled':''}>‹ Prev</button>`;
const range = [];
if (pages <= 7) {
for (let i=1;i<=pages;i++) range.push(i);
} else {
range.push(1);
if (currentPage > 3) range.push('...');
for (let i=Math.max(2,currentPage-1); i<=Math.min(pages-1,currentPage+1); i++) range.push(i);
if (currentPage < pages-2) range.push('...');
range.push(pages);
}
range.forEach(p => {
if (p === '...') html += `<span class="page-info">…</span>`;
else html += `<button class="page-btn ${p===currentPage?'active':''}" onclick="goPage(${p})">${p}</button>`;
});
html += `<button class="page-btn" onclick="goPage(${currentPage+1})" ${currentPage===pages?'disabled':''}>Next ›</button>`;
el.innerHTML = html;
}
function goPage(p) {
currentPage = p;
renderTable();
document.querySelector('.table-wrap').scrollIntoView({behavior:'smooth',block:'nearest'});
}
// ─── HELPERS ─────────────────────────────────────────────────
function countBy(arr, key) {
return arr.reduce((acc, r) => {
const v = r[key];
if (v) acc[v] = (acc[v]||0)+1;
return acc;
}, {});
}
function esc(s) {
return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function titleCase(s) {
return s.toLowerCase().replace(/\b\w/g, c => c.toUpperCase());
}
function showSection(id, btn) {
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
document.getElementById('section-' + id).classList.add('active');
btn.classList.add('active');
}
window.addEventListener('load', init);
window.addEventListener('resize', () => {
if (document.getElementById('section-dashboard').classList.contains('active')) {
drawActivityChart();
}
});
+23
View File
@@ -0,0 +1,23 @@
// Highlight active nav link on scroll
const sections = document.querySelectorAll('section[id]');
const links = document.querySelectorAll('nav a');
const observer = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting) {
links.forEach(l => l.classList.remove('active'));
const active = document.querySelector(`nav a[href="#${e.target.id}"]`);
if (active) active.classList.add('active');
}
});
}, { threshold: 0.3 });
sections.forEach(s => observer.observe(s));
// Hide the profile photo if it fails to load
const profilePhoto = document.querySelector('.profile-photo');
if (profilePhoto) {
profilePhoto.addEventListener('error', () => {
profilePhoto.style.display = 'none';
});
}
Executable → Regular
+8 -867
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="referrer" content="strict-origin-when-cross-origin"> <meta name="referrer" content="strict-origin-when-cross-origin">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'none'; upgrade-insecure-requests"> <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'unsafe-inline' https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self' https://cloudflareinsights.com; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'none'; upgrade-insecure-requests">
<title>K5BSS — Justin Frasier · Ham Radio Station</title> <title>K5BSS — Justin Frasier · Ham Radio Station</title>
<link rel="sitemap" type="application/xml" href="/sitemap.xml"> <link rel="sitemap" type="application/xml" href="/sitemap.xml">
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='4' fill='%230f1218'/%3E%3Ctext x='3' y='11' font-family='IBM Plex Mono,Courier New,monospace' font-size='6' fill='%234a5568'%3E~$%3C/text%3E%3Ctext x='2' y='27' font-family='IBM Plex Mono,Courier New,monospace' font-size='19' font-weight='600' fill='%2322c55e'%3Ejf%3C/text%3E%3C/svg%3E"> <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='4' fill='%230f1218'/%3E%3Ctext x='3' y='11' font-family='IBM Plex Mono,Courier New,monospace' font-size='6' fill='%234a5568'%3E~$%3C/text%3E%3Ctext x='2' y='27' font-family='IBM Plex Mono,Courier New,monospace' font-size='19' font-weight='600' fill='%2322c55e'%3Ejf%3C/text%3E%3C/svg%3E">
@@ -16,570 +16,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@400;700;900&family=Barlow:wght@300;400;600&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@400;700;900&family=Barlow:wght@300;400;600&display=swap" rel="stylesheet">
<style> <link rel="stylesheet" href="css/k5bss.css">
:root {
--bg: #0a0c10;
--surface: #111418;
--surface2: #181c22;
--border: #1e2530;
--green: #00ff88;
--green2: #00cc6a;
--amber: #ffaa00;
--red: #ff4455;
--blue: #4499ff;
--cyan: #00ddff;
--text: #c8d8e8;
--muted: #5a7080;
--font-mono: 'Share Tech Mono', monospace;
--font-display: 'Orbitron', sans-serif;
--font-body: 'Barlow', sans-serif;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--font-body);
font-weight: 300;
min-height: 100vh;
overflow-x: hidden;
}
/* Scanline overlay */
body::before {
content: '';
position: fixed;
inset: 0;
background: repeating-linear-gradient(
to bottom,
transparent 0px,
transparent 2px,
rgba(0,0,0,0.07) 2px,
rgba(0,0,0,0.07) 4px
);
pointer-events: none;
z-index: 9999;
}
/* ─── HEADER ─── */
header {
position: relative;
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 0 2rem;
overflow: hidden;
}
header::after {
content: '';
position: absolute;
bottom: 0; left: 0; right: 0;
height: 2px;
background: linear-gradient(90deg, transparent, var(--green), var(--cyan), transparent);
animation: scanH 4s linear infinite;
}
@keyframes scanH {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.header-inner {
max-width: 1400px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.5rem 0;
gap: 2rem;
flex-wrap: wrap;
}
.callsign-block {
display: flex;
align-items: baseline;
gap: 1rem;
}
.callsign {
font-family: var(--font-display);
font-size: clamp(2.5rem, 5vw, 4rem);
font-weight: 900;
color: var(--green);
text-shadow: 0 0 30px rgba(0,255,136,0.5), 0 0 60px rgba(0,255,136,0.2);
letter-spacing: 0.05em;
line-height: 1;
}
.op-name {
font-family: var(--font-mono);
font-size: 0.85rem;
color: var(--muted);
letter-spacing: 0.15em;
}
.header-status {
display: flex;
gap: 2rem;
flex-wrap: wrap;
}
.status-item {
text-align: center;
}
.status-val {
font-family: var(--font-display);
font-size: 1.6rem;
font-weight: 700;
color: var(--amber);
text-shadow: 0 0 20px rgba(255,170,0,0.4);
display: block;
}
.status-lbl {
font-family: var(--font-mono);
font-size: 0.65rem;
color: var(--muted);
letter-spacing: 0.2em;
text-transform: uppercase;
}
.online-badge {
display: flex;
align-items: center;
gap: 0.5rem;
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--green);
padding: 0.4rem 1rem;
border: 1px solid var(--green);
border-radius: 2px;
background: rgba(0,255,136,0.05);
letter-spacing: 0.1em;
}
.pulse-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: var(--green);
box-shadow: 0 0 8px var(--green);
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(0.7); }
}
/* ─── NAV ─── */
nav {
background: var(--surface2);
border-bottom: 1px solid var(--border);
}
.nav-inner {
max-width: 1400px;
margin: 0 auto;
display: flex;
gap: 0;
padding: 0 2rem;
}
.nav-btn {
font-family: var(--font-mono);
font-size: 0.8rem;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--muted);
padding: 0.9rem 1.5rem;
background: none;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.nav-btn:hover { color: var(--text); }
.nav-btn.active { color: var(--green); border-bottom-color: var(--green); }
/* ─── MAIN ─── */
main {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.section { display: none; }
.section.active { display: block; }
/* ─── DASHBOARD ─── */
.dash-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 4px;
padding: 1.5rem;
position: relative;
overflow: hidden;
}
.card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
}
.card.green::before { background: linear-gradient(90deg, var(--green), transparent); }
.card.amber::before { background: linear-gradient(90deg, var(--amber), transparent); }
.card.blue::before { background: linear-gradient(90deg, var(--blue), transparent); }
.card.cyan::before { background: linear-gradient(90deg, var(--cyan), transparent); }
.card.red::before { background: linear-gradient(90deg, var(--red), transparent); }
.card-title {
font-family: var(--font-mono);
font-size: 0.7rem;
letter-spacing: 0.2em;
color: var(--muted);
text-transform: uppercase;
margin-bottom: 1rem;
}
.bar-chart { display: flex; flex-direction: column; gap: 0.5rem; }
.bar-row {
display: grid;
grid-template-columns: 80px 1fr 40px;
align-items: center;
gap: 0.5rem;
}
.bar-label {
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--text);
text-align: right;
}
.bar-track {
height: 16px;
background: var(--surface2);
border-radius: 2px;
overflow: hidden;
position: relative;
}
.bar-fill {
height: 100%;
border-radius: 2px;
transition: width 1s cubic-bezier(0.4,0,0.2,1);
}
.bar-count {
font-family: var(--font-mono);
font-size: 0.7rem;
color: var(--muted);
}
.country-list {
display: flex;
flex-direction: column;
gap: 0.4rem;
max-height: 220px;
overflow-y: auto;
}
.country-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.3rem 0;
border-bottom: 1px solid var(--border);
font-family: var(--font-mono);
font-size: 0.75rem;
}
.country-name { color: var(--text); }
.country-count {
color: var(--amber);
background: rgba(255,170,0,0.1);
padding: 0.1rem 0.4rem;
border-radius: 2px;
font-size: 0.7rem;
}
/* ─── ACTIVITY CHART ─── */
.activity-card { grid-column: 1 / -1; }
#activityCanvas {
width: 100%;
height: 120px;
display: block;
}
/* ─── LOGBOOK TABLE ─── */
.log-controls {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
flex-wrap: wrap;
align-items: center;
}
.search-input {
font-family: var(--font-mono);
font-size: 0.85rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 2px;
color: var(--text);
padding: 0.5rem 1rem;
flex: 1;
min-width: 200px;
outline: none;
transition: border-color 0.2s;
}
.search-input:focus { border-color: var(--green); }
.search-input::placeholder { color: var(--muted); }
.filter-select {
font-family: var(--font-mono);
font-size: 0.8rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 2px;
color: var(--text);
padding: 0.5rem 0.75rem;
outline: none;
cursor: pointer;
}
.filter-select option { background: var(--surface2); }
.log-meta {
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--muted);
margin-left: auto;
white-space: nowrap;
}
.table-wrap {
overflow-x: auto;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 4px;
}
table {
width: 100%;
border-collapse: collapse;
font-family: var(--font-mono);
font-size: 0.8rem;
}
thead th {
background: var(--surface2);
padding: 0.75rem 1rem;
text-align: left;
color: var(--muted);
letter-spacing: 0.15em;
font-size: 0.7rem;
text-transform: uppercase;
border-bottom: 1px solid var(--border);
white-space: nowrap;
cursor: pointer;
user-select: none;
transition: color 0.2s;
}
thead th:hover { color: var(--green); }
thead th.sort-asc::after { content: ' ▲'; font-size: 0.6rem; }
thead th.sort-desc::after { content: ' ▼'; font-size: 0.6rem; }
tbody tr {
border-bottom: 1px solid rgba(30,37,48,0.5);
transition: background 0.15s;
}
tbody tr:hover { background: rgba(0,255,136,0.03); }
tbody td {
padding: 0.6rem 1rem;
color: var(--text);
white-space: nowrap;
}
.call-cell {
color: var(--cyan);
font-weight: 700;
font-size: 0.9rem;
letter-spacing: 0.05em;
}
.band-badge {
display: inline-block;
padding: 0.15rem 0.5rem;
border-radius: 2px;
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.05em;
}
.band-20M, .band-20m { background: rgba(68,153,255,0.15); color: var(--blue); }
.band-40M, .band-40m { background: rgba(0,255,136,0.12); color: var(--green); }
.band-80M, .band-80m { background: rgba(255,68,85,0.12); color: var(--red); }
.band-10M, .band-10m { background: rgba(0,221,255,0.12); color: var(--cyan); }
.band-default { background: rgba(90,112,128,0.2); color: var(--muted); }
.mode-cell { color: var(--amber); font-size: 0.75rem; letter-spacing: 0.1em; }
.lotw-y { color: var(--green); }
.lotw-n { color: var(--muted); }
.rst-cell { color: var(--text); font-size: 0.75rem; }
.dist-cell { color: var(--muted); font-size: 0.75rem; }
/* Pagination */
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin-top: 1.5rem;
font-family: var(--font-mono);
font-size: 0.8rem;
flex-wrap: wrap;
}
.page-btn {
background: var(--surface);
border: 1px solid var(--border);
color: var(--text);
padding: 0.4rem 0.8rem;
border-radius: 2px;
cursor: pointer;
transition: all 0.2s;
}
.page-btn:hover { border-color: var(--green); color: var(--green); }
.page-btn.active { background: rgba(0,255,136,0.1); border-color: var(--green); color: var(--green); }
.page-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.page-info { color: var(--muted); padding: 0.4rem 0.5rem; }
/* ─── ABOUT ─── */
.about-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
@media (max-width: 768px) {
.about-grid { grid-template-columns: 1fr; }
}
.info-block { margin-bottom: 1.5rem; }
.info-label {
font-family: var(--font-mono);
font-size: 0.65rem;
letter-spacing: 0.2em;
color: var(--muted);
text-transform: uppercase;
margin-bottom: 0.3rem;
}
.info-value {
font-family: var(--font-body);
font-size: 1.1rem;
color: var(--text);
font-weight: 600;
}
.info-value a { color: var(--cyan); text-decoration: none; }
.info-value a:hover { text-decoration: underline; }
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.5rem;
}
.tag {
font-family: var(--font-mono);
font-size: 0.7rem;
letter-spacing: 0.1em;
padding: 0.3rem 0.7rem;
border: 1px solid var(--border);
border-radius: 2px;
color: var(--text);
background: var(--surface2);
}
/* ─── SCROLLBAR ─── */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: var(--surface2); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--muted); }
/* ─── FOOTER ─── */
footer {
text-align: center;
padding: 2rem;
font-family: var(--font-mono);
font-size: 0.7rem;
color: var(--muted);
border-top: 1px solid var(--border);
margin-top: 3rem;
letter-spacing: 0.15em;
}
footer span { color: var(--green); }
/* ─── LOADING ─── */
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 3rem;
gap: 0.5rem;
font-family: var(--font-mono);
font-size: 0.8rem;
color: var(--muted);
}
.spinner {
width: 16px; height: 16px;
border: 2px solid var(--border);
border-top-color: var(--green);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (max-width: 600px) {
.header-inner { padding: 1rem 0; }
.callsign { font-size: 2rem; }
.status-val { font-size: 1.2rem; }
main { padding: 1rem; }
.log-controls { gap: 0.5rem; }
.nav-btn { padding: 0.75rem 1rem; font-size: 0.7rem; }
}
</style>
</head> </head>
<body> <body>
@@ -593,7 +30,7 @@
</div> </div>
<div class="header-status"> <div class="header-status">
<div class="status-item"> <div class="status-item">
<span class="status-val" id="stat-qso">2,112</span> <span class="status-val" id="stat-qso">—</span>
<span class="status-lbl">QSOs Logged</span> <span class="status-lbl">QSOs Logged</span>
</div> </div>
<div class="status-item"> <div class="status-item">
@@ -622,6 +59,10 @@
<main> <main>
<div class="log-error" id="log-error" role="alert" hidden>
⚠ LOG UNAVAILABLE — The logbook could not be loaded. Try again later.
</div>
<!-- DASHBOARD --> <!-- DASHBOARD -->
<div id="section-dashboard" class="section active"> <div id="section-dashboard" class="section active">
<div class="dash-grid"> <div class="dash-grid">
@@ -776,306 +217,6 @@
LOGBOOK GENERATED FROM ADIF EXPORT LOGBOOK GENERATED FROM ADIF EXPORT
</footer> </footer>
<script> <script src="js/k5bss.js"></script>
// ─── DATA ───────────────────────────────────────────────────
const RAW_DATA = [{"call":"WE2M","name":"ROBERT A HOGUE","date":"2021-01-24","time":"0553","band":"40M","mode":"FT8","rst_sent":"-20","rst_rcvd":"-10","country":"United States","state":"WA","qth":"E WENATCHEE","lotw":"Y","dist":"2782","grid":"CN97UJ"},{"call":"WD8AXA","name":"CHARLES D AGOSTI","date":"2021-01-24","time":"0555","band":"40M","mode":"FT8","rst_sent":"-07","rst_rcvd":"-11","country":"United States","state":"MI","qth":"PORTAGE","lotw":"N","dist":"1291","grid":"EN72ef"},{"call":"KG4IXS","name":"RICHARD A VAUGHAN","date":"2021-01-24","time":"0558","band":"40M","mode":"FT8","rst_sent":"+00","rst_rcvd":"-13","country":"United States","state":"VA","qth":"CHATHAM","lotw":"Y","dist":"1390","grid":"FM06HU"},{"call":"KE0DC","name":"","date":"2021-01-24","time":"0600","band":"40M","mode":"FT8","rst_sent":"-08","rst_rcvd":"-09","country":"United States","state":"CO","qth":"AURORA","lotw":"Y","dist":"1274","grid":"DM79OQ"},{"call":"KC0HHN","name":"","date":"2021-01-24","time":"0604","band":"40M","mode":"FT8","rst_sent":"-12","rst_rcvd":"-08","country":"United States","state":"IA","qth":"","lotw":"N","dist":"","grid":"EN31"},{"call":"WB0OEW","name":"JAMES D VOGT","date":"2021-01-24","time":"0606","band":"40M","mode":"FT8","rst_sent":"-10","rst_rcvd":"-15","country":"United States","state":"MO","qth":"CAMDENTON","lotw":"Y","dist":"747","grid":"EM37TH"},{"call":"N4JKO","name":"GARY E HORTON","date":"2021-01-24","time":"0608","band":"40M","mode":"FT8","rst_sent":"-12","rst_rcvd":"-15","country":"United States","state":"TN","qth":"","lotw":"N","dist":"","grid":"EM65RT"},{"call":"WW4LL","name":"LESTER P LEEPER","date":"2021-01-24","time":"0610","band":"40M","mode":"FT8","rst_sent":"-14","rst_rcvd":"-13","country":"United States","state":"GA","qth":"BOWDON","lotw":"Y","dist":"575","grid":"EM73DS"},{"call":"K0MBT","name":"MICHAEL B TEDESCO","date":"2021-01-24","time":"0612","band":"40M","mode":"FT8","rst_sent":"-09","rst_rcvd":"-18","country":"United States","state":"MN","qth":"SHAKOPEE","lotw":"Y","dist":"1247","grid":"EN34HE"},{"call":"WB4LHD","name":"ROY A MCDANIEL","date":"2021-01-24","time":"0618","band":"40M","mode":"FT8","rst_sent":"-14","rst_rcvd":"-12","country":"United States","state":"NC","qth":"SANFORD","lotw":"Y","dist":"1118","grid":"FM03HF"},{"call":"N0KJA","name":"FRANK O KORANDA","date":"2021-01-24","time":"0620","band":"40M","mode":"FT8","rst_sent":"-10","rst_rcvd":"-14","country":"United States","state":"KS","qth":"WICHITA","lotw":"Y","dist":"641","grid":"EM17PT"},{"call":"W9JML","name":"JAMES M LAURENDEAU","date":"2021-01-24","time":"0623","band":"40M","mode":"FT8","rst_sent":"-05","rst_rcvd":"-16","country":"United States","state":"IL","qth":"COLLINSVILLE","lotw":"Y","dist":"668","grid":"EM58GI"},{"call":"K9ADV","name":"DWIGHT A VANDERLINDEN","date":"2021-01-24","time":"0625","band":"40M","mode":"FT8","rst_sent":"-11","rst_rcvd":"-13","country":"United States","state":"IN","qth":"INDIANAPOLIS","lotw":"Y","dist":"929","grid":"EM79MF"},{"call":"WB8YYY","name":"ERNEST E MUENZER","date":"2021-01-24","time":"0628","band":"40M","mode":"FT8","rst_sent":"-10","rst_rcvd":"-05","country":"United States","state":"MI","qth":"","lotw":"Y","dist":"","grid":"EN72"},{"call":"KD2UKS","name":"RAYMOND JOSEPH MCDERMOTT","date":"2021-01-24","time":"0641","band":"40M","mode":"FT8","rst_sent":"-12","rst_rcvd":"-15","country":"United States","state":"NY","qth":"MASTIC","lotw":"N","dist":"1561","grid":"FN30HT"},{"call":"KD8IYI","name":"JEFFREY C WHITLEY","date":"2021-01-24","time":"0644","band":"40M","mode":"FT8","rst_sent":"-13","rst_rcvd":"-13","country":"United States","state":"OH","qth":"","lotw":"Y","dist":"","grid":"EM89LG"},{"call":"K9DW","name":"DAN WELBORN","date":"2021-01-24","time":"0647","band":"40M","mode":"FT8","rst_sent":"-09","rst_rcvd":"-06","country":"United States","state":"AL","qth":"DOTHAN","lotw":"Y","dist":"435","grid":"EM64SA"},{"call":"K8DO","name":"DEAN L OSTERMAN","date":"2021-01-24","time":"0649","band":"40M","mode":"FT8","rst_sent":"-11","rst_rcvd":"-10","country":"United States","state":"MI","qth":"ALLENDALE","lotw":"Y","dist":"1298","grid":"EN62OX"},{"call":"K6VMV","name":"VICTOR M VALADEZ","date":"2021-01-24","time":"0651","band":"40M","mode":"FT8","rst_sent":"-08","rst_rcvd":"-12","country":"United States","state":"CA","qth":"MANTECA","lotw":"N","dist":"1954","grid":"CM97KC"},{"call":"W0ULF","name":"MIKE WOLFE","date":"2021-01-24","time":"0653","band":"40M","mode":"FT8","rst_sent":"-11","rst_rcvd":"-13","country":"United States","state":"NE","qth":"OMAHA","lotw":"Y","dist":"1012","grid":"EN21WC"}];
// We'll load more data dynamically
let ALL_DATA = [];
let filteredData = [];
let sortKey = 'date';
let sortDir = -1; // -1 = desc
let currentPage = 1;
const PAGE_SIZE = 50;
// ─── FULL DATA EMBEDDED ──────────────────────────────────────
// Data will be injected here
const FULL_LOG = [{"call":"WE2M","name":"ROBERT A HOGUE","date":"2021-01-24","time":"0553","band":"40M","mode":"FT8","rst_sent":"-20","rst_rcvd":"-10","country":"United States","state":"WA","qth":"E WENATCHEE","lotw":"Y","dist":"2782","grid":"CN97UJ"},{"call":"WD8AXA","name":"CHARLES D AGOSTI","date":"2021-01-24","time":"0555","band":"40M","mode":"FT8","rst_sent":"-07","rst_rcvd":"-11","country":"United States","state":"MI","qth":"PORTAGE","lotw":"N","dist":"1291","grid":"EN72ef"},{"call":"KG4IXS","name":"RICHARD A VAUGHAN","date":"2021-01-24","time":"0558","band":"40M","mode":"FT8","rst_sent":"+00","rst_rcvd":"-13","country":"United States","state":"VA","qth":"CHATHAM","lotw":"Y","dist":"1390","grid":"FM06HU"},{"call":"KE0DC","name":"DOUGLAS J COMER","date":"2021-01-24","time":"0600","band":"40M","mode":"FT8","rst_sent":"-08","rst_rcvd":"-09","country":"United States","state":"CO","qth":"AURORA","lotw":"Y","dist":"1274","grid":"DM79OQ"},{"call":"KC2DUX","name":"John W Hamann, Jr","date":"2021-01-24","time":"0608","band":"40M","mode":"FT8","rst_sent":"-11","rst_rcvd":"-11","country":"United States","state":"NJ","qth":"Dennisville","lotw":"N","dist":"1850","grid":"FM29oe"},{"call":"N3XLS","name":"Joseph C Raymer","date":"2021-01-24","time":"0613","band":"40M","mode":"FT8","rst_sent":"-07","rst_rcvd":"-15","country":"United States","state":"PA","qth":"North Abington Twp","lotw":"Y","dist":"1880","grid":"FN21DN"},{"call":"HK1O","name":"Juan Guillermo Ochoa P","date":"2021-01-24","time":"0617","band":"40M","mode":"FT8","rst_sent":"-01","rst_rcvd":"-15","country":"Colombia","state":"","qth":"Puerto Colombia","lotw":"Y","dist":"3071","grid":"FK21NA"},{"call":"WJ9E","name":"Michael D Elder","date":"2021-01-24","time":"0645","band":"40M","mode":"FT8","rst_sent":"+05","rst_rcvd":"-14","country":"United States","state":"IN","qth":"Rushville","lotw":"Y","dist":"1081","grid":"EM79GO"},{"call":"K8TE","name":"WILLIAM A MADER, JR","date":"2021-01-24","time":"0647","band":"40M","mode":"FT8","rst_sent":"+06","rst_rcvd":"-05","country":"United States","state":"NM","qth":"RIO RANCHO","lotw":"Y","dist":"1230","grid":"DM65QG"},{"call":"N1UL","name":"ULRICH L ROHDE, PhD","date":"2021-01-24","time":"0703","band":"40M","mode":"FT8","rst_sent":"-08","rst_rcvd":"-11","country":"United States","state":"FL","qth":"MARCO ISLAND","lotw":"Y","dist":"1373","grid":"EL95DW"},{"call":"K9IHM","name":"JERROLD L JOHNSON","date":"2021-01-24","time":"0708","band":"40M","mode":"FT8","rst_sent":"+11","rst_rcvd":"+07","country":"United States","state":"TN","qth":"NEW MARKET","lotw":"Y","dist":"1010","grid":"EM86FC"},{"call":"WB7WUQ","name":"Malcolm A Clark","date":"2021-01-24","time":"0710","band":"40M","mode":"FT8","rst_sent":"-11","rst_rcvd":"-10","country":"United States","state":"WA","qth":"Inchelium","lotw":"Y","dist":"2714","grid":"DN08RG"},{"call":"N6AKO","name":"Luis Dimagiba","date":"2021-01-24","time":"0717","band":"40M","mode":"FT8","rst_sent":"-13","rst_rcvd":"+05","country":"United States","state":"CA","qth":"Arleta","lotw":"Y","dist":"2299","grid":"DM04SG"},{"call":"KC2DUX","name":"John W Hamann, Jr","date":"2021-01-24","time":"0719","band":"40M","mode":"FT8","rst_sent":"-01","rst_rcvd":"-04","country":"United States","state":"NJ","qth":"Dennisville","lotw":"Y","dist":"1850","grid":"FM29OE"},{"call":"KA9FOX","name":"SCOTT E NEADER","date":"2021-01-24","time":"0721","band":"40M","mode":"FT8","rst_sent":"+04","rst_rcvd":"+03","country":"United States","state":"WI","qth":"LA CROSSE","lotw":"Y","dist":"1271","grid":"EN43KS"},{"call":"KI3N","name":"Kenneth A Yates","date":"2021-01-24","time":"0723","band":"40M","mode":"FT8","rst_sent":"+00","rst_rcvd":"-03","country":"United States","state":"AL","qth":"Oxford","lotw":"Y","dist":"742","grid":"EM73CN"},{"call":"KA4NIV","name":"MAURICE E RICE, JR","date":"2021-01-24","time":"0725","band":"40M","mode":"FT8","rst_sent":"-07","rst_rcvd":"+07","country":"United States","state":"SC","qth":"SIMPSONVILLE","lotw":"Y","dist":"1087","grid":"EM84UT"},{"call":"KB1EFS","name":"Steven J Henchy","date":"2021-01-24","time":"0727","band":"40M","mode":"FT8","rst_sent":"-07","rst_rcvd":"-06","country":"United States","state":"MA","qth":"Franklin","lotw":"Y","dist":"2230","grid":"FN42HC"},{"call":"NR5G","name":"LEE E SWANSON","date":"2021-01-24","time":"0728","band":"40M","mode":"FT8","rst_sent":"+01","rst_rcvd":"-06","country":"United States","state":"GA","qth":"WARNER ROBINS","lotw":"N","dist":"943","grid":"EM82ep"},{"call":"KO4ARA","name":"David W Hunt, Jr","date":"2021-01-24","time":"0733","band":"40M","mode":"FT8","rst_sent":"-09","rst_rcvd":"-05","country":"United States","state":"VA","qth":"Annandale","lotw":"Y","dist":"1642","grid":"FM18JT"},{"call":"N7FN","name":"FRANKLIN B NORAGON","date":"2021-01-26","time":"1334","band":"20M","mode":"FT8","rst_sent":"+03","rst_rcvd":"-09","country":"United States","state":"AZ","qth":"Buckeye","lotw":"N","dist":"1765","grid":"DM33qq"},{"call":"N0DOW","name":"Richard WLine truncated
function init() {
ALL_DATA = FULL_LOG.length ? FULL_LOG : RAW_DATA;
filteredData = [...ALL_DATA];
// Update stats
const countries = new Set(ALL_DATA.map(r => r.country).filter(Boolean));
const states = new Set(ALL_DATA.filter(r => r.country === 'United States').map(r => r.state).filter(Boolean));
document.getElementById('stat-dxcc').textContent = countries.size;
document.getElementById('stat-states').textContent = states.size;
buildDashboard();
populateFilters();
renderTable();
}
// ─── DASHBOARD ───────────────────────────────────────────────
function buildDashboard() {
// Modes
const modes = countBy(ALL_DATA, 'mode');
const topModes = Object.entries(modes).sort((a,b)=>b[1]-a[1]).slice(0,7);
const maxMode = topModes[0][1];
const modesEl = document.getElementById('modes-chart');
const modeColors = { FT8:'#ffaa00', FT4:'#ffcc44', DIG:'#ff8800', USB:'#4499ff', LSB:'#66aaff', SSB:'#88bbff', RTTY:'#ff4455' };
modesEl.innerHTML = topModes.map(([m, c]) => `
<div class="bar-row">
<div class="bar-label">${m}</div>
<div class="bar-track"><div class="bar-fill" style="width:0%;background:${modeColors[m]||'#888'}" data-target="${(c/maxMode*100).toFixed(1)}"></div></div>
<div class="bar-count">${c}</div>
</div>`).join('');
// Bands
const bands = countBy(ALL_DATA, 'band');
const topBands = Object.entries(bands).sort((a,b)=>b[1]-a[1]).slice(0,8);
const maxBand = topBands[0][1];
const bandEl = document.getElementById('bands-chart');
const bandColors = { '20M':'#4499ff','40M':'#00ff88','80M':'#ff4455','10M':'#00ddff','30M':'#88aaff','17M':'#aaddff','15M':'#ffaaff','12M':'#ffdd88' };
bandEl.innerHTML = topBands.map(([b, c]) => `
<div class="bar-row">
<div class="bar-label">${b}</div>
<div class="bar-track"><div class="bar-fill" style="width:0%;background:${bandColors[b]||'#888'}" data-target="${(c/maxBand*100).toFixed(1)}"></div></div>
<div class="bar-count">${c}</div>
</div>`).join('');
// Countries
const countries = countBy(ALL_DATA, 'country');
const topCountries = Object.entries(countries).sort((a,b)=>b[1]-a[1]).slice(0,20);
const ctEl = document.getElementById('countries-list');
ctEl.innerHTML = topCountries.map(([c,n]) => `
<div class="country-row">
<span class="country-name">${c||'Unknown'}</span>
<span class="country-count">${n}</span>
</div>`).join('');
// Animate bars after a tick
requestAnimationFrame(() => {
setTimeout(() => {
document.querySelectorAll('.bar-fill').forEach(el => {
el.style.width = el.dataset.target + '%';
});
}, 100);
});
// Activity chart
drawActivityChart();
}
function drawActivityChart() {
const canvas = document.getElementById('activityCanvas');
const dpr = window.devicePixelRatio || 1;
const W = canvas.parentElement.offsetWidth - 48;
const H = 120;
canvas.width = W * dpr;
canvas.height = H * dpr;
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
// Group by year-month
const monthly = {};
ALL_DATA.forEach(r => {
if (!r.date) return;
const key = r.date.slice(0,7);
monthly[key] = (monthly[key]||0) + 1;
});
const keys = Object.keys(monthly).sort();
const vals = keys.map(k => monthly[k]);
const max = Math.max(...vals);
const pad = { l: 10, r: 10, t: 15, b: 30 };
const chartW = W - pad.l - pad.r;
const chartH = H - pad.t - pad.b;
const barW = Math.max(2, (chartW / keys.length) - 1);
ctx.clearRect(0,0,W,H);
// Grid line
ctx.strokeStyle = 'rgba(30,37,48,0.8)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(pad.l, pad.t + chartH);
ctx.lineTo(W - pad.r, pad.t + chartH);
ctx.stroke();
// Bars
keys.forEach((k, i) => {
const x = pad.l + i * (chartW / keys.length);
const h = (vals[i] / max) * chartH;
const y = pad.t + chartH - h;
const grad = ctx.createLinearGradient(0, y, 0, y + h);
grad.addColorStop(0, 'rgba(0,255,136,0.9)');
grad.addColorStop(1, 'rgba(0,255,136,0.15)');
ctx.fillStyle = grad;
ctx.fillRect(x, y, barW, h);
});
// X labels (every 6 months)
ctx.fillStyle = 'rgba(90,112,128,0.9)';
ctx.font = `${9 * dpr / dpr}px 'Share Tech Mono', monospace`;
ctx.textAlign = 'center';
keys.forEach((k, i) => {
if (i % 6 === 0) {
const x = pad.l + i * (chartW / keys.length) + barW/2;
ctx.fillText(k, x, H - 8);
}
});
}
// ─── TABLE ───────────────────────────────────────────────────
function populateFilters() {
const bands = [...new Set(ALL_DATA.map(r=>r.band).filter(Boolean))].sort();
const modes = [...new Set(ALL_DATA.map(r=>r.mode).filter(Boolean))].sort();
const bandSel = document.getElementById('filter-band');
const modeSel = document.getElementById('filter-mode');
bands.forEach(b => { const o = document.createElement('option'); o.value=b; o.text=b; bandSel.appendChild(o); });
modes.forEach(m => { const o = document.createElement('option'); o.value=m; o.text=m; modeSel.appendChild(o); });
}
function applyFilters() {
const q = document.getElementById('search-input').value.trim().toLowerCase();
const band = document.getElementById('filter-band').value;
const mode = document.getElementById('filter-mode').value;
const lotw = document.getElementById('filter-lotw').value;
filteredData = ALL_DATA.filter(r => {
if (band && r.band !== band) return false;
if (mode && r.mode !== mode) return false;
if (lotw && r.lotw !== lotw) return false;
if (q) {
const hay = [r.call, r.name, r.country, r.qth, r.state, r.grid].join(' ').toLowerCase();
if (!hay.includes(q)) return false;
}
return true;
});
currentPage = 1;
renderTable();
}
function sortTable(key) {
if (sortKey === key) sortDir *= -1;
else { sortKey = key; sortDir = key === 'date' || key === 'time' ? -1 : 1; }
filteredData.sort((a,b) => {
let av = a[key]||'', bv = b[key]||'';
if (key === 'dist') { av = parseInt(av)||0; bv = parseInt(bv)||0; }
if (av < bv) return -sortDir;
if (av > bv) return sortDir;
return 0;
});
// Update header indicators
document.querySelectorAll('thead th').forEach(th => {
th.classList.remove('sort-asc','sort-desc');
});
const headers = ['call','name','date','time','band','mode','rst_sent','country','dist','lotw'];
const idx = headers.indexOf(key);
if (idx >= 0) {
const ths = document.querySelectorAll('thead th');
ths[idx].classList.add(sortDir === 1 ? 'sort-asc' : 'sort-desc');
}
renderTable();
}
function renderTable() {
const tbody = document.getElementById('log-tbody');
const total = filteredData.length;
const pages = Math.ceil(total / PAGE_SIZE);
const start = (currentPage - 1) * PAGE_SIZE;
const slice = filteredData.slice(start, start + PAGE_SIZE);
document.getElementById('log-meta').textContent =
`${total.toLocaleString()} QSOs · Page ${currentPage} of ${Math.max(1,pages)}`;
if (slice.length === 0) {
tbody.innerHTML = '<tr><td colspan="10" style="text-align:center;padding:2rem;color:var(--muted);font-family:var(--font-mono);font-size:0.8rem;">NO CONTACTS FOUND</td></tr>';
} else {
tbody.innerHTML = slice.map(r => {
const timeStr = r.time ? r.time.slice(0,2)+':'+r.time.slice(2,4) : '';
const bandClass = `band-${r.band}`;
const lotwClass = r.lotw === 'Y' ? 'lotw-y' : 'lotw-n';
const lotwSym = r.lotw === 'Y' ? '✓' : '·';
const rst = r.rst_sent && r.rst_rcvd ? `${r.rst_sent} / ${r.rst_rcvd}` : r.rst_sent || '';
return `<tr>
<td class="call-cell">${esc(r.call)}</td>
<td>${esc(r.name ? titleCase(r.name) : '')}</td>
<td>${esc(r.date)}</td>
<td>${timeStr}Z</td>
<td><span class="band-badge ${bandClass}">${esc(r.band)}</span></td>
<td class="mode-cell">${esc(r.mode)}</td>
<td class="rst-cell">${esc(rst)}</td>
<td>${esc(r.country||'')}${r.state ? ' <span style="color:var(--muted)">'+esc(r.state)+'</span>' : ''}</td>
<td class="dist-cell">${r.dist ? Number(r.dist).toLocaleString() : ''}</td>
<td class="${lotwClass}" style="text-align:center">${lotwSym}</td>
</tr>`;
}).join('');
}
renderPagination(pages);
}
function renderPagination(pages) {
const el = document.getElementById('pagination');
if (pages <= 1) { el.innerHTML = ''; return; }
let html = `<button class="page-btn" onclick="goPage(${currentPage-1})" ${currentPage===1?'disabled':''}>‹ Prev</button>`;
const range = [];
if (pages <= 7) {
for (let i=1;i<=pages;i++) range.push(i);
} else {
range.push(1);
if (currentPage > 3) range.push('...');
for (let i=Math.max(2,currentPage-1); i<=Math.min(pages-1,currentPage+1); i++) range.push(i);
if (currentPage < pages-2) range.push('...');
range.push(pages);
}
range.forEach(p => {
if (p === '...') html += `<span class="page-info">…</span>`;
else html += `<button class="page-btn ${p===currentPage?'active':''}" onclick="goPage(${p})">${p}</button>`;
});
html += `<button class="page-btn" onclick="goPage(${currentPage+1})" ${currentPage===pages?'disabled':''}>Next ›</button>`;
el.innerHTML = html;
}
function goPage(p) {
currentPage = p;
renderTable();
document.querySelector('.table-wrap').scrollIntoView({behavior:'smooth',block:'nearest'});
}
// ─── HELPERS ─────────────────────────────────────────────────
function countBy(arr, key) {
return arr.reduce((acc, r) => {
const v = r[key];
if (v) acc[v] = (acc[v]||0)+1;
return acc;
}, {});
}
function esc(s) {
return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function titleCase(s) {
return s.toLowerCase().replace(/\b\w/g, c => c.toUpperCase());
}
function showSection(id, btn) {
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
document.getElementById('section-' + id).classList.add('active');
btn.classList.add('active');
}
window.addEventListener('load', init);
window.addEventListener('resize', () => {
if (document.getElementById('section-dashboard').classList.contains('active')) {
drawActivityChart();
}
});
</script>
</body> </body>
</html> </html>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://k5bss.com/sitemap.xml
Executable → Regular
View File
File mode changed.
Executable → Regular
View File
File mode changed.