Files
platform/services/brain/app/models/taxonomy.py
Yusuf Suleman 4592e35732
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
feat: major platform expansion — Brain service, RSS reader, iOS app, AI assistants, Firefox extension
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>
2026-04-03 00:56:29 -05:00

118 lines
4.3 KiB
Python

"""Database models for folders and tags — editable taxonomy."""
import re
import uuid
from datetime import datetime
from sqlalchemy import Column, String, Integer, Boolean, DateTime, ForeignKey, Index, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from app.database import Base
def new_id():
return str(uuid.uuid4())
def slugify(text: str) -> str:
s = text.lower().strip()
s = re.sub(r'[^\w\s-]', '', s)
s = re.sub(r'[\s_]+', '-', s)
return s.strip('-')
class Folder(Base):
__tablename__ = "folders"
id = Column(UUID(as_uuid=False), primary_key=True, default=new_id)
user_id = Column(String(64), nullable=False, index=True)
name = Column(String(128), nullable=False)
slug = Column(String(128), nullable=False)
color = Column(String(7), nullable=True) # hex like #4F46E5
icon = Column(String(32), nullable=True) # lucide icon name like "home"
is_active = Column(Boolean, default=True, nullable=False)
sort_order = Column(Integer, default=0, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
__table_args__ = (
UniqueConstraint('user_id', 'slug', name='uq_folder_user_slug'),
)
class Tag(Base):
__tablename__ = "tags"
id = Column(UUID(as_uuid=False), primary_key=True, default=new_id)
user_id = Column(String(64), nullable=False, index=True)
name = Column(String(128), nullable=False)
slug = Column(String(128), nullable=False)
color = Column(String(7), nullable=True)
icon = Column(String(32), nullable=True)
is_active = Column(Boolean, default=True, nullable=False)
sort_order = Column(Integer, default=0, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
__table_args__ = (
UniqueConstraint('user_id', 'slug', name='uq_tag_user_slug'),
)
class ItemTag(Base):
__tablename__ = "item_tags"
item_id = Column(UUID(as_uuid=False), ForeignKey("items.id", ondelete="CASCADE"), primary_key=True)
tag_id = Column(UUID(as_uuid=False), ForeignKey("tags.id", ondelete="CASCADE"), primary_key=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
# Default folders with colors and icons
DEFAULT_FOLDERS = [
{"name": "Home", "color": "#059669", "icon": "home"},
{"name": "Family", "color": "#D97706", "icon": "heart"},
{"name": "Work", "color": "#4338CA", "icon": "briefcase"},
{"name": "Travel", "color": "#0EA5E9", "icon": "plane"},
{"name": "Islam", "color": "#10B981", "icon": "moon"},
{"name": "Homelab", "color": "#6366F1", "icon": "server"},
{"name": "Vanlife", "color": "#F59E0B", "icon": "truck"},
{"name": "3D Printing", "color": "#EC4899", "icon": "printer"},
{"name": "Documents", "color": "#78716C", "icon": "file-text"},
]
# Default tags to seed for new users
DEFAULT_TAGS = [
"diy", "reference", "home-assistant", "shopping", "video",
"tutorial", "server", "kids", "books", "travel",
"churning", "lawn-garden", "piracy", "work", "3d-printing",
"lectures", "vanlife", "yusuf", "madiha", "hafsa", "mustafa",
"medical", "legal", "vehicle", "insurance", "financial", "homeschool",
]
async def ensure_user_taxonomy(db, user_id: str):
"""Seed default folders and tags for a new user if they have none."""
from sqlalchemy import select, func
folder_count = (await db.execute(
select(func.count()).where(Folder.user_id == user_id)
)).scalar() or 0
if folder_count == 0:
for i, f in enumerate(DEFAULT_FOLDERS):
db.add(Folder(id=new_id(), user_id=user_id, name=f["name"], slug=slugify(f["name"]),
color=f.get("color"), icon=f.get("icon"), sort_order=i))
await db.flush()
tag_count = (await db.execute(
select(func.count()).where(Tag.user_id == user_id)
)).scalar() or 0
if tag_count == 0:
for i, name in enumerate(DEFAULT_TAGS):
db.add(Tag(id=new_id(), user_id=user_id, name=name, slug=slugify(name), sort_order=i))
await db.flush()
await db.commit()