feat: major platform expansion — Brain service, RSS reader, iOS app, AI assistants, Firefox extension
All checks were successful
Security Checks / dependency-audit (push) Successful in 1m13s
Security Checks / secret-scanning (push) Successful in 3s
Security Checks / dockerfile-lint (push) Successful in 3s

Brain Service:
- Playwright stealth crawler replacing browserless (og:image, Readability, Reddit JSON API)
- AI classification with tag definitions and folder assignment
- YouTube video download via yt-dlp
- Karakeep migration complete (96 items)
- Taxonomy management (folders with icons/colors, tags)
- Discovery shuffle, sort options, search (Meilisearch + pgvector)
- Item tag/folder editing, card color accents

RSS Reader Service:
- Custom FastAPI reader replacing Miniflux
- Feed management (add/delete/refresh), category support
- Full article extraction via Readability
- Background content fetching for new entries
- Mark all read with confirmation
- Infinite scroll, retention cleanup (30/60 day)
- 17 feeds migrated from Miniflux

iOS App (SwiftUI):
- Native iOS 17+ app with @Observable architecture
- Cookie-based auth, configurable gateway URL
- Dashboard with custom background photo + frosted glass widgets
- Full fitness module (today/templates/goals/food library)
- AI assistant chat (fitness + brain, raw JSON state management)
- 120fps ProMotion support

AI Assistants (Gateway):
- Unified dispatcher with fitness/brain domain detection
- Fitness: natural language food logging, photo analysis, multi-item splitting
- Brain: save/append/update/delete notes, search & answer, undo support
- Madiha user gets fitness-only (brain disabled)

Firefox Extension:
- One-click save to Brain from any page
- Login with platform credentials
- Right-click context menu (save page/link/image)
- Notes field for URL saves
- Signed and published on AMO

Other:
- Reader bookmark button routes to Brain (was Karakeep)
- Fitness food library with "Add" button + add-to-meal popup
- Kindle send file size check (25MB SMTP2GO limit)
- Atelier UI as default (useAtelierShell=true)
- Mobile upload box in nav drawer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-03 00:56:29 -05:00
parent af1765bd8e
commit 4592e35732
97 changed files with 11009 additions and 532 deletions

View File

@@ -0,0 +1,202 @@
const loginView = document.getElementById("login-view");
const saveView = document.getElementById("save-view");
const successView = document.getElementById("success-view");
const loginBtn = document.getElementById("login-btn");
const loginUser = document.getElementById("login-user");
const loginPass = document.getElementById("login-pass");
const loginError = document.getElementById("login-error");
const logoutBtn = document.getElementById("logout-btn");
const userName = document.getElementById("user-name");
const tabLink = document.getElementById("tab-link");
const tabNote = document.getElementById("tab-note");
const linkMode = document.getElementById("link-mode");
const noteMode = document.getElementById("note-mode");
const pageTitle = document.getElementById("page-title");
const pageUrl = document.getElementById("page-url");
const noteToggleBtn = document.getElementById("note-toggle-btn");
const linkNote = document.getElementById("link-note");
const saveLinkBtn = document.getElementById("save-link-btn");
const noteText = document.getElementById("note-text");
const saveNoteBtn = document.getElementById("save-note-btn");
const saveError = document.getElementById("save-error");
let currentTab = null;
// ── View management ──
function showView(view) {
loginView.classList.add("hidden");
saveView.classList.add("hidden");
successView.classList.add("hidden");
view.classList.remove("hidden");
}
function showError(el, msg) {
el.textContent = msg;
el.classList.remove("hidden");
}
function hideError(el) {
el.classList.add("hidden");
}
// ── Init ──
async function init() {
const resp = await browser.runtime.sendMessage({ action: "check-auth" });
if (resp.authenticated) {
userName.textContent = resp.user?.display_name || resp.user?.username || "";
showView(saveView);
await loadCurrentTab();
} else {
showView(loginView);
loginUser.focus();
}
}
async function loadCurrentTab() {
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
if (tab) {
currentTab = tab;
pageTitle.textContent = tab.title || "Untitled";
try {
pageUrl.textContent = new URL(tab.url).hostname;
} catch {
pageUrl.textContent = tab.url;
}
}
}
// ── Login ──
loginBtn.addEventListener("click", async () => {
const user = loginUser.value.trim();
const pass = loginPass.value;
if (!user || !pass) return;
loginBtn.disabled = true;
loginBtn.textContent = "Signing in...";
hideError(loginError);
try {
const resp = await browser.runtime.sendMessage({
action: "login",
username: user,
password: pass,
});
if (resp && resp.success) {
const auth = await browser.runtime.sendMessage({ action: "check-auth" });
userName.textContent = auth.user?.display_name || auth.user?.username || "";
showView(saveView);
await loadCurrentTab();
} else {
showError(loginError, (resp && resp.error) || "Login failed");
}
} catch (e) {
showError(loginError, "Error: " + (e.message || e));
}
loginBtn.disabled = false;
loginBtn.textContent = "Sign in";
});
loginPass.addEventListener("keydown", (e) => {
if (e.key === "Enter") loginBtn.click();
});
// ── Logout ──
logoutBtn.addEventListener("click", async () => {
await browser.runtime.sendMessage({ action: "logout" });
showView(loginView);
loginUser.value = "";
loginPass.value = "";
});
// ── Mode tabs ──
tabLink.addEventListener("click", () => {
tabLink.classList.add("active");
tabNote.classList.remove("active");
linkMode.classList.remove("hidden");
noteMode.classList.add("hidden");
hideError(saveError);
});
tabNote.addEventListener("click", () => {
tabNote.classList.add("active");
tabLink.classList.remove("active");
noteMode.classList.remove("hidden");
linkMode.classList.add("hidden");
hideError(saveError);
noteText.focus();
});
// ── Note toggle on link mode ──
noteToggleBtn.addEventListener("click", () => {
linkNote.classList.toggle("hidden");
if (!linkNote.classList.contains("hidden")) {
noteToggleBtn.textContent = "- Hide note";
linkNote.focus();
} else {
noteToggleBtn.textContent = "+ Add a note";
}
});
// ── Save link ──
saveLinkBtn.addEventListener("click", async () => {
if (!currentTab?.url) return;
saveLinkBtn.disabled = true;
saveLinkBtn.textContent = "Saving...";
hideError(saveError);
try {
await browser.runtime.sendMessage({
action: "save-link",
url: currentTab.url,
title: currentTab.title,
note: linkNote.value.trim() || undefined,
});
showView(successView);
setTimeout(() => window.close(), 1500);
} catch (e) {
showError(saveError, e.message || "Save failed");
saveLinkBtn.disabled = false;
saveLinkBtn.textContent = "Save page";
}
});
// ── Save note ──
saveNoteBtn.addEventListener("click", async () => {
const text = noteText.value.trim();
if (!text) return;
saveNoteBtn.disabled = true;
saveNoteBtn.textContent = "Saving...";
hideError(saveError);
try {
await browser.runtime.sendMessage({ action: "save-note", text });
showView(successView);
setTimeout(() => window.close(), 1500);
} catch (e) {
showError(saveError, e.message || "Save failed");
saveNoteBtn.disabled = false;
saveNoteBtn.textContent = "Save note";
}
});
// ── Start ──
init().catch((e) => {
console.error("[Brain popup] Init failed:", e);
// Show login view as fallback
showView(loginView);
loginUser.focus();
});