mirror of
https://github.com/blackboxprogramming/BlackRoad-Operating-System.git
synced 2026-03-17 00:57:12 -05:00
Add v0.2 pillars: chaos inbox, identity center, command palette
This commit is contained in:
@@ -13,7 +13,8 @@ from app.redis_client import close_redis
|
||||
from app.routers import (
|
||||
auth, email, social, video, files, blockchain, ai_chat, devices, miner,
|
||||
digitalocean, github, huggingface, vscode, games, browser, dashboard,
|
||||
railway, vercel, stripe, twilio, slack, discord, sentry, api_health, agents
|
||||
railway, vercel, stripe, twilio, slack, discord, sentry, api_health, agents,
|
||||
capture, identity_center, notifications_center, creator, compliance_ops, search
|
||||
)
|
||||
from app.services.crypto import rotate_plaintext_wallet_keys
|
||||
|
||||
@@ -138,6 +139,12 @@ app.include_router(twilio.router)
|
||||
app.include_router(slack.router)
|
||||
app.include_router(discord.router)
|
||||
app.include_router(sentry.router)
|
||||
app.include_router(capture.router)
|
||||
app.include_router(identity_center.router)
|
||||
app.include_router(notifications_center.router)
|
||||
app.include_router(creator.router)
|
||||
app.include_router(compliance_ops.router)
|
||||
app.include_router(search.router)
|
||||
|
||||
# API health monitoring
|
||||
app.include_router(api_health.router)
|
||||
|
||||
@@ -6,9 +6,12 @@ from app.models.video import Video, VideoView, VideoLike
|
||||
from app.models.file import File, Folder
|
||||
from app.models.device import Device, DeviceMetric, DeviceLog
|
||||
from app.models.blockchain import Block, Transaction, Wallet
|
||||
from app.models.device import Device
|
||||
from app.models.ai_chat import Conversation, Message
|
||||
from app.models.device import Device, DeviceMetric, DeviceLog
|
||||
from app.models.capture import CaptureItem, CaptureCluster
|
||||
from app.models.identity_profile import UserProfile
|
||||
from app.models.notification import Notification
|
||||
from app.models.creator import CreativeProject
|
||||
from app.models.compliance_event import ComplianceEvent
|
||||
|
||||
__all__ = [
|
||||
"User",
|
||||
@@ -29,10 +32,12 @@ __all__ = [
|
||||
"Block",
|
||||
"Transaction",
|
||||
"Wallet",
|
||||
"Device",
|
||||
"Conversation",
|
||||
"Message",
|
||||
"Device",
|
||||
"DeviceMetric",
|
||||
"DeviceLog",
|
||||
"CaptureItem",
|
||||
"CaptureCluster",
|
||||
"UserProfile",
|
||||
"Notification",
|
||||
"CreativeProject",
|
||||
"ComplianceEvent",
|
||||
]
|
||||
|
||||
39
backend/app/models/capture.py
Normal file
39
backend/app/models/capture.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""Capture and clustering models for chaotic inputs"""
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Text, ForeignKey, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class CaptureItem(Base):
|
||||
"""Generic capture item that can represent notes, links, screenshots, etc."""
|
||||
|
||||
__tablename__ = "capture_items"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
type = Column(String(50), default="note", index=True)
|
||||
raw_content = Column(Text)
|
||||
source = Column(String(100), default="manual", index=True)
|
||||
tags = Column(JSON, default=list)
|
||||
related_to = Column(JSON, default=list)
|
||||
status = Column(String(50), default="inbox", index=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
def __repr__(self):
|
||||
return f"<CaptureItem {self.id}:{self.type}>"
|
||||
|
||||
|
||||
class CaptureCluster(Base):
|
||||
"""Lightweight clustering container for captured items."""
|
||||
|
||||
__tablename__ = "capture_clusters"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(255), nullable=False)
|
||||
description = Column(Text)
|
||||
item_ids = Column(JSON, default=list)
|
||||
last_refreshed_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
def __repr__(self):
|
||||
return f"<CaptureCluster {self.name}>"
|
||||
21
backend/app/models/compliance_event.py
Normal file
21
backend/app/models/compliance_event.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""Lightweight compliance and ops events"""
|
||||
from sqlalchemy import Column, Integer, String, DateTime, JSON
|
||||
from sqlalchemy.sql import func
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class ComplianceEvent(Base):
|
||||
"""Event log for compliance and ops visibility."""
|
||||
|
||||
__tablename__ = "compliance_events"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
actor = Column(String(255))
|
||||
action = Column(String(255))
|
||||
resource = Column(String(255))
|
||||
metadata = Column(JSON, default=dict)
|
||||
severity = Column(String(50), default="info")
|
||||
timestamp = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ComplianceEvent {self.action} by {self.actor}>"
|
||||
24
backend/app/models/creator.py
Normal file
24
backend/app/models/creator.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""Creator workspace models"""
|
||||
from sqlalchemy import Column, Integer, String, Text, JSON, DateTime
|
||||
from sqlalchemy.sql import func
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class CreativeProject(Base):
|
||||
"""Creative project container for creators"""
|
||||
|
||||
__tablename__ = "creative_projects"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
title = Column(String(255), nullable=False)
|
||||
type = Column(String(100), default="mixed")
|
||||
description = Column(Text)
|
||||
links_to_assets = Column(JSON, default=list)
|
||||
status = Column(String(50), default="idea")
|
||||
revenue_streams = Column(JSON, default=dict)
|
||||
notes = Column(Text)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
def __repr__(self):
|
||||
return f"<CreativeProject {self.title}>"
|
||||
33
backend/app/models/identity_profile.py
Normal file
33
backend/app/models/identity_profile.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Identity profile model centralizing user fields"""
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy import DateTime
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class UserProfile(Base):
|
||||
"""Canonical user profile stored once per account."""
|
||||
|
||||
__tablename__ = "user_profiles"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), unique=True)
|
||||
name = Column(String(255))
|
||||
legal_name = Column(String(255))
|
||||
email = Column(String(255))
|
||||
secondary_emails = Column(JSON, default=list)
|
||||
phone = Column(String(50))
|
||||
secondary_phones = Column(JSON, default=list)
|
||||
address = Column(String(500))
|
||||
timezone = Column(String(100))
|
||||
pronouns = Column(String(100))
|
||||
avatar_url = Column(String(500))
|
||||
external_ids = Column(JSON, default=dict)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
user = relationship("User", backref="profile", uselist=False)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<UserProfile {self.user_id}>"
|
||||
23
backend/app/models/notification.py
Normal file
23
backend/app/models/notification.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""Notification model for OS-level alerts"""
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
|
||||
from sqlalchemy.sql import func
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class Notification(Base):
|
||||
"""Stores notifications across apps"""
|
||||
|
||||
__tablename__ = "notifications"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
type = Column(String(50), default="info")
|
||||
source_app_id = Column(String(100))
|
||||
title = Column(String(255))
|
||||
body = Column(String(1000))
|
||||
importance = Column(String(50), default="normal")
|
||||
delivery_mode = Column(String(50), default="immediate")
|
||||
read_at = Column(DateTime(timezone=True))
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Notification {self.id}:{self.title}>"
|
||||
150
backend/app/routers/capture.py
Normal file
150
backend/app/routers/capture.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""Routes for Chaos Inbox capture items and clustering"""
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.auth import get_current_active_user
|
||||
from app.database import get_db
|
||||
from app.models.capture import CaptureItem, CaptureCluster
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter(prefix="/api/capture", tags=["Chaos Inbox"])
|
||||
|
||||
|
||||
class CaptureItemCreate(BaseModel):
|
||||
type: str = "note"
|
||||
raw_content: Optional[str] = None
|
||||
source: Optional[str] = "manual"
|
||||
tags: List[str] = Field(default_factory=list)
|
||||
related_to: List[str] = Field(default_factory=list)
|
||||
status: str = "inbox"
|
||||
|
||||
|
||||
class CaptureItemResponse(BaseModel):
|
||||
id: int
|
||||
type: str
|
||||
raw_content: Optional[str]
|
||||
source: Optional[str]
|
||||
tags: List[str]
|
||||
related_to: List[str]
|
||||
status: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class TagUpdate(BaseModel):
|
||||
tags: List[str]
|
||||
|
||||
|
||||
class StatusUpdate(BaseModel):
|
||||
status: str
|
||||
|
||||
|
||||
class ClusterCreate(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
item_ids: List[int] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ClusterResponse(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
description: Optional[str]
|
||||
item_ids: List[int]
|
||||
last_refreshed_at: Optional[str]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@router.post("/items", response_model=CaptureItemResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_capture_item(
|
||||
item: CaptureItemCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Create a new capture item."""
|
||||
capture_item = CaptureItem(**item.model_dump())
|
||||
db.add(capture_item)
|
||||
await db.commit()
|
||||
await db.refresh(capture_item)
|
||||
return capture_item
|
||||
|
||||
|
||||
@router.get("/items", response_model=List[CaptureItemResponse])
|
||||
async def list_capture_items(
|
||||
status_filter: Optional[str] = None,
|
||||
type_filter: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""List capture items with optional filters."""
|
||||
query = select(CaptureItem)
|
||||
if status_filter:
|
||||
query = query.where(CaptureItem.status == status_filter)
|
||||
if type_filter:
|
||||
query = query.where(CaptureItem.type == type_filter)
|
||||
|
||||
result = await db.execute(query.order_by(CaptureItem.created_at.desc()))
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.post("/items/{item_id}/tag", response_model=CaptureItemResponse)
|
||||
async def update_tags(
|
||||
item_id: int,
|
||||
payload: TagUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
result = await db.execute(select(CaptureItem).where(CaptureItem.id == item_id))
|
||||
item = result.scalar_one_or_none()
|
||||
if not item:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Item not found")
|
||||
|
||||
item.tags = payload.tags
|
||||
await db.commit()
|
||||
await db.refresh(item)
|
||||
return item
|
||||
|
||||
|
||||
@router.post("/items/{item_id}/status", response_model=CaptureItemResponse)
|
||||
async def update_status(
|
||||
item_id: int,
|
||||
payload: StatusUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
result = await db.execute(select(CaptureItem).where(CaptureItem.id == item_id))
|
||||
item = result.scalar_one_or_none()
|
||||
if not item:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Item not found")
|
||||
|
||||
item.status = payload.status
|
||||
await db.commit()
|
||||
await db.refresh(item)
|
||||
return item
|
||||
|
||||
|
||||
@router.post("/clusters", response_model=ClusterResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_cluster(
|
||||
payload: ClusterCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
cluster = CaptureCluster(**payload.model_dump())
|
||||
db.add(cluster)
|
||||
await db.commit()
|
||||
await db.refresh(cluster)
|
||||
return cluster
|
||||
|
||||
|
||||
@router.get("/clusters", response_model=List[ClusterResponse])
|
||||
async def list_clusters(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
result = await db.execute(select(CaptureCluster).order_by(CaptureCluster.last_refreshed_at.desc()))
|
||||
return result.scalars().all()
|
||||
35
backend/app/routers/compliance_ops.py
Normal file
35
backend/app/routers/compliance_ops.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Compliance and operations visibility routes"""
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.auth import get_current_active_user
|
||||
from app.database import get_db
|
||||
from app.models.compliance_event import ComplianceEvent
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter(prefix="/api/compliance", tags=["Compliance"])
|
||||
|
||||
|
||||
class ComplianceEventResponse(BaseModel):
|
||||
id: int
|
||||
actor: str
|
||||
action: str
|
||||
resource: str
|
||||
severity: str
|
||||
metadata: dict | None = None
|
||||
timestamp: str | None = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@router.get("/events", response_model=List[ComplianceEventResponse])
|
||||
async def list_events(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
result = await db.execute(select(ComplianceEvent).order_by(ComplianceEvent.timestamp.desc()))
|
||||
return result.scalars().all()
|
||||
85
backend/app/routers/creator.py
Normal file
85
backend/app/routers/creator.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""Creator Studio routes"""
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.auth import get_current_active_user
|
||||
from app.database import get_db
|
||||
from app.models.creator import CreativeProject
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter(prefix="/api/creator", tags=["Creator"])
|
||||
|
||||
|
||||
class CreativeProjectPayload(BaseModel):
|
||||
title: str
|
||||
type: str = "mixed"
|
||||
description: Optional[str] = None
|
||||
links_to_assets: List[str] = Field(default_factory=list)
|
||||
status: str = "idea"
|
||||
revenue_streams: dict = Field(default_factory=dict)
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class CreativeProjectResponse(CreativeProjectPayload):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@router.get("/projects", response_model=List[CreativeProjectResponse])
|
||||
async def list_projects(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
result = await db.execute(select(CreativeProject).order_by(CreativeProject.created_at.desc()))
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.post("/projects", response_model=CreativeProjectResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_project(
|
||||
payload: CreativeProjectPayload,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
project = CreativeProject(**payload.model_dump())
|
||||
db.add(project)
|
||||
await db.commit()
|
||||
await db.refresh(project)
|
||||
return project
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}", response_model=CreativeProjectResponse)
|
||||
async def get_project(
|
||||
project_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
result = await db.execute(select(CreativeProject).where(CreativeProject.id == project_id))
|
||||
project = result.scalar_one_or_none()
|
||||
if not project:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||||
return project
|
||||
|
||||
|
||||
@router.put("/projects/{project_id}", response_model=CreativeProjectResponse)
|
||||
async def update_project(
|
||||
project_id: int,
|
||||
payload: CreativeProjectPayload,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
result = await db.execute(select(CreativeProject).where(CreativeProject.id == project_id))
|
||||
project = result.scalar_one_or_none()
|
||||
if not project:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||||
|
||||
for field, value in payload.model_dump().items():
|
||||
setattr(project, field, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(project)
|
||||
return project
|
||||
123
backend/app/routers/identity_center.py
Normal file
123
backend/app/routers/identity_center.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""Identity Center routes"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from app.auth import get_current_active_user
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
from app.models.identity_profile import UserProfile
|
||||
|
||||
router = APIRouter(prefix="/api/identity", tags=["Identity"])
|
||||
|
||||
|
||||
class UserProfilePayload(BaseModel):
|
||||
name: Optional[str] = None
|
||||
legal_name: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
secondary_emails: List[str] = Field(default_factory=list)
|
||||
phone: Optional[str] = None
|
||||
secondary_phones: List[str] = Field(default_factory=list)
|
||||
address: Optional[str] = None
|
||||
timezone: Optional[str] = None
|
||||
pronouns: Optional[str] = None
|
||||
avatar_url: Optional[str] = None
|
||||
external_ids: Dict[str, str] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class UserProfileResponse(UserProfilePayload):
|
||||
completeness: int = 0
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
def calculate_completeness(profile: UserProfile) -> int:
|
||||
filled = [
|
||||
profile.name,
|
||||
profile.email,
|
||||
profile.phone,
|
||||
profile.address,
|
||||
profile.timezone,
|
||||
profile.pronouns,
|
||||
]
|
||||
score = int((len([f for f in filled if f]) / len(filled)) * 100)
|
||||
return min(score + (10 if profile.avatar_url else 0), 100)
|
||||
|
||||
|
||||
@router.get("/profile", response_model=UserProfileResponse)
|
||||
async def get_profile(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
result = await db.execute(select(UserProfile).where(UserProfile.user_id == current_user.id))
|
||||
profile = result.scalar_one_or_none()
|
||||
if not profile:
|
||||
profile = UserProfile(user_id=current_user.id, email=current_user.email, name=current_user.full_name)
|
||||
db.add(profile)
|
||||
await db.commit()
|
||||
await db.refresh(profile)
|
||||
|
||||
response = UserProfileResponse.model_validate(profile)
|
||||
response.completeness = calculate_completeness(profile)
|
||||
return response
|
||||
|
||||
|
||||
@router.put("/profile", response_model=UserProfileResponse)
|
||||
async def update_profile(
|
||||
payload: UserProfilePayload,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
result = await db.execute(select(UserProfile).where(UserProfile.user_id == current_user.id))
|
||||
profile = result.scalar_one_or_none()
|
||||
if not profile:
|
||||
profile = UserProfile(user_id=current_user.id)
|
||||
db.add(profile)
|
||||
|
||||
for field, value in payload.model_dump().items():
|
||||
setattr(profile, field, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(profile)
|
||||
|
||||
response = UserProfileResponse.model_validate(profile)
|
||||
response.completeness = calculate_completeness(profile)
|
||||
return response
|
||||
|
||||
|
||||
@router.get("/linked", response_model=dict)
|
||||
async def list_linked_accounts(
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
# Placeholder for linked services registry
|
||||
return {
|
||||
"github": bool(current_user.wallet_address),
|
||||
"wallet": bool(current_user.wallet_address),
|
||||
"discord": False,
|
||||
"railway": False,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/link_external", response_model=dict)
|
||||
async def link_external(
|
||||
provider: str,
|
||||
external_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
result = await db.execute(select(UserProfile).where(UserProfile.user_id == current_user.id))
|
||||
profile = result.scalar_one_or_none()
|
||||
if not profile:
|
||||
profile = UserProfile(user_id=current_user.id)
|
||||
db.add(profile)
|
||||
|
||||
external = profile.external_ids or {}
|
||||
external[provider] = external_id
|
||||
profile.external_ids = external
|
||||
await db.commit()
|
||||
await db.refresh(profile)
|
||||
|
||||
return {"provider": provider, "external_id": external_id}
|
||||
81
backend/app/routers/notifications_center.py
Normal file
81
backend/app/routers/notifications_center.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""Notification center routes"""
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.auth import get_current_active_user
|
||||
from app.database import get_db
|
||||
from app.models.notification import Notification
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter(prefix="/api/notifications", tags=["Notifications"])
|
||||
|
||||
|
||||
class NotificationCreate(BaseModel):
|
||||
type: str = "info"
|
||||
source_app_id: Optional[str] = None
|
||||
title: str
|
||||
body: str
|
||||
importance: str = "normal"
|
||||
delivery_mode: str = "immediate"
|
||||
|
||||
|
||||
class NotificationResponse(BaseModel):
|
||||
id: int
|
||||
type: str
|
||||
source_app_id: Optional[str]
|
||||
title: str
|
||||
body: str
|
||||
importance: str
|
||||
delivery_mode: str
|
||||
read_at: Optional[str]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@router.post("", response_model=NotificationResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_notification(
|
||||
payload: NotificationCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
notification = Notification(**payload.model_dump())
|
||||
db.add(notification)
|
||||
await db.commit()
|
||||
await db.refresh(notification)
|
||||
return notification
|
||||
|
||||
|
||||
@router.get("", response_model=List[NotificationResponse])
|
||||
async def list_notifications(
|
||||
importance: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
query = select(Notification)
|
||||
if importance:
|
||||
query = query.where(Notification.importance == importance)
|
||||
result = await db.execute(query.order_by(Notification.created_at.desc()))
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.post("/{notification_id}/read", response_model=NotificationResponse)
|
||||
async def mark_read(
|
||||
notification_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
result = await db.execute(select(Notification).where(Notification.id == notification_id))
|
||||
notification = result.scalar_one_or_none()
|
||||
if not notification:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Notification not found")
|
||||
|
||||
if not notification.read_at:
|
||||
notification.read_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
await db.refresh(notification)
|
||||
return notification
|
||||
88
backend/app/routers/search.py
Normal file
88
backend/app/routers/search.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""Unified search endpoints"""
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.auth import get_current_active_user
|
||||
from app.database import get_db
|
||||
from app.models.capture import CaptureItem
|
||||
from app.models.identity_profile import UserProfile
|
||||
from app.models.creator import CreativeProject
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter(prefix="/api/search", tags=["Search"])
|
||||
|
||||
|
||||
class SearchResult(BaseModel):
|
||||
app_id: str
|
||||
type: str
|
||||
title: str
|
||||
snippet: str | None = None
|
||||
ref_id: str | None = None
|
||||
|
||||
|
||||
@router.get("", response_model=List[SearchResult])
|
||||
async def unified_search(
|
||||
q: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
results: List[SearchResult] = []
|
||||
|
||||
# Capture items search
|
||||
capture_result = await db.execute(
|
||||
select(CaptureItem).where(CaptureItem.raw_content.ilike(f"%{q}%"))
|
||||
)
|
||||
for item in capture_result.scalars().all():
|
||||
results.append(
|
||||
SearchResult(
|
||||
app_id="chaos-inbox",
|
||||
type=item.type,
|
||||
title=item.raw_content[:60] if item.raw_content else item.type,
|
||||
snippet=item.status,
|
||||
ref_id=str(item.id)
|
||||
)
|
||||
)
|
||||
|
||||
# Creative projects
|
||||
project_result = await db.execute(
|
||||
select(CreativeProject).where(CreativeProject.title.ilike(f"%{q}%"))
|
||||
)
|
||||
for project in project_result.scalars().all():
|
||||
results.append(
|
||||
SearchResult(
|
||||
app_id="creator-studio",
|
||||
type=project.type,
|
||||
title=project.title,
|
||||
snippet=project.description[:80] if project.description else None,
|
||||
ref_id=str(project.id)
|
||||
)
|
||||
)
|
||||
|
||||
# Identity profile
|
||||
profile_result = await db.execute(select(UserProfile).where(UserProfile.user_id == current_user.id))
|
||||
profile = profile_result.scalar_one_or_none()
|
||||
if profile and q.lower() in (profile.name or "").lower():
|
||||
results.append(
|
||||
SearchResult(
|
||||
app_id="identity-center",
|
||||
type="profile",
|
||||
title=profile.name or "Profile",
|
||||
snippet=profile.email,
|
||||
ref_id=str(profile.id)
|
||||
)
|
||||
)
|
||||
|
||||
# App discovery (front-end uses registry; include sample results)
|
||||
if "identity" in q.lower():
|
||||
results.append(SearchResult(app_id="identity-center", type="app", title="Identity Center"))
|
||||
if "chaos" in q.lower() or "note" in q.lower():
|
||||
results.append(SearchResult(app_id="chaos-inbox", type="app", title="Chaos Inbox"))
|
||||
if "creator" in q.lower():
|
||||
results.append(SearchResult(app_id="creator-studio", type="app", title="Creator Studio"))
|
||||
if "compliance" in q.lower():
|
||||
results.append(SearchResult(app_id="compliance-ops", type="app", title="Compliance & Ops"))
|
||||
|
||||
return results
|
||||
Reference in New Issue
Block a user