mirror of
https://github.com/blackboxprogramming/BlackRoad-Operating-System.git
synced 2026-03-18 04:33:59 -05:00
Merge branch origin/codex/implement-v0.2-of-blackroad-os into main
This commit is contained in:
@@ -12,6 +12,15 @@ A comprehensive FastAPI backend for the BlackRoad Operating System, a Windows 95
|
|||||||
- **File Storage** - File explorer with folder management and sharing
|
- **File Storage** - File explorer with folder management and sharing
|
||||||
- **RoadCoin Blockchain** - Cryptocurrency with mining, transactions, and wallet management
|
- **RoadCoin Blockchain** - Cryptocurrency with mining, transactions, and wallet management
|
||||||
- **AI Chat** - Conversational AI assistant with conversation history
|
- **AI Chat** - Conversational AI assistant with conversation history
|
||||||
|
- **Chaos Inbox / Identity / Notifications / Creator / Compliance** - New v0.2 APIs for capture, profiles, alerts, creative projects, and audit visibility
|
||||||
|
|
||||||
|
### New v0.2 endpoints
|
||||||
|
- `/api/capture/*` — capture items, clustering, status and tagging
|
||||||
|
- `/api/identity/profile` — canonical user profile for OS apps
|
||||||
|
- `/api/notifications` — create/list/mark notifications
|
||||||
|
- `/api/creator/projects` — manage creator projects and assets
|
||||||
|
- `/api/compliance/events` — surface audit events
|
||||||
|
- `/api/search?q=` — unified search scaffold
|
||||||
|
|
||||||
### Technology Stack
|
### Technology Stack
|
||||||
- **FastAPI** - Modern, fast Python web framework
|
- **FastAPI** - Modern, fast Python web framework
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ from app.redis_client import close_redis
|
|||||||
from app.routers import (
|
from app.routers import (
|
||||||
auth, email, social, video, files, blockchain, ai_chat, devices, miner,
|
auth, email, social, video, files, blockchain, ai_chat, devices, miner,
|
||||||
digitalocean, github, huggingface, vscode, games, browser, dashboard,
|
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
|
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(slack.router)
|
||||||
app.include_router(discord.router)
|
app.include_router(discord.router)
|
||||||
app.include_router(sentry.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
|
# API health monitoring
|
||||||
app.include_router(api_health.router)
|
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.file import File, Folder
|
||||||
from app.models.device import Device, DeviceMetric, DeviceLog
|
from app.models.device import Device, DeviceMetric, DeviceLog
|
||||||
from app.models.blockchain import Block, Transaction, Wallet
|
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.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__ = [
|
__all__ = [
|
||||||
"User",
|
"User",
|
||||||
@@ -29,10 +32,12 @@ __all__ = [
|
|||||||
"Block",
|
"Block",
|
||||||
"Transaction",
|
"Transaction",
|
||||||
"Wallet",
|
"Wallet",
|
||||||
"Device",
|
|
||||||
"Conversation",
|
"Conversation",
|
||||||
"Message",
|
"Message",
|
||||||
"Device",
|
"CaptureItem",
|
||||||
"DeviceMetric",
|
"CaptureCluster",
|
||||||
"DeviceLog",
|
"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
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# BlackRoad OS v0.1.1
|
# BlackRoad OS v0.2
|
||||||
|
|
||||||
**The Living Portal** — A complete front-end operating system for the BlackRoad ecosystem.
|
**The Living Portal** — A complete front-end operating system for the BlackRoad ecosystem.
|
||||||
|
|
||||||
@@ -12,12 +12,14 @@
|
|||||||
|
|
||||||
BlackRoad OS is a **production-ready**, fully-accessible desktop operating system built entirely with vanilla JavaScript, HTML, and CSS. No frameworks, no build tools, no dependencies - just clean, maintainable code.
|
BlackRoad OS is a **production-ready**, fully-accessible desktop operating system built entirely with vanilla JavaScript, HTML, and CSS. No frameworks, no build tools, no dependencies - just clean, maintainable code.
|
||||||
|
|
||||||
**New in v0.1.1:**
|
**New in v0.2:**
|
||||||
- ✨ **Accessibility-first** - Full keyboard navigation, ARIA attributes throughout
|
- 🌀 **Chaos Inbox** for neurodivergent-friendly capture and clustering
|
||||||
- 🎯 **Lifecycle hooks** - Apps can listen to window events
|
- 🪪 **Identity Center** to kill duplication across apps
|
||||||
- 🔧 **Config layer** - Feature flags and API endpoint management
|
- 🔔 **Notification Center focus modes** to tame alert noise
|
||||||
- 📚 **Component library** - 15 polished, accessible UI primitives
|
- 🎨 **Creator Studio** baseline workspace for creators
|
||||||
- 📖 **Comprehensive docs** - ARCHITECTURE.md + EXTENDING.md guides
|
- 🧭 **Compliance & Ops** surface for audits/workflows
|
||||||
|
- ⌨️ **Global command palette** (Ctrl/Cmd + K) unified search
|
||||||
|
- 🎨 **High contrast theme** added to theme cycle
|
||||||
|
|
||||||
It provides a complete enterprise portal for managing all BlackRoad operations including:
|
It provides a complete enterprise portal for managing all BlackRoad operations including:
|
||||||
|
|
||||||
|
|||||||
@@ -515,3 +515,24 @@
|
|||||||
.tab-content {
|
.tab-content {
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Chaos Inbox */
|
||||||
|
.chaos-inbox .two-column-layout { display: grid; grid-template-columns: 2fr 1fr; gap: 12px; }
|
||||||
|
.chaos-meta { display: flex; justify-content: space-between; color: var(--text-secondary); font-size: 12px; }
|
||||||
|
.chaos-body { margin: 8px 0; color: var(--text-primary); }
|
||||||
|
.chaos-tags { color: var(--text-secondary); font-size: 12px; }
|
||||||
|
.chaos-filters { margin-bottom: 12px; }
|
||||||
|
.chaos-clusters .pill { margin-right: 6px; display: inline-block; padding: 4px 6px; }
|
||||||
|
|
||||||
|
/* Identity Center */
|
||||||
|
.identity-center .identity-row { display: grid; grid-template-columns: 1fr 2fr; padding: 6px 0; }
|
||||||
|
.identity-center .label { color: var(--text-secondary); }
|
||||||
|
.identity-howto { padding-left: 18px; line-height: 1.5; }
|
||||||
|
|
||||||
|
/* Creator Studio */
|
||||||
|
.creator-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
|
||||||
|
.creator-body p { margin: 0 0 8px 0; }
|
||||||
|
.creator-assets ul { padding-left: 18px; }
|
||||||
|
|
||||||
|
/* Compliance & Ops */
|
||||||
|
.compliance-ops { display: flex; flex-direction: column; gap: 12px; }
|
||||||
|
|||||||
@@ -37,6 +37,21 @@ body[data-theme="nightOS"] {
|
|||||||
--taskbar-bg: rgba(10, 0, 20, 0.98);
|
--taskbar-bg: rgba(10, 0, 20, 0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body[data-theme="contrastOS"] {
|
||||||
|
--primary: #FFD700;
|
||||||
|
--primary-dark: #C0A000;
|
||||||
|
--primary-light: #FFE85A;
|
||||||
|
--bg-desktop: #000;
|
||||||
|
--bg-surface: #111;
|
||||||
|
--bg-surface-hover: #191919;
|
||||||
|
--bg-window: #0c0c0c;
|
||||||
|
--text-primary: #fff;
|
||||||
|
--text-secondary: #e0e0e0;
|
||||||
|
--text-dim: #ccc;
|
||||||
|
--border-color: #FFD700;
|
||||||
|
--taskbar-bg: #000;
|
||||||
|
}
|
||||||
|
|
||||||
/* Desktop */
|
/* Desktop */
|
||||||
.desktop {
|
.desktop {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -415,6 +430,37 @@ body[data-theme="nightOS"] {
|
|||||||
border-left: 3px solid var(--info);
|
border-left: 3px solid var(--info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Command palette */
|
||||||
|
.command-palette {
|
||||||
|
position: fixed;
|
||||||
|
top: 20%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 600px;
|
||||||
|
background: var(--bg-window);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
box-shadow: 0 8px 32px var(--shadow);
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: none;
|
||||||
|
z-index: 11000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette.open { display: block; }
|
||||||
|
.command-palette input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-results { max-height: 320px; overflow: auto; display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.command-group-title { font-weight: 600; color: var(--text-primary); margin-bottom: 4px; }
|
||||||
|
.command-row { padding: 8px; border-radius: 8px; cursor: pointer; background: var(--bg-surface); }
|
||||||
|
.command-row:hover, .command-row:focus { background: var(--bg-surface-hover); outline: 1px solid var(--primary); }
|
||||||
|
|
||||||
/* Scrollbar Styling */
|
/* Scrollbar Styling */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
|
|||||||
@@ -91,10 +91,14 @@
|
|||||||
<script src="js/apps/compliance.js"></script>
|
<script src="js/apps/compliance.js"></script>
|
||||||
<script src="js/apps/finance.js"></script>
|
<script src="js/apps/finance.js"></script>
|
||||||
<script src="js/apps/identity.js"></script>
|
<script src="js/apps/identity.js"></script>
|
||||||
|
<script src="js/apps/identity_center.js"></script>
|
||||||
<script src="js/apps/research.js"></script>
|
<script src="js/apps/research.js"></script>
|
||||||
<script src="js/apps/engineering.js"></script>
|
<script src="js/apps/engineering.js"></script>
|
||||||
<script src="js/apps/settings.js"></script>
|
<script src="js/apps/settings.js"></script>
|
||||||
<script src="js/apps/notifications.js"></script>
|
<script src="js/apps/notifications.js"></script>
|
||||||
|
<script src="js/apps/chaos_inbox.js"></script>
|
||||||
|
<script src="js/apps/creator_studio.js"></script>
|
||||||
|
<script src="js/apps/compliance_ops.js"></script>
|
||||||
<script src="js/apps/corporate.js"></script>
|
<script src="js/apps/corporate.js"></script>
|
||||||
|
|
||||||
<!-- Registry and Bootloader (load last) -->
|
<!-- Registry and Bootloader (load last) -->
|
||||||
|
|||||||
163
blackroad-os/js/apps/chaos_inbox.js
Normal file
163
blackroad-os/js/apps/chaos_inbox.js
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
/**
|
||||||
|
* Chaos Inbox
|
||||||
|
* Neurodivergent-friendly capture surface for scraps, links, and ideas
|
||||||
|
*/
|
||||||
|
|
||||||
|
window.ChaosInboxApp = function() {
|
||||||
|
const appId = 'chaos-inbox';
|
||||||
|
|
||||||
|
const toolbar = document.createElement('div');
|
||||||
|
toolbar.className = 'window-toolbar';
|
||||||
|
|
||||||
|
const quickCaptureInput = document.createElement('input');
|
||||||
|
quickCaptureInput.type = 'text';
|
||||||
|
quickCaptureInput.placeholder = 'Quick capture a note...';
|
||||||
|
quickCaptureInput.setAttribute('aria-label', 'Quick capture note');
|
||||||
|
quickCaptureInput.className = 'input';
|
||||||
|
|
||||||
|
const captureButton = Components.Button('Save', {
|
||||||
|
onClick: () => {
|
||||||
|
if (!quickCaptureInput.value) return;
|
||||||
|
const newItem = {
|
||||||
|
id: Date.now(),
|
||||||
|
type: 'note',
|
||||||
|
raw_content: quickCaptureInput.value,
|
||||||
|
status: 'inbox',
|
||||||
|
created_at: new Date().toISOString()
|
||||||
|
};
|
||||||
|
MockData.captureItems.unshift(newItem);
|
||||||
|
quickCaptureInput.value = '';
|
||||||
|
renderContent();
|
||||||
|
window.OS.showNotification({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Captured',
|
||||||
|
message: 'Added to Chaos Inbox',
|
||||||
|
duration: 1500
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
toolbar.appendChild(quickCaptureInput);
|
||||||
|
toolbar.appendChild(captureButton);
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'chaos-inbox';
|
||||||
|
|
||||||
|
function renderItemCard(item) {
|
||||||
|
const tags = item.tags?.length ? item.tags.join(', ') : 'No tags';
|
||||||
|
const statusBadge = Components.Badge(item.status, item.status === 'inbox' ? 'warning' : 'info');
|
||||||
|
const content = document.createElement('div');
|
||||||
|
content.innerHTML = `
|
||||||
|
<div class="chaos-meta">
|
||||||
|
<span>${item.type.toUpperCase()} • ${item.source || 'manual'}</span>
|
||||||
|
<span>${item.created_at}</span>
|
||||||
|
</div>
|
||||||
|
<div class="chaos-body">${item.raw_content || 'Empty'}</div>
|
||||||
|
<div class="chaos-tags">${tags}</div>
|
||||||
|
`;
|
||||||
|
const footer = document.createElement('div');
|
||||||
|
footer.style.display = 'flex';
|
||||||
|
footer.style.justifyContent = 'space-between';
|
||||||
|
footer.appendChild(statusBadge);
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
const archiveBtn = Components.Button('Archive', {
|
||||||
|
size: 'small',
|
||||||
|
onClick: () => {
|
||||||
|
item.status = 'archived';
|
||||||
|
renderContent();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const resurfaceBtn = Components.Button('Resurface', {
|
||||||
|
size: 'small',
|
||||||
|
onClick: () => {
|
||||||
|
item.status = 'resurfaced';
|
||||||
|
renderContent();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
actions.appendChild(archiveBtn);
|
||||||
|
actions.appendChild(resurfaceBtn);
|
||||||
|
footer.appendChild(actions);
|
||||||
|
|
||||||
|
return Components.Card({
|
||||||
|
title: item.raw_content?.slice(0, 40) || item.type,
|
||||||
|
subtitle: `Status: ${item.status}`,
|
||||||
|
content,
|
||||||
|
footer
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderClusters() {
|
||||||
|
const clusterWrap = document.createElement('div');
|
||||||
|
clusterWrap.className = 'chaos-clusters';
|
||||||
|
const heading = document.createElement('h3');
|
||||||
|
heading.textContent = 'Suggested clusters';
|
||||||
|
clusterWrap.appendChild(heading);
|
||||||
|
MockData.captureClusters.forEach(cluster => {
|
||||||
|
const linked = MockData.captureItems.filter(item => cluster.item_ids.includes(item.id));
|
||||||
|
const body = document.createElement('div');
|
||||||
|
body.innerHTML = `<div class="cluster-desc">${cluster.description}</div>`;
|
||||||
|
linked.forEach(item => {
|
||||||
|
const pill = Components.Badge(item.type, 'info');
|
||||||
|
pill.textContent = `${item.type}: ${item.raw_content?.slice(0, 24)}`;
|
||||||
|
pill.classList.add('pill');
|
||||||
|
body.appendChild(pill);
|
||||||
|
});
|
||||||
|
clusterWrap.appendChild(Components.Card({ title: cluster.name, content: body }));
|
||||||
|
});
|
||||||
|
return clusterWrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResurface() {
|
||||||
|
const older = MockData.captureItems.filter(item => item.status === 'inbox');
|
||||||
|
if (!older.length) return document.createElement('div');
|
||||||
|
const list = Components.List(older.map(item => ({
|
||||||
|
title: item.raw_content || item.type,
|
||||||
|
subtitle: `Captured ${item.created_at}`,
|
||||||
|
icon: '🔄'
|
||||||
|
})));
|
||||||
|
return Components.Card({ title: 'Resurface', subtitle: 'Things that need love', content: list });
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderContent() {
|
||||||
|
container.innerHTML = '';
|
||||||
|
const filters = document.createElement('div');
|
||||||
|
filters.className = 'chaos-filters';
|
||||||
|
const statusFilter = document.createElement('select');
|
||||||
|
statusFilter.innerHTML = `<option value="all">All</option><option value="inbox">Inbox</option><option value="clustered">Clustered</option><option value="resurfaced">Resurfaced</option>`;
|
||||||
|
statusFilter.onchange = () => renderContent();
|
||||||
|
filters.appendChild(statusFilter);
|
||||||
|
|
||||||
|
const columns = document.createElement('div');
|
||||||
|
columns.className = 'two-column-layout';
|
||||||
|
const left = document.createElement('div');
|
||||||
|
const right = document.createElement('div');
|
||||||
|
|
||||||
|
const selectedStatus = statusFilter.value;
|
||||||
|
const items = MockData.captureItems.filter(item => selectedStatus === 'all' || item.status === selectedStatus);
|
||||||
|
if (!items.length) {
|
||||||
|
left.appendChild(Components.EmptyState({ icon: '🌀', title: 'Nothing captured yet', text: 'Quick add something above.' }));
|
||||||
|
} else {
|
||||||
|
items.forEach(item => left.appendChild(renderItemCard(item)));
|
||||||
|
}
|
||||||
|
|
||||||
|
right.appendChild(renderClusters());
|
||||||
|
right.appendChild(renderResurface());
|
||||||
|
|
||||||
|
columns.appendChild(left);
|
||||||
|
columns.appendChild(right);
|
||||||
|
container.appendChild(filters);
|
||||||
|
container.appendChild(columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent();
|
||||||
|
|
||||||
|
window.OS.createWindow({
|
||||||
|
id: appId,
|
||||||
|
title: 'Chaos Inbox',
|
||||||
|
icon: '🌀',
|
||||||
|
toolbar,
|
||||||
|
content: container,
|
||||||
|
width: '1100px',
|
||||||
|
height: '720px'
|
||||||
|
});
|
||||||
|
};
|
||||||
49
blackroad-os/js/apps/compliance_ops.js
Normal file
49
blackroad-os/js/apps/compliance_ops.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Compliance & Ops console
|
||||||
|
*/
|
||||||
|
|
||||||
|
window.ComplianceOpsApp = function() {
|
||||||
|
const appId = 'compliance-ops';
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'compliance-ops';
|
||||||
|
|
||||||
|
const eventsTable = Components.Table(
|
||||||
|
[
|
||||||
|
{ key: 'timestamp', label: 'When' },
|
||||||
|
{ key: 'actor', label: 'Actor' },
|
||||||
|
{ key: 'action', label: 'Action' },
|
||||||
|
{ key: 'resource', label: 'Resource' },
|
||||||
|
{ key: 'severity', label: 'Severity', render: (val) => Components.Badge(val, val === 'critical' ? 'error' : 'info') }
|
||||||
|
],
|
||||||
|
MockData.auditLogs.map(log => ({
|
||||||
|
timestamp: log.timestamp,
|
||||||
|
actor: log.user,
|
||||||
|
action: log.event,
|
||||||
|
resource: log.ip,
|
||||||
|
severity: log.result
|
||||||
|
})),
|
||||||
|
{ caption: 'Recent events' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const workflows = Components.Card({
|
||||||
|
title: 'Workflows',
|
||||||
|
subtitle: 'Make compliance visible instead of hidden',
|
||||||
|
content: Components.List([
|
||||||
|
{ icon: '✅', title: 'Marketing review', subtitle: 'Queue: 5 items' },
|
||||||
|
{ icon: '🛡️', title: 'Security review', subtitle: 'Auto checks nightly' },
|
||||||
|
{ icon: '📜', title: 'Policy updates', subtitle: 'Notify teams automatically' }
|
||||||
|
])
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(Components.Card({ title: 'Events', content: eventsTable }));
|
||||||
|
container.appendChild(workflows);
|
||||||
|
|
||||||
|
window.OS.createWindow({
|
||||||
|
id: appId,
|
||||||
|
title: 'Compliance & Ops',
|
||||||
|
icon: '🧭',
|
||||||
|
content: container,
|
||||||
|
width: '900px',
|
||||||
|
height: '650px'
|
||||||
|
});
|
||||||
|
};
|
||||||
60
blackroad-os/js/apps/creator_studio.js
Normal file
60
blackroad-os/js/apps/creator_studio.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* Creator Studio - minimal hub for creative projects
|
||||||
|
*/
|
||||||
|
|
||||||
|
window.CreatorStudioApp = function() {
|
||||||
|
const appId = 'creator-studio';
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'creator-studio';
|
||||||
|
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'creator-header';
|
||||||
|
header.innerHTML = '<div><h2>Creator Studio</h2><p>Track creative work without a dozen tabs.</p></div>';
|
||||||
|
const newBtn = Components.Button('New idea', {
|
||||||
|
onClick: () => {
|
||||||
|
const title = prompt('Project title');
|
||||||
|
if (!title) return;
|
||||||
|
MockData.creativeProjects.unshift({ id: Date.now(), title, type: 'mixed', status: 'idea', description: '' });
|
||||||
|
renderProjects();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
header.appendChild(newBtn);
|
||||||
|
|
||||||
|
container.appendChild(header);
|
||||||
|
|
||||||
|
function renderProjects() {
|
||||||
|
const listContainer = document.createElement('div');
|
||||||
|
listContainer.className = 'creator-list';
|
||||||
|
MockData.creativeProjects.forEach(project => {
|
||||||
|
const assets = (project.links_to_assets || []).map(link => `<li><a href="${link}">${link}</a></li>`).join('');
|
||||||
|
const revenue = Object.entries(project.revenue_streams || {}).map(([k,v]) => `${k}: $${v}`).join(', ') || 'No revenue data';
|
||||||
|
const card = Components.Card({
|
||||||
|
title: project.title,
|
||||||
|
subtitle: `${project.type} • ${project.status}`,
|
||||||
|
content: `<div class="creator-body">
|
||||||
|
<p>${project.description || 'No description yet'}</p>
|
||||||
|
<div class="creator-assets"><strong>Assets</strong><ul>${assets}</ul></div>
|
||||||
|
<div class="creator-revenue"><strong>Revenue</strong>: ${revenue}</div>
|
||||||
|
<div class="creator-notes">${project.notes || ''}</div>
|
||||||
|
</div>`,
|
||||||
|
footer: Components.Button('Mark published', {
|
||||||
|
size: 'small',
|
||||||
|
onClick: () => { project.status = 'published'; renderProjects(); }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
listContainer.appendChild(card);
|
||||||
|
});
|
||||||
|
return listContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.appendChild(renderProjects());
|
||||||
|
|
||||||
|
window.OS.createWindow({
|
||||||
|
id: appId,
|
||||||
|
title: 'Creator Studio',
|
||||||
|
icon: '🎨',
|
||||||
|
content: container,
|
||||||
|
width: '1000px',
|
||||||
|
height: '700px'
|
||||||
|
});
|
||||||
|
};
|
||||||
78
blackroad-os/js/apps/identity_center.js
Normal file
78
blackroad-os/js/apps/identity_center.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Identity Center - unified profile
|
||||||
|
*/
|
||||||
|
|
||||||
|
window.IdentityCenterApp = function() {
|
||||||
|
const appId = 'identity-center';
|
||||||
|
const profile = MockData.identityProfile;
|
||||||
|
|
||||||
|
const toolbar = document.createElement('div');
|
||||||
|
toolbar.className = 'window-toolbar';
|
||||||
|
const completeness = calculateCompleteness(profile);
|
||||||
|
const completenessBadge = Components.Badge(`${completeness}% complete`, completeness > 80 ? 'success' : 'warning');
|
||||||
|
toolbar.appendChild(completenessBadge);
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'identity-center';
|
||||||
|
|
||||||
|
function field(label, value) {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'identity-row';
|
||||||
|
row.innerHTML = `<div class="label">${label}</div><div class="value">${value || 'Not set'}</div>`;
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
const profileCard = Components.Card({
|
||||||
|
title: 'Profile',
|
||||||
|
subtitle: 'One canonical record',
|
||||||
|
content: (() => {
|
||||||
|
const wrap = document.createElement('div');
|
||||||
|
wrap.appendChild(field('Name', profile.name));
|
||||||
|
wrap.appendChild(field('Legal name', profile.legal_name));
|
||||||
|
wrap.appendChild(field('Email', profile.email));
|
||||||
|
wrap.appendChild(field('Phone', profile.phone));
|
||||||
|
wrap.appendChild(field('Address', profile.address));
|
||||||
|
wrap.appendChild(field('Timezone', profile.timezone));
|
||||||
|
wrap.appendChild(field('Pronouns', profile.pronouns));
|
||||||
|
return wrap;
|
||||||
|
})()
|
||||||
|
});
|
||||||
|
|
||||||
|
const externalList = Components.List(Object.entries(profile.external_ids || {}).map(([key, val]) => ({
|
||||||
|
icon: '🔗',
|
||||||
|
title: key,
|
||||||
|
subtitle: val
|
||||||
|
})));
|
||||||
|
|
||||||
|
const connections = Components.Card({ title: 'Connected services', content: externalList || 'No connections yet' });
|
||||||
|
|
||||||
|
const howTo = Components.Card({
|
||||||
|
title: 'Reuse identity in apps',
|
||||||
|
content: `<ul class="identity-howto">
|
||||||
|
<li>Apps read from this profile instead of re-asking basic info.</li>
|
||||||
|
<li>Developers can call /api/identity/profile via the SDK.</li>
|
||||||
|
<li>External IDs keep GitHub/Railway/Discord linked without re-auth prompts.</li>
|
||||||
|
</ul>`
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(profileCard);
|
||||||
|
container.appendChild(connections);
|
||||||
|
container.appendChild(howTo);
|
||||||
|
|
||||||
|
window.OS.createWindow({
|
||||||
|
id: appId,
|
||||||
|
title: 'Identity Center',
|
||||||
|
icon: '🪪',
|
||||||
|
toolbar,
|
||||||
|
content: container,
|
||||||
|
width: '800px',
|
||||||
|
height: '650px'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function calculateCompleteness(profile) {
|
||||||
|
const filled = ['name', 'email', 'phone', 'address', 'timezone', 'pronouns'].filter(key => profile[key]);
|
||||||
|
let score = Math.round((filled.length / 6) * 100);
|
||||||
|
if (profile.avatar_url) score += 10;
|
||||||
|
return Math.min(score, 100);
|
||||||
|
}
|
||||||
@@ -22,29 +22,36 @@ window.NotificationsApp = function() {
|
|||||||
message: 'All notifications marked as read',
|
message: 'All notifications marked as read',
|
||||||
duration: 2000
|
duration: 2000
|
||||||
});
|
});
|
||||||
// Refresh would go here
|
renderContent();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const clearAllBtn = Components.Button('Clear All', {
|
const focusSelect = document.createElement('select');
|
||||||
type: 'danger',
|
focusSelect.innerHTML = `<option value="normal">Normal</option><option value="deep">Deep Work</option><option value="offline">Offline</option>`;
|
||||||
onClick: () => {
|
focusSelect.onchange = () => {
|
||||||
if (confirm('Clear all notifications?')) {
|
const mode = focusSelect.value;
|
||||||
|
window.OS.eventBus.emit('notifications:focus', { mode });
|
||||||
window.OS.showNotification({
|
window.OS.showNotification({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: 'Notifications Cleared',
|
title: 'Focus mode',
|
||||||
message: 'All notifications have been removed',
|
message: mode === 'deep' ? 'Only high-importance alerts will interrupt you' : mode === 'offline' ? 'Notifications will quietly queue' : 'All notifications enabled',
|
||||||
duration: 2000
|
duration: 2000
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
}
|
focusSelect.setAttribute('aria-label', 'Focus mode');
|
||||||
});
|
|
||||||
|
|
||||||
toolbar.appendChild(markAllReadBtn);
|
toolbar.appendChild(markAllReadBtn);
|
||||||
toolbar.appendChild(clearAllBtn);
|
toolbar.appendChild(focusSelect);
|
||||||
|
|
||||||
// Create content
|
// Create content
|
||||||
const content = createNotificationsContent();
|
const content = document.createElement('div');
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
content.innerHTML = '';
|
||||||
|
content.appendChild(createNotificationsContent());
|
||||||
|
};
|
||||||
|
|
||||||
|
renderContent();
|
||||||
|
|
||||||
// Create window
|
// Create window
|
||||||
window.OS.createWindow({
|
window.OS.createWindow({
|
||||||
|
|||||||
@@ -126,6 +126,38 @@ const MockData = {
|
|||||||
{ id: 'notif_004', type: 'success', title: 'Compliance Approved', message: 'Fee Schedule Update Email has been approved', timestamp: '2025-11-15 16:52:00', read: true }
|
{ id: 'notif_004', type: 'success', title: 'Compliance Approved', message: 'Fee Schedule Update Email has been approved', timestamp: '2025-11-15 16:52:00', read: true }
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Chaos Inbox capture items
|
||||||
|
captureItems: [
|
||||||
|
{ id: 1, type: 'note', raw_content: 'Call back Jamie re: brand refresh', source: 'mobile', tags: ['marketing'], status: 'inbox', created_at: '2025-11-12' },
|
||||||
|
{ id: 2, type: 'link', raw_content: 'https://example.com/roadchain-deck', source: 'web_capture', tags: ['roadchain'], status: 'clustered', created_at: '2025-11-10' },
|
||||||
|
{ id: 3, type: 'idea', raw_content: 'Course outline: GPU confidence bootcamp', source: 'manual', tags: ['education', 'hardware'], status: 'resurfaced', created_at: '2025-11-01' },
|
||||||
|
{ id: 4, type: 'screenshot', raw_content: 'Screenshot: confusing AWS invoice UI', source: 'desktop', tags: ['compliance'], status: 'inbox', created_at: '2025-10-28' }
|
||||||
|
],
|
||||||
|
|
||||||
|
captureClusters: [
|
||||||
|
{ id: 1, name: 'Hardware & PiOps', description: 'Troubleshooting notes and hardware tasks', item_ids: [3, 4] },
|
||||||
|
{ id: 2, name: 'Marketing & Brand', description: 'Content drafts and approvals', item_ids: [1, 2] }
|
||||||
|
],
|
||||||
|
|
||||||
|
// Unified identity profile
|
||||||
|
identityProfile: {
|
||||||
|
name: 'BlackRoad Pilot',
|
||||||
|
legal_name: 'BlackRoad Pilot',
|
||||||
|
email: 'pilot@blackroad.io',
|
||||||
|
phone: '+1-555-123-4567',
|
||||||
|
address: '1 Infinite Road, Neo City',
|
||||||
|
timezone: 'UTC',
|
||||||
|
pronouns: 'they/them',
|
||||||
|
avatar_url: '',
|
||||||
|
external_ids: { github: 'pilot', discord: 'pilot#0001' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// Creator workspace
|
||||||
|
creativeProjects: [
|
||||||
|
{ id: 1, title: 'RoadStudio Lite Launch Video', type: 'video', status: 'in_production', description: '3 minute walkthrough for creators', links_to_assets: ['https://drive.example.com/video'], revenue_streams: { youtube: 200 }, notes: 'Need new b-roll of OS desktop' },
|
||||||
|
{ id: 2, title: 'GPU Confidence Course', type: 'course', status: 'drafting', description: 'Micro-course to make GPUs less scary', links_to_assets: ['notion://gpu-course-outline'], revenue_streams: { preorders: 12 }, notes: 'Pair with PiOps demo' }
|
||||||
|
],
|
||||||
|
|
||||||
// Corporate Departments
|
// Corporate Departments
|
||||||
departments: [
|
departments: [
|
||||||
{ id: 'dept_hr', name: 'Human Resources', icon: '👥', color: '#5AF' },
|
{ id: 'dept_hr', name: 'Human Resources', icon: '👥', color: '#5AF' },
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class BlackRoadOS {
|
|||||||
this.eventBus = new EventEmitter();
|
this.eventBus = new EventEmitter();
|
||||||
this.windowsContainer = null;
|
this.windowsContainer = null;
|
||||||
this.taskbarWindows = null;
|
this.taskbarWindows = null;
|
||||||
|
this.commandPalette = null;
|
||||||
|
|
||||||
// App lifecycle hooks registry
|
// App lifecycle hooks registry
|
||||||
this.lifecycleHooks = {
|
this.lifecycleHooks = {
|
||||||
@@ -72,9 +73,7 @@ class BlackRoadOS {
|
|||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.ctrlKey && e.key === 'k') {
|
if (e.ctrlKey && e.key === 'k') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// TODO v0.2.0: Implement command palette
|
this.toggleCommandPalette();
|
||||||
// Should show searchable list of all apps, commands, and recent windows
|
|
||||||
console.log('⌨️ Command palette - coming in v0.2.0');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -591,6 +590,108 @@ class BlackRoadOS {
|
|||||||
return Array.from(this.windows.values());
|
return Array.from(this.windows.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle global command palette for unified search
|
||||||
|
*/
|
||||||
|
toggleCommandPalette() {
|
||||||
|
if (!this.commandPalette) {
|
||||||
|
this.buildCommandPalette();
|
||||||
|
}
|
||||||
|
const isVisible = this.commandPalette.classList.contains('open');
|
||||||
|
if (isVisible) {
|
||||||
|
this.commandPalette.classList.remove('open');
|
||||||
|
} else {
|
||||||
|
this.commandPalette.classList.add('open');
|
||||||
|
const input = this.commandPalette.querySelector('input');
|
||||||
|
input.value = '';
|
||||||
|
input.focus();
|
||||||
|
this.populatePaletteResults('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildCommandPalette() {
|
||||||
|
this.commandPalette = document.createElement('div');
|
||||||
|
this.commandPalette.className = 'command-palette';
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'text';
|
||||||
|
input.placeholder = 'Search apps, notes, and knowledge (Ctrl/Cmd + K)';
|
||||||
|
input.setAttribute('aria-label', 'Global search');
|
||||||
|
this.commandPalette.appendChild(input);
|
||||||
|
|
||||||
|
const results = document.createElement('div');
|
||||||
|
results.className = 'command-results';
|
||||||
|
this.commandPalette.appendChild(results);
|
||||||
|
|
||||||
|
input.addEventListener('input', (e) => this.populatePaletteResults(e.target.value));
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') this.toggleCommandPalette();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(this.commandPalette);
|
||||||
|
this.populatePaletteResults('');
|
||||||
|
}
|
||||||
|
|
||||||
|
populatePaletteResults(query) {
|
||||||
|
if (!this.commandPalette) return;
|
||||||
|
const resultsContainer = this.commandPalette.querySelector('.command-results');
|
||||||
|
resultsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
const lower = query.toLowerCase();
|
||||||
|
const appMatches = Object.values(window.AppRegistry).filter(app =>
|
||||||
|
app.name.toLowerCase().includes(lower) || app.description.toLowerCase().includes(lower)
|
||||||
|
);
|
||||||
|
|
||||||
|
const captureMatches = (window.MockData?.captureItems || []).filter(item =>
|
||||||
|
!query || (item.raw_content || '').toLowerCase().includes(lower)
|
||||||
|
).slice(0, 5);
|
||||||
|
|
||||||
|
const projectMatches = (window.MockData?.creativeProjects || []).filter(project =>
|
||||||
|
!query || project.title.toLowerCase().includes(lower)
|
||||||
|
).slice(0, 5);
|
||||||
|
|
||||||
|
const renderGroup = (title, items, onClick) => {
|
||||||
|
if (!items.length) return;
|
||||||
|
const group = document.createElement('div');
|
||||||
|
group.className = 'command-group';
|
||||||
|
const heading = document.createElement('div');
|
||||||
|
heading.className = 'command-group-title';
|
||||||
|
heading.textContent = title;
|
||||||
|
group.appendChild(heading);
|
||||||
|
items.forEach(item => {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'command-row';
|
||||||
|
row.textContent = item.label;
|
||||||
|
row.tabIndex = 0;
|
||||||
|
row.addEventListener('click', () => onClick(item));
|
||||||
|
row.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') onClick(item);
|
||||||
|
});
|
||||||
|
group.appendChild(row);
|
||||||
|
});
|
||||||
|
resultsContainer.appendChild(group);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderGroup('Apps', appMatches.map(app => ({ label: `${app.icon} ${app.name}`, id: app.id })), (item) => {
|
||||||
|
window.launchApp(item.id);
|
||||||
|
this.toggleCommandPalette();
|
||||||
|
});
|
||||||
|
|
||||||
|
renderGroup('Chaos Inbox', captureMatches.map(c => ({ label: `🌀 ${c.raw_content || c.type}`, id: 'chaos-inbox' })), (item) => {
|
||||||
|
window.launchApp(item.id);
|
||||||
|
this.toggleCommandPalette();
|
||||||
|
});
|
||||||
|
|
||||||
|
renderGroup('Creator projects', projectMatches.map(p => ({ label: `🎨 ${p.title}`, id: 'creator-studio' })), (item) => {
|
||||||
|
window.launchApp(item.id);
|
||||||
|
this.toggleCommandPalette();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resultsContainer.childElementCount) {
|
||||||
|
resultsContainer.textContent = 'No matches yet. Try searching for an app or project.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show a toast notification
|
* Show a toast notification
|
||||||
* @param {Object} options - Notification options
|
* @param {Object} options - Notification options
|
||||||
|
|||||||
@@ -127,6 +127,46 @@ const AppRegistry = {
|
|||||||
category: 'Corporate',
|
category: 'Corporate',
|
||||||
entry: window.CorporateApp,
|
entry: window.CorporateApp,
|
||||||
defaultSize: { width: '800px', height: '600px' }
|
defaultSize: { width: '800px', height: '600px' }
|
||||||
|
},
|
||||||
|
|
||||||
|
'chaos-inbox': {
|
||||||
|
id: 'chaos-inbox',
|
||||||
|
name: 'Chaos Inbox',
|
||||||
|
icon: '🌀',
|
||||||
|
description: 'All your scraps in one forgiving place',
|
||||||
|
category: 'Focus',
|
||||||
|
entry: window.ChaosInboxApp,
|
||||||
|
defaultSize: { width: '1100px', height: '720px' }
|
||||||
|
},
|
||||||
|
|
||||||
|
'identity-center': {
|
||||||
|
id: 'identity-center',
|
||||||
|
name: 'Identity Center',
|
||||||
|
icon: '🪪',
|
||||||
|
description: 'Your info once, used everywhere',
|
||||||
|
category: 'System',
|
||||||
|
entry: window.IdentityCenterApp,
|
||||||
|
defaultSize: { width: '800px', height: '650px' }
|
||||||
|
},
|
||||||
|
|
||||||
|
'creator-studio': {
|
||||||
|
id: 'creator-studio',
|
||||||
|
name: 'Creator Studio',
|
||||||
|
icon: '🎨',
|
||||||
|
description: 'Home base for creative work',
|
||||||
|
category: 'Creators',
|
||||||
|
entry: window.CreatorStudioApp,
|
||||||
|
defaultSize: { width: '1000px', height: '700px' }
|
||||||
|
},
|
||||||
|
|
||||||
|
'compliance-ops': {
|
||||||
|
id: 'compliance-ops',
|
||||||
|
name: 'Compliance & Ops',
|
||||||
|
icon: '🧭',
|
||||||
|
description: 'Transparent logs & workflows',
|
||||||
|
category: 'Compliance',
|
||||||
|
entry: window.ComplianceOpsApp,
|
||||||
|
defaultSize: { width: '900px', height: '650px' }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
class ThemeManager {
|
class ThemeManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.currentTheme = 'tealOS';
|
this.currentTheme = 'tealOS';
|
||||||
this.availableThemes = ['tealOS', 'nightOS']; // Extensible list
|
this.availableThemes = ['tealOS', 'nightOS', 'contrastOS']; // Extensible list
|
||||||
// TODO v0.2.0: Load available themes dynamically from CSS
|
// TODO v0.2.0: Load available themes dynamically from CSS
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
@@ -80,8 +80,10 @@ class ThemeManager {
|
|||||||
* TODO v0.2.0: Support more than 2 themes with dropdown or cycle logic
|
* TODO v0.2.0: Support more than 2 themes with dropdown or cycle logic
|
||||||
*/
|
*/
|
||||||
toggleTheme() {
|
toggleTheme() {
|
||||||
// Simple toggle for now (2 themes)
|
// Cycle through available themes
|
||||||
this.currentTheme = this.currentTheme === 'tealOS' ? 'nightOS' : 'tealOS';
|
const currentIndex = this.availableThemes.indexOf(this.currentTheme);
|
||||||
|
const nextIndex = (currentIndex + 1) % this.availableThemes.length;
|
||||||
|
this.currentTheme = this.availableThemes[nextIndex];
|
||||||
|
|
||||||
this.applyTheme(this.currentTheme);
|
this.applyTheme(this.currentTheme);
|
||||||
this.saveTheme();
|
this.saveTheme();
|
||||||
@@ -143,13 +145,13 @@ class ThemeManager {
|
|||||||
|
|
||||||
const icon = toggleBtn.querySelector('.icon');
|
const icon = toggleBtn.querySelector('.icon');
|
||||||
if (icon) {
|
if (icon) {
|
||||||
// Sun for dark theme (clicking will go to light)
|
const iconMap = { tealOS: '🌙', nightOS: '☀️', contrastOS: '⚡️' };
|
||||||
// Moon for light theme (clicking will go to dark)
|
icon.textContent = iconMap[this.currentTheme] || '🎨';
|
||||||
icon.textContent = this.currentTheme === 'tealOS' ? '🌙' : '☀️';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update aria-label for clarity
|
// Update aria-label for clarity
|
||||||
const nextTheme = this.currentTheme === 'tealOS' ? 'Night OS' : 'Teal OS';
|
const currentIndex = this.availableThemes.indexOf(this.currentTheme);
|
||||||
|
const nextTheme = this.availableThemes[(currentIndex + 1) % this.availableThemes.length] || 'Teal OS';
|
||||||
toggleBtn.setAttribute('aria-label', `Switch to ${nextTheme}`);
|
toggleBtn.setAttribute('aria-label', `Switch to ${nextTheme}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
7
docs/ACCESSIBILITY_NOTES.md
Normal file
7
docs/ACCESSIBILITY_NOTES.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Accessibility Notes (v0.2)
|
||||||
|
|
||||||
|
- Added **contrastOS** theme for high-contrast users; theme manager cycles through three presets.
|
||||||
|
- Global command palette (Ctrl/Cmd + K) is keyboard navigable and labeled for screen readers.
|
||||||
|
- Desktop icons/start menu already expose `aria-label`/keyboard handlers; palette and new apps keep focusable controls.
|
||||||
|
- Notification Center shows focus modes to minimize interruption; quick batch actions reduce cognitive load.
|
||||||
|
- Remaining gaps: full keyboard traversal inside every app, color contrast audit across legacy apps, and screen reader labels for dynamic tables.
|
||||||
35
docs/DESIGN_V0_2.md
Normal file
35
docs/DESIGN_V0_2.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# BlackRoad OS v0.2 Design Note
|
||||||
|
|
||||||
|
## Pillars
|
||||||
|
- **Chaos & Neurodivergent Support:** generic CaptureItem model + Chaos Inbox UI to hold loose scraps, clusters, and resurfacing.
|
||||||
|
- **Unified Identity & Duplication Killer:** UserProfile model + Identity Center app exposes a single canonical record for apps.
|
||||||
|
- **Attention & Notification Engine:** Notification model/API plus Notification Center focus modes.
|
||||||
|
- **Unified Search & Knowledge:** lightweight command palette (Ctrl/Cmd+K) that searches apps, captured items, and creator projects; backend search endpoint for future plumbing.
|
||||||
|
- **Creator Workspace Baseline:** CreativeProject model + Creator Studio to centralize creative work and assets.
|
||||||
|
- **Enterprise & Compliance Surface:** ComplianceEvent model + Compliance & Ops UI for audits/workflows.
|
||||||
|
- **Hardware & Pi Ops Visibility:** Pi Ops kept in registry; Chaos clusters track hardware notes; hooks for energy/compute tagging documented.
|
||||||
|
- **Accessibility & UX:** High-contrast theme, keyboard-friendly palette, ARIA labels for launcher/palette.
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
- `CaptureItem` + `CaptureCluster` (capture.py) for multi-modal scraps with tags/status.
|
||||||
|
- `UserProfile` (identity_profile.py) canonical identity, external IDs.
|
||||||
|
- `Notification` (notification.py) app-level alerts with importance/delivery.
|
||||||
|
- `CreativeProject` (creator.py) type/status/assets/revenue/notes.
|
||||||
|
- `ComplianceEvent` (compliance_event.py) actor/action/resource/severity metadata.
|
||||||
|
|
||||||
|
## APIs
|
||||||
|
- Capture: `POST/GET /api/capture/items`, tagging, status, clusters.
|
||||||
|
- Identity: `GET/PUT /api/identity/profile`, linked accounts, link external IDs.
|
||||||
|
- Notifications: `POST/GET /api/notifications`, mark read.
|
||||||
|
- Creator: CRUD under `/api/creator/projects`.
|
||||||
|
- Compliance: `/api/compliance/events` list.
|
||||||
|
- Search: `/api/search?q=` unified lookup scaffold.
|
||||||
|
|
||||||
|
## Frontend Surfaces
|
||||||
|
- New apps: Chaos Inbox, Identity Center, Creator Studio, Compliance & Ops.
|
||||||
|
- Notification Center adds focus modes; command palette overlays globally.
|
||||||
|
- High-contrast theme added to theme cycle; new CSS for command palette and apps.
|
||||||
|
|
||||||
|
## Safety & Next Steps
|
||||||
|
- All models auto-migrate via SQLAlchemy create_all; endpoints gated by `get_current_active_user`.
|
||||||
|
- Future work: agent-powered clustering, real notifications toasts->backend, Pi energy telemetry, app SDK hook for identity fetch.
|
||||||
15
docs/OS_V0_2_PAIN_MAPPING.md
Normal file
15
docs/OS_V0_2_PAIN_MAPPING.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# BlackRoad OS v0.2 – Pain Mapping
|
||||||
|
|
||||||
|
This release connects PRD pain clusters from **THE NEW AGE** to concrete OS surfaces.
|
||||||
|
|
||||||
|
| Pain Cluster | OS Response | Paths/Modules |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Fragmentation & duplication | Identity Center centralizes profile + external IDs; unified search surfaces apps/data. | `backend/app/routers/identity_center.py`, `backend/app/models/identity_profile.py`, `blackroad-os/js/apps/identity_center.js`, `blackroad-os/js/os.js` |
|
||||||
|
| Neurodivergent hostility / chaos | Chaos Inbox collects notes, links, screenshots, resurfacing suggestions. | `backend/app/models/capture.py`, `backend/app/routers/capture.py`, `blackroad-os/js/apps/chaos_inbox.js`, `blackroad-os/js/assets/apps.css` |
|
||||||
|
| Notification apocalypse | Notification Center focus modes + centralized API. | `backend/app/models/notification.py`, `backend/app/routers/notifications_center.py`, `blackroad-os/js/apps/notifications.js` |
|
||||||
|
| Creator extraction | Creator Studio organizes creative projects and assets. | `backend/app/models/creator.py`, `backend/app/routers/creator.py`, `blackroad-os/js/apps/creator_studio.js` |
|
||||||
|
| Legacy enterprise & compliance | Compliance & Ops surface for audits/workflows. | `backend/app/models/compliance_event.py`, `backend/app/routers/compliance_ops.py`, `blackroad-os/js/apps/compliance_ops.js` |
|
||||||
|
| Attention management & knowledge | Command palette / unified search entry point. | `backend/app/routers/search.py`, `blackroad-os/js/os.js`, `blackroad-os/assets/styles.css` |
|
||||||
|
| Hardware fear & Pi visibility | Devices remain in PiOps app; Chaos clusters resurface hardware notes. | `blackroad-os/js/apps/pi_ops.js`, `blackroad-os/js/apps/chaos_inbox.js` |
|
||||||
|
| Accessibility crisis | Added high-contrast theme + keyboard palette + ARIA prompts. | `blackroad-os/js/theme.js`, `blackroad-os/assets/styles.css` |
|
||||||
|
|
||||||
Reference in New Issue
Block a user