Skip to content

Commit 6fbe129

Browse files
authored
Update ksc-timestamps.js
1 parent fd55354 commit 6fbe129

1 file changed

Lines changed: 237 additions & 0 deletions

File tree

ksc/assets/ksc-timestamps.js

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,238 @@
1+
/**
2+
* ksc-timestamps.js
3+
* KSC Triple-Seal Timestamp System
4+
* Owner: Rabbi Dr. Yonatan Blum
5+
* Changes require Red Team signoff before deployment.
6+
* Deploy to: /ksc/assets/ksc-timestamps.js
7+
*
8+
* Three calendars. One sealed moment. Every page carries all three.
9+
* Hebrew date: live fetch from hebcal.com — never calculated, never cached stale.
10+
* Dreamspell: local calculation — cannot fail.
11+
* Gregorian: browser-native.
12+
*
13+
* Certified by Yonatan Blum against Dreamspell calendar standard.
14+
*/
15+
16+
(function () {
17+
'use strict';
18+
19+
/* ── DREAMSPELL MOON NAMES ── */
20+
const MOON_NAMES = [
21+
'', // index 0 unused
22+
'Magnetic', // Moon 1
23+
'Lunar', // Moon 2
24+
'Electric', // Moon 3
25+
'Self-Existing', // Moon 4
26+
'Overtone', // Moon 5
27+
'Rhythmic', // Moon 6
28+
'Resonant', // Moon 7
29+
'Galactic', // Moon 8
30+
'Solar', // Moon 9
31+
'Planetary', // Moon 10
32+
'Spectral', // Moon 11
33+
'Crystal', // Moon 12
34+
'Cosmic' // Moon 13
35+
];
36+
37+
/**
38+
* Calculate Dreamspell date for a given Gregorian date.
39+
*
40+
* Algorithm (certified by Yonatan Blum):
41+
* - Anchor: Magnetic Moon (Moon 1) begins July 26 each year
42+
* - Each moon: exactly 28 days
43+
* - 13 moons × 28 days = 364 days per Dreamspell year
44+
* - Day Out of Time: July 25 (day 365, not in any moon)
45+
* - Leap years: Feb 29 maps to same position as non-leap March 1 would
46+
* (Dreamspell uses fixed 365-day year — no adjustment needed)
47+
*
48+
* @param {Date} [date] — defaults to current date
49+
* @returns {Object} { moon, moonName, day } or { special: 'Day Out of Time' }
50+
*/
51+
function getDreamspell(date) {
52+
date = date || new Date();
53+
54+
const year = date.getFullYear();
55+
const anchorThisYear = new Date(year, 6, 26); // July 26
56+
const anchor = date >= anchorThisYear
57+
? anchorThisYear
58+
: new Date(year - 1, 6, 26);
59+
60+
// Day Out of Time: July 25 of the current Dreamspell year
61+
const dayOutOfTime = new Date(anchor.getFullYear(), 6, 25);
62+
if (date.toDateString() === dayOutOfTime.toDateString()) {
63+
return { special: 'Day Out of Time' };
64+
}
65+
66+
// Days since Magnetic Moon start (0-indexed)
67+
const daysSince = Math.floor((date - anchor) / 86400000);
68+
69+
const moon = Math.floor(daysSince / 28) + 1; // 1–13
70+
const day = (daysSince % 28) + 1; // 1–28
71+
72+
return {
73+
moon,
74+
moonName: MOON_NAMES[moon] || '?',
75+
day
76+
};
77+
}
78+
79+
/**
80+
* Generate session storage cache key for Hebrew date.
81+
* Includes day/evening period to account for Hebrew day
82+
* beginning at sunset (~18:00 local time approximation).
83+
*
84+
* Note: sunset is approximated at 18:00 local time.
85+
* Hebrew date may briefly reflect prior day until fetch refreshes.
86+
*
87+
* @returns {string} cache key
88+
*/
89+
function getHebCacheKey() {
90+
const now = new Date();
91+
const dateStr = now.toISOString().split('T')[0]; // YYYY-MM-DD
92+
const period = now.getHours() >= 18 ? 'eve' : 'day';
93+
return `ksc-hebrew-${dateStr}-${period}`;
94+
}
95+
96+
/**
97+
* Fetch Hebrew date from hebcal.com.
98+
* Caches in sessionStorage to reduce API calls across page navigations.
99+
* Cache invalidates at sunset transition (18:00 approximation).
100+
*
101+
* @param {Date} [date] — defaults to current date
102+
* @returns {Promise<Object|null>} hebcal JSON or null on failure
103+
*/
104+
async function fetchHebrewDate(date) {
105+
date = date || new Date();
106+
const key = getHebCacheKey();
107+
108+
// Check session cache
109+
try {
110+
const cached = sessionStorage.getItem(key);
111+
if (cached) return JSON.parse(cached);
112+
} catch (e) { /* sessionStorage unavailable — proceed to fetch */ }
113+
114+
// Live fetch
115+
const url = `https://www.hebcal.com/converter?gd=${date.getDate()}&gm=${date.getMonth() + 1}&gy=${date.getFullYear()}&g2h=1&cfg=json`;
116+
117+
try {
118+
const controller = new AbortController();
119+
const timeoutId = setTimeout(() => controller.abort(), 5000);
120+
121+
const response = await fetch(url, { signal: controller.signal });
122+
clearTimeout(timeoutId);
123+
124+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
125+
126+
const data = await response.json();
127+
try { sessionStorage.setItem(key, JSON.stringify(data)); } catch (e) {}
128+
return data;
129+
130+
} catch (e) {
131+
return null; // caller handles failure state
132+
}
133+
}
134+
135+
/**
136+
* Render all three timestamps to the page.
137+
* Targets elements: #ts-gregorian, #ts-hebrew, #ts-dreamspell
138+
* Called on DOMContentLoaded.
139+
*/
140+
async function renderTimestamps() {
141+
const now = new Date();
142+
143+
// ── Gregorian (synchronous) ──
144+
const tsGreg = document.getElementById('ts-gregorian');
145+
if (tsGreg) {
146+
tsGreg.textContent = now.toLocaleDateString('en-US', {
147+
month: 'long',
148+
day: 'numeric',
149+
year: 'numeric'
150+
});
151+
}
152+
153+
// ── Dreamspell (synchronous) ──
154+
const tsDream = document.getElementById('ts-dreamspell');
155+
if (tsDream) {
156+
const ds = getDreamspell(now);
157+
tsDream.textContent = ds.special
158+
? ds.special
159+
: `Day ${ds.day}, ${ds.moonName} Moon ${ds.moon}`;
160+
}
161+
162+
// ── Hebrew (async — live fetch) ──
163+
const tsHeb = document.getElementById('ts-hebrew');
164+
if (tsHeb) {
165+
const data = await fetchHebrewDate(now);
166+
if (data && data.hebrew) {
167+
tsHeb.textContent = data.hebrew;
168+
} else {
169+
tsHeb.textContent = '[date unavailable]';
170+
tsHeb.classList.add('fail');
171+
}
172+
}
173+
}
174+
175+
/**
176+
* Staleness check for manually maintained data nodes.
177+
* After 18 months from build date, adds amber "· verify current" warning
178+
* to source labels on manual nodes (excluding staleness-exempt nodes).
179+
*/
180+
function checkStaleness() {
181+
const meta = document.querySelector('meta[name="ksc-build-date"]');
182+
if (!meta || !meta.content) return;
183+
184+
const buildDate = new Date(meta.content);
185+
const monthsElapsed = (new Date() - buildDate) / (1000 * 60 * 60 * 24 * 30);
186+
187+
if (monthsElapsed > 18) {
188+
document.querySelectorAll(
189+
'[data-manual-node]:not([data-staleness-exempt]) .data-source'
190+
).forEach(el => {
191+
el.textContent += ' · verify current';
192+
el.classList.add('stale');
193+
});
194+
}
195+
}
196+
197+
/**
198+
* Audit log toggle.
199+
* Called by onclick on the Audit Log nav button.
200+
* Declared on window so inline onclick can find it.
201+
*/
202+
window.toggleAudit = function () {
203+
const panel = document.getElementById('audit-panel');
204+
const btn = document.getElementById('audit-toggle');
205+
if (!panel) return;
206+
207+
const open = panel.classList.toggle('visible');
208+
panel.setAttribute('aria-hidden', String(!open));
209+
if (btn) {
210+
btn.setAttribute('aria-expanded', String(open));
211+
btn.classList.toggle('active', open);
212+
}
213+
if (open) {
214+
panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
215+
}
216+
};
217+
218+
/* ── EXPORTS ── */
219+
window.KSCTimestamps = {
220+
getDreamspell,
221+
fetchHebrewDate,
222+
renderTimestamps,
223+
checkStaleness
224+
};
225+
226+
/* ── AUTO-INIT ── */
227+
if (document.readyState === 'loading') {
228+
document.addEventListener('DOMContentLoaded', () => {
229+
renderTimestamps();
230+
checkStaleness();
231+
});
232+
} else {
233+
renderTimestamps();
234+
checkStaleness();
235+
}
236+
237+
})();
1238

0 commit comments

Comments
 (0)