diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index dcb228082..5c6aec91d 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -225,7 +225,7 @@ jobs: run: | umask 077 strix_llm_file="$RUNNER_TEMP/strix_llm.txt" - printf '%s' "github_models/gpt-5.4" > "$strix_llm_file" + printf '%s' "github_models/gpt-4o" > "$strix_llm_file" echo "STRIX_LLM_FILE=$strix_llm_file" >> "$GITHUB_ENV" - name: Prepare LLM API base input file diff --git a/.vooster/project.json b/.vooster/project.json new file mode 100644 index 000000000..ee8c3733e --- /dev/null +++ b/.vooster/project.json @@ -0,0 +1,6 @@ +{ + "uid": "XIDB", + "name": "Naruon - AI Email Workspace", + "description": null, + "connectedAt": "2026-05-24T13:23:50.376Z" +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index f4c703fb4..6dc04d32a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ explicit `if: ${{ always() }}` upload steps when needed. - Prefer upgrading or removing vulnerable dependencies over downgrading patched packages unless compatibility evidence is recorded in the PR. -- Strix Security Scan must use `github_models/gpt-5.4` as the default model to bypass Vertex AI GCP credential prerequisites in PR bounds. +- Strix Security Scan must use `github_models/gpt-4o` as the default model to bypass Vertex AI GCP credential prerequisites in PR bounds. ## PR automation and review defaults diff --git a/backend/__pycache__/import_fixtures.cpython-310.pyc b/backend/__pycache__/import_fixtures.cpython-310.pyc index 6b82747ac..f7756fafd 100644 Binary files a/backend/__pycache__/import_fixtures.cpython-310.pyc and b/backend/__pycache__/import_fixtures.cpython-310.pyc differ diff --git a/backend/__pycache__/main.cpython-310.pyc b/backend/__pycache__/main.cpython-310.pyc index ce3e242f3..89c7310be 100644 Binary files a/backend/__pycache__/main.cpython-310.pyc and b/backend/__pycache__/main.cpython-310.pyc differ diff --git a/backend/api/__pycache__/accounts.cpython-310.pyc b/backend/api/__pycache__/accounts.cpython-310.pyc index 11dfc3750..76857ea17 100644 Binary files a/backend/api/__pycache__/accounts.cpython-310.pyc and b/backend/api/__pycache__/accounts.cpython-310.pyc differ diff --git a/backend/api/__pycache__/dav.cpython-310.pyc b/backend/api/__pycache__/dav.cpython-310.pyc index 8b768e29d..6c7e66965 100644 Binary files a/backend/api/__pycache__/dav.cpython-310.pyc and b/backend/api/__pycache__/dav.cpython-310.pyc differ diff --git a/backend/api/__pycache__/emails.cpython-310.pyc b/backend/api/__pycache__/emails.cpython-310.pyc index e9e36969a..6630b78fd 100644 Binary files a/backend/api/__pycache__/emails.cpython-310.pyc and b/backend/api/__pycache__/emails.cpython-310.pyc differ diff --git a/backend/api/__pycache__/ontology.cpython-310.pyc b/backend/api/__pycache__/ontology.cpython-310.pyc index 3c28b027f..9e9e34baa 100644 Binary files a/backend/api/__pycache__/ontology.cpython-310.pyc and b/backend/api/__pycache__/ontology.cpython-310.pyc differ diff --git a/backend/api/__pycache__/runner_ws.cpython-310.pyc b/backend/api/__pycache__/runner_ws.cpython-310.pyc index 4d038d82f..36432059d 100644 Binary files a/backend/api/__pycache__/runner_ws.cpython-310.pyc and b/backend/api/__pycache__/runner_ws.cpython-310.pyc differ diff --git a/backend/api/webdav.py b/backend/api/webdav.py new file mode 100644 index 000000000..c49f85752 --- /dev/null +++ b/backend/api/webdav.py @@ -0,0 +1,28 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from pydantic import BaseModel +from typing import List + +from api.auth import get_auth_context, AuthContext +from services.webdav_service import webdav_service + +router = APIRouter(prefix="/api/webdav", tags=["webdav"]) + +class WebdavAccountResponse(BaseModel): + account_id: int + server_url: str + username: str + +class ProjectFolderResponse(BaseModel): + folder_id: int + project_name: str + webdav_path: str + +@router.get("/accounts", response_model=List[WebdavAccountResponse]) +async def get_webdav_accounts(auth_context: AuthContext = Depends(get_auth_context)): + user_id = auth_context.user_id + return webdav_service.get_connected_accounts(user_id) + +@router.get("/folders", response_model=List[ProjectFolderResponse]) +async def get_project_folders(auth_context: AuthContext = Depends(get_auth_context)): + user_id = auth_context.user_id + return webdav_service.get_project_folders(user_id) diff --git a/backend/core/__pycache__/config.cpython-310.pyc b/backend/core/__pycache__/config.cpython-310.pyc index d44a189f2..e5c66dce8 100644 Binary files a/backend/core/__pycache__/config.cpython-310.pyc and b/backend/core/__pycache__/config.cpython-310.pyc differ diff --git a/backend/db/__pycache__/models.cpython-310.pyc b/backend/db/__pycache__/models.cpython-310.pyc index a7c6335bf..6def96442 100644 Binary files a/backend/db/__pycache__/models.cpython-310.pyc and b/backend/db/__pycache__/models.cpython-310.pyc differ diff --git a/backend/db/models.py b/backend/db/models.py index c7cf85322..1bd6d15fe 100644 --- a/backend/db/models.py +++ b/backend/db/models.py @@ -339,3 +339,30 @@ class SenderRelationship(Base): default=lambda: datetime.datetime.now(datetime.timezone.utc), onupdate=lambda: datetime.datetime.now(datetime.timezone.utc), ) + + +class WebdavAccount(Base): + __tablename__ = "webdav_accounts" + + id: Mapped[int] = mapped_column("account_id", primary_key=True) + user_id: Mapped[str] = mapped_column(String, index=True, nullable=False) + server_url: Mapped[str] = mapped_column(String, nullable=False) + username: Mapped[str] = mapped_column(String, nullable=False) + credentials_encrypted: Mapped[str] = mapped_column(EncryptedString, nullable=False) + created_at: Mapped[datetime.datetime] = mapped_column( + DateTime(timezone=True), + default=lambda: datetime.datetime.now(datetime.timezone.utc), + ) + + +class ProjectFolder(Base): + __tablename__ = "project_folders" + + id: Mapped[int] = mapped_column("folder_id", primary_key=True) + user_id: Mapped[str] = mapped_column(String, index=True, nullable=False) + project_name: Mapped[str] = mapped_column(String, index=True, nullable=False) + webdav_path: Mapped[str] = mapped_column(String, nullable=False) + created_at: Mapped[datetime.datetime] = mapped_column( + DateTime(timezone=True), + default=lambda: datetime.datetime.now(datetime.timezone.utc), + ) diff --git a/backend/main.py b/backend/main.py index 982265831..811ae48d5 100644 --- a/backend/main.py +++ b/backend/main.py @@ -18,6 +18,7 @@ from api.runner_ws import router as runner_ws_router from api.dav import router as dav_router from api.accounts import router as accounts_router +from api.webdav import router as webdav_router from services.imap_worker import ImapSyncWorker from prometheus_fastapi_instrumentator import Instrumentator from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor @@ -92,6 +93,7 @@ async def lifespan(app: FastAPI): app.include_router(runner_ws_router) app.include_router(dav_router) app.include_router(accounts_router, dependencies=PRIVATE_API_DEPENDENCIES) +app.include_router(webdav_router, dependencies=PRIVATE_API_DEPENDENCIES) app.add_middleware( diff --git a/backend/requirements.txt b/backend/requirements.txt index 745c55663..b51a97f4a 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -24,3 +24,4 @@ opentelemetry-sdk==1.25.0 opentelemetry-instrumentation-fastapi==0.46b0 opentelemetry-exporter-otlp==1.25.0 setuptools==78.1.1 +websockets==14.1 diff --git a/backend/runner/connector.py b/backend/runner/connector.py new file mode 100644 index 000000000..85cdb8725 --- /dev/null +++ b/backend/runner/connector.py @@ -0,0 +1,74 @@ +import asyncio +import logging +from typing import Dict, Any + +try: + import websockets +except ImportError: + # Optional dependency for the runner + websockets = None + +logger = logging.getLogger(__name__) + +class SelfHostedConnector: + def __init__(self, target_ws_url: str, token: str): + self.target_ws_url = target_ws_url + self.token = token + self.connection = None + self.is_connected = False + + async def connect(self): + if websockets is None: + logger.error("websockets library is not installed. Runner cannot start.") + return + + headers = {"Authorization": f"Bearer {self.token}"} + try: + self.connection = await websockets.connect(self.target_ws_url, additional_headers=headers) + self.is_connected = True + logger.info(f"Connected to Naruon Gateway at {self.target_ws_url}") + await self._listen_loop() + except asyncio.CancelledError: + raise + except (ConnectionRefusedError, OSError, asyncio.TimeoutError) as e: + self.is_connected = False + logger.exception(f"Failed to connect to Naruon Gateway: {e}") + except Exception as e: + # We catch Exception here but limit logger.exception to websocket errors + # to avoid referencing websockets.exceptions directly in the tuple + # in case websockets is None (though we return early if it is) + if websockets and isinstance(e, websockets.exceptions.WebSocketException): + self.is_connected = False + logger.exception(f"Failed to connect to Naruon Gateway: {e}") + else: + self.is_connected = False + logger.exception(f"Failed to connect to Naruon Gateway with unexpected error: {e}") + + async def _listen_loop(self): + if not self.connection: + return + try: + while self.is_connected: + message = await self.connection.recv() + await self.handle_message(message) + except Exception as e: + if websockets and isinstance(e, websockets.exceptions.ConnectionClosed): + logger.warning("Connection closed by remote gateway.") + else: + logger.warning(f"Connection loop ended: {e}") + self.is_connected = False + + async def handle_message(self, message: str | bytes): + # Dispatch message to internal SMTP/IMAP proxy handlers + logger.debug(f"Received instruction from gateway: {message}") + pass + + async def send_response(self, response: Dict[str, Any]): + if self.is_connected and self.connection: + import json + await self.connection.send(json.dumps(response)) + +if __name__ == "__main__": + # Example usage for local bootstrap + connector = SelfHostedConnector("ws://localhost:8080/api/runner/ws", "sample-token") + asyncio.run(connector.connect()) diff --git a/backend/schema/connector.py b/backend/schema/connector.py new file mode 100644 index 000000000..0bb361890 --- /dev/null +++ b/backend/schema/connector.py @@ -0,0 +1,33 @@ +from typing import Literal +from pydantic import BaseModel, ConfigDict, Field + + +class SelfHostedConnectorRegistrationRequest(BaseModel): + model_config = ConfigDict(extra="forbid") + + connector_id: str = Field(..., description="Unique identifier for the self-hosted connector") + public_key: str = Field(..., description="Public key for mTLS or secure payload exchange") + supported_protocols: list[Literal["imap", "smtp", "pop3", "caldav", "webdav"]] = Field( + default_factory=list, description="Protocols supported by this connector instance" + ) + capabilities: list[str] = Field(default_factory=list) + + +class SelfHostedConnectorRegistrationResponse(BaseModel): + model_config = ConfigDict(extra="forbid") + + connector_id: str + status: Literal["pending_approval", "active", "rejected"] + issued_certificate: str | None = None + endpoint_url: str + + +class OIDCGatewayConfig(BaseModel): + model_config = ConfigDict(extra="forbid") + + provider_name: str + issuer_url: str + client_id: str + audience: str + required_scopes: list[str] + enforce_rbac_sync: bool = True diff --git a/backend/scripts/__pycache__/bootstrap_db.cpython-310.pyc b/backend/scripts/__pycache__/bootstrap_db.cpython-310.pyc index e6e760edb..dd36d007c 100644 Binary files a/backend/scripts/__pycache__/bootstrap_db.cpython-310.pyc and b/backend/scripts/__pycache__/bootstrap_db.cpython-310.pyc differ diff --git a/backend/scripts/__pycache__/import_fixtures.cpython-310.pyc b/backend/scripts/__pycache__/import_fixtures.cpython-310.pyc index fad00d8c4..c9b90c2e6 100644 Binary files a/backend/scripts/__pycache__/import_fixtures.cpython-310.pyc and b/backend/scripts/__pycache__/import_fixtures.cpython-310.pyc differ diff --git a/backend/services/__pycache__/access_policy.cpython-310.pyc b/backend/services/__pycache__/access_policy.cpython-310.pyc index 40abb469a..0bada95b0 100644 Binary files a/backend/services/__pycache__/access_policy.cpython-310.pyc and b/backend/services/__pycache__/access_policy.cpython-310.pyc differ diff --git a/backend/services/__pycache__/archive.cpython-310.pyc b/backend/services/__pycache__/archive.cpython-310.pyc index 4d4b1d364..6ab485f0c 100644 Binary files a/backend/services/__pycache__/archive.cpython-310.pyc and b/backend/services/__pycache__/archive.cpython-310.pyc differ diff --git a/backend/services/__pycache__/calendar_sync.cpython-310.pyc b/backend/services/__pycache__/calendar_sync.cpython-310.pyc index d598e6d18..7d9ba9ea3 100644 Binary files a/backend/services/__pycache__/calendar_sync.cpython-310.pyc and b/backend/services/__pycache__/calendar_sync.cpython-310.pyc differ diff --git a/backend/services/__pycache__/knowledge_extractor.cpython-310.pyc b/backend/services/__pycache__/knowledge_extractor.cpython-310.pyc index 1acf72b00..2483ac083 100644 Binary files a/backend/services/__pycache__/knowledge_extractor.cpython-310.pyc and b/backend/services/__pycache__/knowledge_extractor.cpython-310.pyc differ diff --git a/backend/services/__pycache__/text_safety.cpython-310.pyc b/backend/services/__pycache__/text_safety.cpython-310.pyc index 9b9445107..28bc7e7a3 100644 Binary files a/backend/services/__pycache__/text_safety.cpython-310.pyc and b/backend/services/__pycache__/text_safety.cpython-310.pyc differ diff --git a/backend/services/__pycache__/threading_service.cpython-310.pyc b/backend/services/__pycache__/threading_service.cpython-310.pyc index 638f3bf1c..76b126945 100644 Binary files a/backend/services/__pycache__/threading_service.cpython-310.pyc and b/backend/services/__pycache__/threading_service.cpython-310.pyc differ diff --git a/backend/services/caldav_service.py b/backend/services/caldav_service.py new file mode 100644 index 000000000..3384f5850 --- /dev/null +++ b/backend/services/caldav_service.py @@ -0,0 +1,29 @@ +import logging +from typing import Dict, Any + +logger = logging.getLogger(__name__) + +class CalDavService: + def __init__(self): + pass + + def determine_writeback_target(self, task_context: Dict[str, Any], connected_accounts: list) -> str: + """ + Determines the most appropriate CalDav account to write back to, + based on the context of the task (e.g., if it originated from a company email). + """ + # Basic ontology/context mock logic + source_email = task_context.get("source_email", "") + if isinstance(source_email, str) and "@" in source_email: + source_domain = source_email.strip().lower().rsplit("@", 1)[-1] + for account in connected_accounts: + account_domain = str(account.get("domain", "")).lower().strip() + if account_domain and source_domain == account_domain: + return account.get("account_id") + + # Fallback to the primary account + if connected_accounts: + return connected_accounts[0].get("account_id") + return "default_system_caldav" + +caldav_service = CalDavService() diff --git a/backend/services/email_service.py b/backend/services/email_service.py new file mode 100644 index 000000000..aea9432e6 --- /dev/null +++ b/backend/services/email_service.py @@ -0,0 +1,50 @@ +import hashlib +import logging +from typing import Dict, Any +import email.utils + +logger = logging.getLogger(__name__) + +def generate_email_fingerprint(email_data: Dict[str, Any]) -> str: + """ + Generates a unique fingerprint for an email based on its sender, subject, date, and body content. + Used to de-duplicate emails from ZIP imports or forwarding loops. + """ + sender = str(email_data.get("sender") or "") + subject = str(email_data.get("subject") or "") + date = str(email_data.get("date") or "") + body = str(email_data.get("body") or "") + body_snippet = body[:500] # First 500 chars + + raw_str = f"{sender}|{subject}|{date}|{body_snippet}" + return hashlib.sha256(raw_str.encode("utf-8")).hexdigest() + +def detect_reply_tracking(email_data: Dict[str, Any]) -> bool: + """ + Detects if the user sent an email that expects a reply. + """ + body = str(email_data.get("body") or "").lower() + return "please reply" in body or "?" in body + +def process_self_to_self(email_data: Dict[str, Any], user_email: str) -> bool: + """ + Detects if an email is sent from the user to themselves, turning it into a knowledge node. + """ + sender_raw = str(email_data.get("sender") or "") + recipients_raw = email_data.get("recipients") or [] + recipient_inputs = recipients_raw if isinstance(recipients_raw, list) else [recipients_raw] + recipient_inputs = [str(v) for v in recipient_inputs] + + _, sender_addr = email.utils.parseaddr(sender_raw) + normalized_user = user_email.strip().lower() + normalized_sender = sender_addr.strip().lower() + parsed_recipients = { + addr.strip().lower() + for _, addr in email.utils.getaddresses(recipient_inputs) + if addr + } + + if normalized_user and normalized_user == normalized_sender and normalized_user in parsed_recipients: + logger.info("Self-to-self email detected. Organizing as knowledge node.") + return True + return False diff --git a/backend/services/ontology_service.py b/backend/services/ontology_service.py new file mode 100644 index 000000000..e69c4b237 --- /dev/null +++ b/backend/services/ontology_service.py @@ -0,0 +1,36 @@ +import logging +from typing import Dict, Any + +logger = logging.getLogger(__name__) + +class OntologyService: + def __init__(self): + self.relationships = {} + + def analyze_sender_relationship(self, user_email: str, sender_email: str, email_content: str) -> Dict[str, Any]: + """ + Analyzes the email content to build a relationship graph (DAG) between the user and the sender. + Returns attributes like the relationship type (e.g., Colleague, Client, Newsletter, Unknown) + and confidence score. + """ + # A simple stub logic for Phase 10 implementation + relationship_type = "Unknown" + confidence = 0.5 + + if "unsubscribe" in email_content.lower(): + relationship_type = "Newsletter" + confidence = 0.9 + elif "@" in user_email and "@" in sender_email: + user_domain = user_email.split("@")[1].lower() + sender_domain = sender_email.split("@")[1].lower() + if user_domain == sender_domain: + relationship_type = "Colleague" + confidence = 0.85 + + logger.info(f"Analyzed relationship: {sender_email} -> {relationship_type} (conf: {confidence})") + return { + "type": relationship_type, + "confidence": confidence + } + +ontology_service = OntologyService() diff --git a/backend/services/webdav_service.py b/backend/services/webdav_service.py new file mode 100644 index 000000000..1e1a74482 --- /dev/null +++ b/backend/services/webdav_service.py @@ -0,0 +1,53 @@ +import logging +from typing import Dict, Any, List + +logger = logging.getLogger(__name__) + +class WebDavService: + def __init__(self): + self._mock_accounts = { + "demo_user": [ + { + "account_id": 1, + "server_url": "https://webdav.naruon.net", + "username": "demo_user" + } + ] + } + self._mock_folders = { + "demo_user": [ + { + "folder_id": 1, + "project_name": "Naruon Roadmap 2026", + "webdav_path": "/Projects/Naruon_Roadmap_2026" + }, + { + "folder_id": 2, + "project_name": "Marketing Assets", + "webdav_path": "/Projects/Marketing_Assets" + } + ] + } + + def get_connected_accounts(self, user_id: str) -> List[Dict[str, Any]]: + """ + Fetch connected WebDAV accounts for a user. + In a real implementation, this queries the database. + """ + return self._mock_accounts.get(user_id, []) + + def get_project_folders(self, user_id: str) -> List[Dict[str, Any]]: + """ + Fetch the list of project folders structured by AI. + """ + return self._mock_folders.get(user_id, []) + + def sync_attachments_to_folder(self, email_id: str, project_name: str) -> bool: + """ + Organizes an email's attachments into the specified WebDAV project folder. + """ + logger.info(f"Syncing attachments from email {email_id} to project {project_name}") + # Mock implementation: in reality, this would download from storage and upload via webdavclient3 + return True + +webdav_service = WebDavService() diff --git a/backend/tests/__pycache__/test_access_policy.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_access_policy.cpython-310-pytest-9.0.3.pyc index 8f360feb5..341e2d085 100644 Binary files a/backend/tests/__pycache__/test_access_policy.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_access_policy.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_accounts_api.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_accounts_api.cpython-310-pytest-9.0.3.pyc index 8d504ddfd..675bee478 100644 Binary files a/backend/tests/__pycache__/test_accounts_api.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_accounts_api.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_apm_observability.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_apm_observability.cpython-310-pytest-9.0.3.pyc index 01af927fc..c4647a9db 100644 Binary files a/backend/tests/__pycache__/test_apm_observability.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_apm_observability.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_archive.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_archive.cpython-310-pytest-9.0.3.pyc index ca87ba819..55bd29098 100644 Binary files a/backend/tests/__pycache__/test_archive.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_archive.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_auth_real.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_auth_real.cpython-310-pytest-9.0.3.pyc index 93d7956e0..74900e385 100644 Binary files a/backend/tests/__pycache__/test_auth_real.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_auth_real.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_bootstrap_db.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_bootstrap_db.cpython-310-pytest-9.0.3.pyc index 84fae8162..300d4e46d 100644 Binary files a/backend/tests/__pycache__/test_bootstrap_db.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_bootstrap_db.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_calendar_api.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_calendar_api.cpython-310-pytest-9.0.3.pyc index 4349cc3b4..e9edcab54 100644 Binary files a/backend/tests/__pycache__/test_calendar_api.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_calendar_api.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_calendar_service.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_calendar_service.cpython-310-pytest-9.0.3.pyc index b0e930e47..ca59ab335 100644 Binary files a/backend/tests/__pycache__/test_calendar_service.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_calendar_service.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_calendar_sync.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_calendar_sync.cpython-310-pytest-9.0.3.pyc index 43d58cde9..d114acff6 100644 Binary files a/backend/tests/__pycache__/test_calendar_sync.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_calendar_sync.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_config.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_config.cpython-310-pytest-9.0.3.pyc index d64014d66..320b6b54f 100644 Binary files a/backend/tests/__pycache__/test_config.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_config.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_dav_api.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_dav_api.cpython-310-pytest-9.0.3.pyc index 5e954f7e6..29867dbf8 100644 Binary files a/backend/tests/__pycache__/test_dav_api.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_dav_api.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_email_client.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_email_client.cpython-310-pytest-9.0.3.pyc index 04b7571a9..9311b108a 100644 Binary files a/backend/tests/__pycache__/test_email_client.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_email_client.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_email_client_smtp.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_email_client_smtp.cpython-310-pytest-9.0.3.pyc index e32d1f0d8..916ab4291 100644 Binary files a/backend/tests/__pycache__/test_email_client_smtp.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_email_client_smtp.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_email_parser.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_email_parser.cpython-310-pytest-9.0.3.pyc index b17bb0acd..a37d0b47d 100644 Binary files a/backend/tests/__pycache__/test_email_parser.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_email_parser.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_emails_api.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_emails_api.cpython-310-pytest-9.0.3.pyc index d1f8ec64e..c96853f7f 100644 Binary files a/backend/tests/__pycache__/test_emails_api.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_emails_api.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_import_fixtures.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_import_fixtures.cpython-310-pytest-9.0.3.pyc index 5abd7293a..df1092b54 100644 Binary files a/backend/tests/__pycache__/test_import_fixtures.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_import_fixtures.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_knowledge_extractor.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_knowledge_extractor.cpython-310-pytest-9.0.3.pyc index 279fcfd19..2394ec4fd 100644 Binary files a/backend/tests/__pycache__/test_knowledge_extractor.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_knowledge_extractor.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_llm_api.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_llm_api.cpython-310-pytest-9.0.3.pyc index f920b885f..7e7567e7a 100644 Binary files a/backend/tests/__pycache__/test_llm_api.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_llm_api.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_llm_providers_api.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_llm_providers_api.cpython-310-pytest-9.0.3.pyc index 23b145203..e73817c49 100644 Binary files a/backend/tests/__pycache__/test_llm_providers_api.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_llm_providers_api.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_llm_service.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_llm_service.cpython-310-pytest-9.0.3.pyc index 0b5232d48..8d07951e7 100644 Binary files a/backend/tests/__pycache__/test_llm_service.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_llm_service.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_main.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_main.cpython-310-pytest-9.0.3.pyc index d64496297..75f8ae1b3 100644 Binary files a/backend/tests/__pycache__/test_main.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_main.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_network_api.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_network_api.cpython-310-pytest-9.0.3.pyc index e79f4c8ed..41e561563 100644 Binary files a/backend/tests/__pycache__/test_network_api.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_network_api.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_ontology_api.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_ontology_api.cpython-310-pytest-9.0.3.pyc index b36748ac1..649f3f5f8 100644 Binary files a/backend/tests/__pycache__/test_ontology_api.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_ontology_api.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_prompts_api.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_prompts_api.cpython-310-pytest-9.0.3.pyc index 894c0d4f5..5064144be 100644 Binary files a/backend/tests/__pycache__/test_prompts_api.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_prompts_api.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_release_governance.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_release_governance.cpython-310-pytest-9.0.3.pyc index 570652d5a..ecdc7feb2 100644 Binary files a/backend/tests/__pycache__/test_release_governance.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_release_governance.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_repo_hygiene.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_repo_hygiene.cpython-310-pytest-9.0.3.pyc index f14129725..c78ba6594 100644 Binary files a/backend/tests/__pycache__/test_repo_hygiene.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_repo_hygiene.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_runtime_config_api.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_runtime_config_api.cpython-310-pytest-9.0.3.pyc index 2bfdbf51d..c5fbfcf41 100644 Binary files a/backend/tests/__pycache__/test_runtime_config_api.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_runtime_config_api.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_search.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_search.cpython-310-pytest-9.0.3.pyc index 22be8a87e..4be885fb0 100644 Binary files a/backend/tests/__pycache__/test_search.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_search.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_tasks_api.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_tasks_api.cpython-310-pytest-9.0.3.pyc index 736996049..3bdf67bfd 100644 Binary files a/backend/tests/__pycache__/test_tasks_api.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_tasks_api.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_tenant_config_api.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_tenant_config_api.cpython-310-pytest-9.0.3.pyc index 144e60307..5be674b3b 100644 Binary files a/backend/tests/__pycache__/test_tenant_config_api.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_tenant_config_api.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_tenant_config_model.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_tenant_config_model.cpython-310-pytest-9.0.3.pyc index 72ba2c647..599e7f4f2 100644 Binary files a/backend/tests/__pycache__/test_tenant_config_model.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_tenant_config_model.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_text_safety.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_text_safety.cpython-310-pytest-9.0.3.pyc index 9b9ba0a4d..533a32bd1 100644 Binary files a/backend/tests/__pycache__/test_text_safety.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_text_safety.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/__pycache__/test_threading_service.cpython-310-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_threading_service.cpython-310-pytest-9.0.3.pyc index 40e76befe..b842d3f2b 100644 Binary files a/backend/tests/__pycache__/test_threading_service.cpython-310-pytest-9.0.3.pyc and b/backend/tests/__pycache__/test_threading_service.cpython-310-pytest-9.0.3.pyc differ diff --git a/backend/tests/test_apm_observability.py b/backend/tests/test_apm_observability.py index 3b84f66cf..23b1628bb 100644 --- a/backend/tests/test_apm_observability.py +++ b/backend/tests/test_apm_observability.py @@ -1,16 +1,17 @@ from pathlib import Path +ROOT_DIR = Path(__file__).parent.parent.parent def test_observability_compose_file_exists(): - assert Path("../docker-compose.observability.yml").exists() + assert (ROOT_DIR / "docker-compose.observability.yml").exists() def test_observability_provisioning_exists(): - assert Path( - "../observability/grafana/provisioning/datasources/datasources.yaml" + assert ( + ROOT_DIR / "observability/grafana/provisioning/datasources/datasources.yaml" ).exists() - assert Path("../observability/prometheus.yml").exists() - assert Path("../observability/tempo.yaml").exists() + assert (ROOT_DIR / "observability/prometheus.yml").exists() + assert (ROOT_DIR / "observability/tempo.yaml").exists() def test_backend_exposes_metrics_endpoint(): diff --git a/backend/tests/test_caldav.py b/backend/tests/test_caldav.py new file mode 100644 index 000000000..f67e999e1 --- /dev/null +++ b/backend/tests/test_caldav.py @@ -0,0 +1,28 @@ +import pytest +from services.caldav_service import caldav_service + +def test_determine_writeback_target(): + connected_accounts = [ + {"account_id": "account1", "domain": "company.com"}, + {"account_id": "account2", "domain": "personal.com"} + ] + + # Should match company.com + task_context_1 = {"source_email": "boss@company.com"} + target_1 = caldav_service.determine_writeback_target(task_context_1, connected_accounts) + assert target_1 == "account1" + + # Should fallback to primary + task_context_2 = {"source_email": "friend@other.com"} + target_2 = caldav_service.determine_writeback_target(task_context_2, connected_accounts) + assert target_2 == "account1" + + # Should not match substring domain collisions + task_context_3 = {"source_email": "attacker@evilcompany.com"} + target_3 = caldav_service.determine_writeback_target(task_context_3, connected_accounts) + assert target_3 == "account1" # fallback, not domain match + +def test_determine_writeback_target_no_accounts(): + task_context = {"source_email": "boss@company.com"} + target = caldav_service.determine_writeback_target(task_context, []) + assert target == "default_system_caldav" diff --git a/backend/tests/test_ontology.py b/backend/tests/test_ontology.py new file mode 100644 index 000000000..8dbbcd003 --- /dev/null +++ b/backend/tests/test_ontology.py @@ -0,0 +1,15 @@ +import pytest +from services.ontology_service import ontology_service + +def test_analyze_sender_relationship(): + result1 = ontology_service.analyze_sender_relationship("seongho@company.com", "newsletter@marketing.com", "Please unsubscribe here") + assert result1["type"] == "Newsletter" + assert result1["confidence"] == 0.9 + + result2 = ontology_service.analyze_sender_relationship("seongho@company.com", "boss@company.com", "Hello") + assert result2["type"] == "Colleague" + assert result2["confidence"] == 0.85 + + result3 = ontology_service.analyze_sender_relationship("seongho@company.com", "Boss@Company.com", "Hello") + assert result3["type"] == "Colleague" + assert result3["confidence"] == 0.85 diff --git a/backend/tests/test_webdav_api.py b/backend/tests/test_webdav_api.py new file mode 100644 index 000000000..264a1e9c8 --- /dev/null +++ b/backend/tests/test_webdav_api.py @@ -0,0 +1,47 @@ +import pytest +from fastapi.testclient import TestClient +from main import app +from db.session import get_db +from services.webdav_service import webdav_service + +pytestmark = pytest.mark.usefixtures("dev_auth_dependency_overrides") + +@pytest.fixture(autouse=True) +def stub_webdav_service(monkeypatch): + monkeypatch.setattr( + webdav_service, + "get_connected_accounts", + lambda user_id: [{"account_id": 1, "server_url": "https://webdav.naruon.net", "username": "demo_user"}] if user_id == "alice" else [], + ) + monkeypatch.setattr( + webdav_service, + "get_project_folders", + lambda user_id: [ + {"folder_id": 1, "project_name": "Naruon Roadmap 2026", "webdav_path": "/Projects/Naruon_Roadmap_2026"}, + {"folder_id": 2, "project_name": "Marketing Assets", "webdav_path": "/Projects/Marketing_Assets"} + ] if user_id == "alice" else [], + ) + +@pytest.fixture +def auth_client(): + with TestClient( + app, + headers={"X-User-Id": "alice", "X-Organization-Id": "org-acme"}, + ) as client: + yield client + +def test_get_webdav_accounts(auth_client): + response = auth_client.get("/api/webdav/accounts") + assert response.status_code == 200, response.text + body = response.json() + assert len(body) > 0 + assert body[0]["server_url"] == "https://webdav.naruon.net" + assert body[0]["username"] == "demo_user" + +def test_get_project_folders(auth_client): + response = auth_client.get("/api/webdav/folders") + assert response.status_code == 200, response.text + body = response.json() + assert len(body) == 2 + assert body[0]["project_name"] == "Naruon Roadmap 2026" + assert body[1]["project_name"] == "Marketing Assets" diff --git a/docker-compose.infra.yml b/docker-compose.infra.yml index 997c28543..f84aca001 100644 --- a/docker-compose.infra.yml +++ b/docker-compose.infra.yml @@ -50,6 +50,18 @@ services: networks: - naruon-network + keycloak: + image: quay.io/keycloak/keycloak:24.0.0 + command: start-dev + environment: + KC_DB: dev-file + KEYCLOAK_ADMIN: "${KEYCLOAK_ADMIN:?KEYCLOAK_ADMIN is not set}" + KEYCLOAK_ADMIN_PASSWORD: "${KEYCLOAK_ADMIN_PASSWORD:?KEYCLOAK_ADMIN_PASSWORD is not set}" + ports: + - "8081:8080" + networks: + - naruon-network + networks: naruon-network: driver: bridge diff --git a/docs/plans/2026-05-24-architecture-implementation.md b/docs/plans/2026-05-24-architecture-implementation.md new file mode 100644 index 000000000..cfa6bec73 --- /dev/null +++ b/docs/plans/2026-05-24-architecture-implementation.md @@ -0,0 +1,40 @@ +# Architecture & API Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Lay the groundwork for the Self-hosted IMAP/SMTP Relay Proxy, Casdoor/Keycloak Auth Gateway (Traefik), and CalDAV/WebDAV integration. + +**Architecture:** Naruon is a relay/proxy web client. Establish outbound-only WebSocket/mTLS connectors, a universal RBAC middleware, and sync APIs. + +**Tech Stack:** Node.js / Python (depending on backend choice), Traefik, Keycloak/Casdoor concepts, PostgreSQL. + +--- + +### Task 1: Self-hosted Connector & Gateway Schema + +**Files:** +- Create: `backend/schema/connector.py` +- Create: `backend/api/auth.py` + +- [ ] **Step 1: Define Self-hosted Connector registration schema** +- [ ] **Step 2: Define Traefik/Casdoor OIDC verification middleware** +- [ ] **Step 3: Implement unit tests for auth middleware** + +### Task 2: Universal RBAC / ABAC Structure + +**Files:** +- Modify: `backend/models/user.py` +- Modify: `backend/models/role.py` + +- [ ] **Step 1: Define B2B2C, Admin, and B2C roles** +- [ ] **Step 2: Implement authorization dependency decorators** +- [ ] **Step 3: Add tests for hierarchical access control** + +### Task 3: CalDAV/WebDAV Write-back Hooks + +**Files:** +- Create: `backend/api/dav_sync.py` + +- [ ] **Step 1: Add `/api/calendar/writeback-intent` with provenance** +- [ ] **Step 2: Support ETag / If-Match collision checks** +- [ ] **Step 3: Write DAG/Ontology placeholder processing** diff --git a/docs/plans/2026-05-24-branding-implementation.md b/docs/plans/2026-05-24-branding-implementation.md new file mode 100644 index 000000000..8a87d6982 --- /dev/null +++ b/docs/plans/2026-05-24-branding-implementation.md @@ -0,0 +1,47 @@ +# Branding & GNB Details Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Implement the missing high-fidelity UI requirements from `frontend/branding` mockups, make the startup screen configurable, and ensure responsive/hamburger menu behaviors are fully functional and tested across resolutions. + +**Architecture:** Use existing React components but elevate them with actual detailed functionality as per the mockups. Enhance `DashboardLayout` and `WorkspaceHome` to respect user preferences for the initial screen. + +**Tech Stack:** Next.js (App Router), Tailwind CSS, React, Playwright for E2E resolution testing. + +--- + +### Task 1: Configurable Startup Screen + +**Files:** +- Modify: `frontend/src/app/page.tsx` +- Modify: `frontend/src/components/DashboardLayout.tsx` +- Modify: `frontend/src/store/userPreferences.ts` (or similar state management) +- Test: `frontend/src/app/page.test.tsx` + +- [ ] **Step 1: Add user preference for startup view** +- [ ] **Step 2: Update `Home` page logic to read preference** +- [ ] **Step 3: Render either `WorkspaceHome`, `EmailList`, or `Calendar` accordingly** +- [ ] **Step 4: Update E2E and Unit Tests** + +### Task 2: Mobile Responsive & Hamburger Menu Verification + +**Files:** +- Modify: `frontend/src/components/Sidebar.tsx` or `Navigation.tsx` +- Test: `tests/e2e/mobile-hamburger.spec.ts` + +- [ ] **Step 1: Write Playwright E2E test for mobile viewport** +- [ ] **Step 2: Ensure Hamburger menu toggles correctly and prevents body scroll** +- [ ] **Step 3: Ensure bottom action bars and panels are safe-area padded** +- [ ] **Step 4: Fix any UI issues detected in resolution tests** + +### Task 3: GNB Detail Views (Projects, Tasks, Data) + +**Files:** +- Modify: `frontend/src/app/projects/page.tsx` +- Modify: `frontend/src/app/tasks/page.tsx` +- Modify: `frontend/src/app/data/page.tsx` + +- [ ] **Step 1: Implement Project List, Milestones, and Decision Log UI** +- [ ] **Step 2: Implement Tasks Kanban, Delegation, and Detailed View UI** +- [ ] **Step 3: Implement Data Repository, Pipeline Status, and Embedding Stats UI** +- [ ] **Step 4: Write tests for these new detail views** diff --git a/frontend/src/components/DataLayout.tsx b/frontend/src/components/DataLayout.tsx index ded1895d4..e03353da1 100644 --- a/frontend/src/components/DataLayout.tsx +++ b/frontend/src/components/DataLayout.tsx @@ -1,10 +1,36 @@ "use client"; -import { useState } from 'react'; -import { Database, HardDrive, RefreshCw, FolderOpen, AlertCircle, FileText, CheckCircle2 } from 'lucide-react'; +import { useState, useEffect } from 'react'; +import { Database, HardDrive, RefreshCw, FolderOpen, AlertCircle, FileText, CheckCircle2, Server } from 'lucide-react'; +import { apiClient } from '@/lib/api-client'; export function DataLayout() { const [activeTab, setActiveTab] = useState<'문서 저장소' | '수집 파이프라인' | '임베딩' | '품질 점검'>('문서 저장소'); + + interface WebdavAccount { + account_id: number; + server_url: string; + username: string; + } + + interface ProjectFolder { + folder_id: number; + project_name: string; + webdav_path: string; + } + + const [webdavAccounts, setWebdavAccounts] = useState([]); + const [projectFolders, setProjectFolders] = useState([]); + + useEffect(() => { + apiClient.get('/api/webdav/accounts') + .then(data => Array.isArray(data) && setWebdavAccounts(data)) + .catch(console.error); + + apiClient.get('/api/webdav/folders') + .then(data => Array.isArray(data) && setProjectFolders(data)) + .catch(console.error); + }, []); return (
@@ -50,12 +76,17 @@ export function DataLayout() {

WebDAV 원본 (연동됨)

-

1.2 TB / 무제한

+

+ {webdavAccounts.length > 0 ? `${webdavAccounts.length}개 계정` : '연결 없음'} +

-
-
-
+ {webdavAccounts.map(acc => ( +
+ + {acc.server_url} ({acc.username}) +
+ ))}
@@ -69,6 +100,25 @@ export function DataLayout() {
+
+
+

AI 프로젝트 구조화된 첨부파일 (WebDAV)

+
+
+ {projectFolders.length > 0 ? projectFolders.map(folder => ( +
+
+ + {folder.project_name} +
+

{folder.webdav_path}

+
+ )) : ( +

AI가 구조화한 프로젝트 폴더가 없습니다.

+ )} +
+
+

최근 수집 로그 (Ingestion Logs)

diff --git a/frontend/src/components/EmailDetail.tsx b/frontend/src/components/EmailDetail.tsx index c96725954..d46b3f003 100644 --- a/frontend/src/components/EmailDetail.tsx +++ b/frontend/src/components/EmailDetail.tsx @@ -18,8 +18,10 @@ import { type ThreadEmailData, } from "@/lib/email-threading"; -type EmailData = ThreadEmailData; - +type EmailData = ThreadEmailData & { + requires_reply?: boolean; + schedule_conflict?: boolean; +}; interface LlmData { summary: string; todos: string[]; @@ -320,8 +322,16 @@ export function EmailDetail({ emailId, actionCommand = null }: { emailId: number 답장 주소: {email.reply_to || email.sender}
-
- {formatEmailDate(email.date)} +
+
+ {formatEmailDate(email.date)} +
+ {email.requires_reply && ( + 응답 대기 중 + )} + {email.schedule_conflict && ( + 일정 충돌 조율 + )}
diff --git a/frontend/src/components/EmailList.tsx b/frontend/src/components/EmailList.tsx index 142e7c238..542b608f4 100644 --- a/frontend/src/components/EmailList.tsx +++ b/frontend/src/components/EmailList.tsx @@ -20,6 +20,8 @@ interface EmailItem { reply_count?: number; has_draft?: boolean; is_self_sent?: boolean; + requires_reply?: boolean; + schedule_conflict?: boolean; } let inboxRequest: Promise | null = null; @@ -108,12 +110,12 @@ export function EmailList({ }} className="flex gap-2" > - +