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>
118 lines
4.3 KiB
Python
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()
|