Merge branch 'dev'

This commit is contained in:
Jeff Emmett 2026-03-22 15:10:47 -07:00
commit 637174b0cb
4 changed files with 123 additions and 26 deletions

View File

@ -42,21 +42,10 @@ services:
- SMTP_HOST=${SMTP_HOST:-mailcowdockerized-postfix-mailcow-1}
- SMTP_PORT=${SMTP_PORT:-587}
- SMTP_USER=${SMTP_USER:-noreply@rmail.online}
- SMTP_PASS=${SMTP_PASS}
- SITE_URL=https://rspace.online
- RTASKS_REPO_BASE=/repos
- RTASKS_HMAC_SECRET=${RTASKS_HMAC_SECRET}
- RTASKS_API_KEY=${RTASKS_API_KEY}
- SPLAT_NOTIFY_EMAIL=jeffemmett@gmail.com
- TWENTY_API_URL=http://twenty-ch-server:3000
- TWENTY_API_TOKEN=${TWENTY_API_TOKEN:-}
- TRANSAK_API_KEY=${TRANSAK_API_KEY:-}
- TRANSAK_API_KEY_STAGING=${TRANSAK_API_KEY_STAGING:-}
- TRANSAK_API_KEY_PRODUCTION=${TRANSAK_API_KEY_PRODUCTION:-}
- TRANSAK_SECRET=${TRANSAK_SECRET:-}
- TRANSAK_WEBHOOK_SECRET_STAGING=${TRANSAK_WEBHOOK_SECRET_STAGING:-}
- TRANSAK_WEBHOOK_SECRET_PRODUCTION=${TRANSAK_WEBHOOK_SECRET_PRODUCTION:-}
- TRANSAK_ENV=${TRANSAK_ENV:-STAGING}
- OLLAMA_URL=http://ollama:11434
- INFISICAL_AI_CLIENT_ID=${INFISICAL_AI_CLIENT_ID}
- INFISICAL_AI_CLIENT_SECRET=${INFISICAL_AI_CLIENT_SECRET}

View File

@ -741,8 +741,12 @@ class FolkCalendarView extends HTMLElement {
}
private getEventsForDate(dateStr: string): any[] {
return this.events.filter(e => e.start_time && e.start_time.startsWith(dateStr)
&& !this.filteredSources.has(e.source_name));
return this.events.filter(e => {
if (!e.start_time || this.filteredSources.has(e.source_name)) return false;
const startDay = e.start_time.slice(0, 10);
const endDay = e.end_time ? e.end_time.slice(0, 10) : startDay;
return dateStr >= startDay && dateStr <= endDay;
});
}
private getRemindersForDate(dateStr: string): any[] {
@ -870,10 +874,6 @@ class FolkCalendarView extends HTMLElement {
<button class="nav-btn" id="btn-tour" style="margin-left:auto;font-size:0.78rem">Tour</button>
</div>
${this.renderSources()}
<div class="zoom-bar--top">${this.renderZoomController()}</div>
<div class="main-layout ${isDocked ? "main-layout--docked" : ""}" id="main-layout"${isDocked ? ` style="grid-template-columns: 1fr 6px ${this.mapPanelWidth}px"` : ""}>
<div class="calendar-pane" id="calendar-pane">
${this.renderCalendarContent()}
@ -882,6 +882,10 @@ class FolkCalendarView extends HTMLElement {
${this.renderMapPanel()}
</div>
${this.renderSources()}
<div class="zoom-bar--top">${this.renderZoomController()}</div>
<div class="bottom-bar">
${this.renderLunarOverlay()}
<button class="bottom-bar__lunar-toggle ${this.showLunar ? "active" : ""}" id="toggle-lunar" title="Toggle lunar phases (l)">\u{1F319}</button>
@ -1068,6 +1072,7 @@ class FolkCalendarView extends HTMLElement {
private renderSources(): string {
if (this.sources.length === 0) return "";
return `<div class="sources">
<div class="sources-heading">Calendar Legend</div>
${this.sources.map(s => `<span class="src-badge ${this.filteredSources.has(s.name) ? "filtered" : ""}"
data-source="${this.esc(s.name)}"
style="border-color:${s.color || "#666"};color:${s.color || "#aaa"}">${this.esc(s.name)}</span>`).join("")}
@ -1193,6 +1198,82 @@ class FolkCalendarView extends HTMLElement {
html += this.renderDayDetail(this.expandedDay, dayEvents);
}
// Multi-day event span bars
html += this.renderMultiDaySpans(year, month, firstDay, daysInMonth);
return html;
}
private renderMultiDaySpans(year: number, month: number, firstDay: number, daysInMonth: number): string {
const mm = String(month + 1).padStart(2, "0");
const monthStart = `${year}-${mm}-01`;
const monthEnd = `${year}-${mm}-${String(daysInMonth).padStart(2, "0")}`;
const multiDay = this.events.filter(e => {
if (!e.start_time || !e.end_time || this.filteredSources.has(e.source_name)) return false;
const sd = e.start_time.slice(0, 10);
const ed = e.end_time.slice(0, 10);
return ed > sd && ed >= monthStart && sd <= monthEnd;
});
if (multiDay.length === 0) return "";
const totalCells = firstDay + daysInMonth + ((7 - ((firstDay + daysInMonth) % 7)) % 7);
const totalRows = totalCells / 7;
let html = "";
// Helper: date string → cell index (clamped to grid)
const dateToCellIdx = (ds: string): number => {
const d = new Date(ds);
if (d.getFullYear() === year && d.getMonth() === month) return firstDay + d.getDate() - 1;
if (d < new Date(year, month, 1)) return 0;
return totalCells - 1;
};
for (let row = 0; row < totalRows; row++) {
const rowStart = row * 7;
const rowEnd = rowStart + 6;
const rowEvents: { ev: any; c0: number; c1: number }[] = [];
for (const e of multiDay) {
const sc = Math.max(dateToCellIdx(e.start_time.slice(0, 10)), rowStart);
const ec = Math.min(dateToCellIdx(e.end_time.slice(0, 10)), rowEnd);
if (sc > rowEnd || ec < rowStart) continue;
rowEvents.push({ ev: e, c0: sc - rowStart, c1: ec - rowStart });
}
if (rowEvents.length === 0) continue;
// Assign lanes (max 2 visible)
const MAX_LANES = 2;
const lanes: typeof rowEvents[] = [];
let overflow = 0;
for (const re of rowEvents) {
let placed = false;
for (let l = 0; l < MAX_LANES; l++) {
if (!lanes[l]) lanes[l] = [];
if (!lanes[l].some(x => re.c0 <= x.c1 && re.c1 >= x.c0)) {
lanes[l].push(re); placed = true; break;
}
}
if (!placed) overflow++;
}
const gridRow = row + 1;
for (let l = 0; l < lanes.length; l++) {
for (const re of lanes[l]) {
const color = re.ev.source_color || "#6366f1";
const realStart = dateToCellIdx(re.ev.start_time.slice(0, 10));
const realEnd = dateToCellIdx(re.ev.end_time.slice(0, 10));
const contL = realStart < rowStart;
const contR = realEnd > rowEnd;
const rL = contL ? "0" : "4px";
const rR = contR ? "0" : "4px";
html += `<div class="ev-span" style="grid-row:${gridRow};grid-column:${re.c0 + 1}/${re.c1 + 2};margin-top:${22 + l * 18}px;background:${color}25;border-left:2px solid ${color};border-radius:${rL} ${rR} ${rR} ${rL}" data-event-id="${re.ev.id}" title="${this.esc(re.ev.title)}"><span class="ev-span-text" style="color:${color}">${this.esc(re.ev.title)}</span></div>`;
}
}
if (overflow > 0) {
html += `<div class="ev-span-more" style="grid-row:${gridRow};grid-column:1;margin-top:${22 + MAX_LANES * 18}px">+${overflow} more</div>`;
}
}
return html;
}
@ -2604,14 +2685,15 @@ class FolkCalendarView extends HTMLElement {
.zoom-bar__section--coupled .zoom-bar__spatial-track { opacity: 0.5; pointer-events: none; }
.zoom-bar__section--coupled .zoom-bar__spatial-thumb { cursor: default; }
/* ── Sources ── */
.sources { display: flex; gap: 6px; margin-bottom: 10px; flex-wrap: wrap; }
/* ── Sources / Calendar Legend ── */
.sources { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; align-items: center; }
.sources-heading { font-size: 9px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--rs-text-muted); margin-right: 4px; }
.src-badge { font-size: 10px; padding: 3px 8px; border-radius: 10px; border: 1px solid var(--rs-border-strong); cursor: pointer; transition: opacity 0.15s; user-select: none; }
.src-badge:hover { filter: brightness(1.2); }
.src-badge.filtered { opacity: 0.3; text-decoration: line-through; }
/* ── Zoom Bar Top ── */
.zoom-bar--top { margin-bottom: 10px; padding: 8px 12px; background: var(--rs-bg-surface); border: 1px solid var(--rs-border-subtle); border-radius: 8px; }
.zoom-bar--top { margin-top: 10px; padding: 8px 12px; background: var(--rs-bg-surface); border: 1px solid var(--rs-border-subtle); border-radius: 8px; }
.zoom-bar--top .zoom-bar__track { min-width: 200px; }
.zoom-bar--top .zoom-bar__row { gap: 12px; }
.zoom-bar--top .zoom-bar__tick-label { font-size: 10px; }
@ -2671,6 +2753,12 @@ class FolkCalendarView extends HTMLElement {
.dd-likelihood { font-size: 9px; color: var(--rs-warning); margin-left: 6px; }
.dot--tentative { border: 1px dashed; background: transparent !important; width: 6px; height: 6px; border-radius: 50%; display: inline-block; margin: 1px; }
/* ── Multi-day Span Bars ── */
.ev-span { height: 16px; font-size: 10px; line-height: 16px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0 4px; cursor: pointer; z-index: 2; align-self: start; pointer-events: auto; }
.ev-span:hover { filter: brightness(1.15); }
.ev-span-text { font-weight: 500; }
.ev-span-more { height: 14px; font-size: 9px; line-height: 14px; color: var(--rs-text-muted); z-index: 2; align-self: start; pointer-events: none; padding: 0 4px; }
/* ── Drop Target ── */
.day.drop-target { background: rgba(245,158,11,0.15); border: 2px dashed var(--rs-warning); }
@ -2958,7 +3046,7 @@ class FolkCalendarView extends HTMLElement {
.wd { font-size: 10px; padding: 3px; }
.season-cities, .week-event-meta, .tl-event-desc, .yv-country { display: none; }
.week-view { font-size: 10px; }
.zoom-bar--top { padding: 6px 8px; margin-bottom: 6px; }
.zoom-bar--top { padding: 6px 8px; margin-top: 6px; }
.zoom-bar--top .zoom-bar__track { min-width: 120px; }
.zoom-bar__track { min-width: 120px; }
.zoom-bar__label-end { font-size: 9px; }

View File

@ -431,7 +431,7 @@ export class RStackIdentity extends HTMLElement {
this.#render();
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true }));
} else if (action === "my-account") {
this.#showAccountModal();
this.showAccountModal();
} else if (action === "my-spaces") {
this.#showSpacesModal();
} else if (action === "my-wallets") {
@ -488,6 +488,11 @@ export class RStackIdentity extends HTMLElement {
}
}
/** Public: check if user has an active session */
isSignedIn(): boolean {
return getSession() !== null;
}
/** Public method: show the auth modal programmatically */
showAuthModal(callbacks?: { onSuccess?: () => void; onCancel?: () => void }): void {
if (document.querySelector(".rstack-auth-overlay")) return;
@ -695,7 +700,7 @@ export class RStackIdentity extends HTMLElement {
// ── Account modal (consolidated) ──
#showAccountModal(): void {
showAccountModal(): void {
if (document.querySelector(".rstack-account-overlay")) return;
const overlay = document.createElement("div");
overlay.className = "rstack-account-overlay";

View File

@ -2983,10 +2983,25 @@
{ target: 'rstack-space-switcher', title: 'Spaces', msg: 'Each space is a self-contained community with its own data, members, and encryption. Switch between spaces or create a new one here.' },
{ target: 'rstack-identity', title: 'Passwordless Identity', msg: 'Sign in with <strong>passkeys</strong> \u2014 no passwords or seed phrases. Your identity is cryptographic and portable across all spaces.' },
{ target: '#canvas', title: 'The Canvas', msg: 'Drag to pan, scroll to zoom. Click any shape to interact with it. Connect shapes with arrows by dragging between ports. Everything syncs in real-time.' },
{ target: '#bottom-toolbar', title: 'Drawing Tools', msg: 'Select, draw, and erase on the canvas. Switch between tools here, or use keyboard shortcuts (V for select, P for pen, E for eraser).' },
{ target: 'rstack-identity', title: 'Secure (You)rIdentity', msg: 'Set up your rIdentity to protect your account. Connect an <strong>email</strong>, add <strong>trusted guardians</strong> for social recovery, or link an external wallet — then click Get Started to begin.' },
];
var step = 0, tourEl = document.getElementById('rspace-tour'), backdrop = document.getElementById('rspace-tour-backdrop'), spotlight = document.getElementById('rspace-tour-spotlight'), tooltip = document.getElementById('rspace-tour-tooltip'), titleEl = document.getElementById('rspace-tour-title'), msgEl = document.getElementById('rspace-tour-msg'), stepEl = document.getElementById('rspace-tour-step'), prevBtn = document.getElementById('rspace-tour-prev'), nextBtn = document.getElementById('rspace-tour-next'), skipBtn = document.getElementById('rspace-tour-skip');
function endTour() { localStorage.setItem('rspace_tour_done', '1'); if (tourEl) tourEl.style.display = 'none'; }
function triggerIdentitySetup() {
var el = document.querySelector('rstack-identity');
if (!el) return;
if (el.isSignedIn && el.isSignedIn()) {
el.showAccountModal();
} else {
el.showAuthModal({
onSuccess: function() {
setTimeout(function() {
if (el.showAccountModal) el.showAccountModal();
}, 500);
}
});
}
}
function showStep() {
if (!tourEl || !backdrop || !spotlight || !tooltip) return;
var s = TOUR_STEPS[step], target = document.querySelector(s.target);
@ -3000,14 +3015,14 @@
stepEl.textContent = (step + 1) + ' / ' + TOUR_STEPS.length; titleEl.textContent = s.title; msgEl.innerHTML = s.msg;
prevBtn.style.display = step > 0 ? '' : 'none'; nextBtn.textContent = step === TOUR_STEPS.length - 1 ? 'Get Started' : 'Next';
}
if (nextBtn) nextBtn.addEventListener('click', function() { step++; if (step >= TOUR_STEPS.length) { endTour(); return; } showStep(); });
if (nextBtn) nextBtn.addEventListener('click', function() { step++; if (step >= TOUR_STEPS.length) { endTour(); triggerIdentitySetup(); return; } showStep(); });
if (prevBtn) prevBtn.addEventListener('click', function() { if (step > 0) { step--; showStep(); } });
if (skipBtn) skipBtn.addEventListener('click', endTour);
if (backdrop) backdrop.addEventListener('click', endTour);
document.addEventListener('keydown', function(e) {
if (!tourEl || tourEl.style.display === 'none') return;
if (e.key === 'Escape') { endTour(); e.preventDefault(); }
if (e.key === 'ArrowRight' || e.key === 'Enter') { step++; if (step >= TOUR_STEPS.length) { endTour(); return; } showStep(); e.preventDefault(); }
if (e.key === 'ArrowRight' || e.key === 'Enter') { step++; if (step >= TOUR_STEPS.length) { endTour(); triggerIdentitySetup(); return; } showStep(); e.preventDefault(); }
if (e.key === 'ArrowLeft' && step > 0) { step--; showStep(); e.preventDefault(); }
});
setTimeout(function() { if (tourEl) { tourEl.style.display = ''; showStep(); } }, 800);