Add mobile-first kanban board view for Chorus triage #3

Merged
pyr0ball merged 9 commits from feat/chorus-triage-views into main 2026-07-15 20:14:53 -07:00
4 changed files with 50 additions and 11 deletions
Showing only changes of commit e8efb6566f - Show all commits

View file

@ -25,7 +25,7 @@ class ThreadIngestCog(commands.Cog):
return return
parsed = parse_ping_embed(parent_message.embeds[0]) parsed = parse_ping_embed(parent_message.embeds[0])
await self.backend_client.create_item( item = await self.backend_client.create_item(
modality=parsed["modality"], modality=parsed["modality"],
raw_content=parsed["raw_content"], raw_content=parsed["raw_content"],
captured_at=parsed["captured_at"], captured_at=parsed["captured_at"],
@ -33,8 +33,9 @@ class ThreadIngestCog(commands.Cog):
sender_id=parsed["sender_id"], sender_id=parsed["sender_id"],
direction=parsed["direction"], direction=parsed["direction"],
) )
item_url = f"{self.config.web_base_url}/?item={item['id']}"
await thread.send( await thread.send(
"Logged in Chorus - open the app to fill in details and track this one." f"Logged in Chorus - {item_url} to fill in details and track this one."
) )

View file

@ -9,6 +9,7 @@ class Config:
inbox_channel_id: int inbox_channel_id: int
triage_role_id: int triage_role_id: int
backend_base_url: str backend_base_url: str
web_base_url: str
google_voice_imap_host: str google_voice_imap_host: str
google_voice_imap_user: str google_voice_imap_user: str
google_voice_imap_password: str google_voice_imap_password: str
@ -33,6 +34,7 @@ def load_config() -> Config:
inbox_channel_id=int(env("CHORUS_INBOX_CHANNEL_ID")), inbox_channel_id=int(env("CHORUS_INBOX_CHANNEL_ID")),
triage_role_id=int(env("CHORUS_TRIAGE_ROLE_ID")), triage_role_id=int(env("CHORUS_TRIAGE_ROLE_ID")),
backend_base_url=env("CHORUS_BACKEND_BASE_URL"), backend_base_url=env("CHORUS_BACKEND_BASE_URL"),
web_base_url=os.environ.get("CHORUS_WEB_BASE_URL", "https://chorus.circuitforge.tech"),
google_voice_imap_host=env("CHORUS_GVOICE_IMAP_HOST"), google_voice_imap_host=env("CHORUS_GVOICE_IMAP_HOST"),
google_voice_imap_user=env("CHORUS_GVOICE_IMAP_USER"), google_voice_imap_user=env("CHORUS_GVOICE_IMAP_USER"),
google_voice_imap_password=env("CHORUS_GVOICE_IMAP_PASSWORD"), google_voice_imap_password=env("CHORUS_GVOICE_IMAP_PASSWORD"),

View file

@ -7,12 +7,26 @@ from chorus_bot.ping import build_ping_embed
class _FakeConfig: class _FakeConfig:
inbox_channel_id = 111 inbox_channel_id = 111
web_base_url = "https://chorus.circuitforge.tech"
def _make_thread(embed):
parent_message = MagicMock()
parent_message.embeds = [embed]
thread = MagicMock(spec=discord.Thread)
thread.id = 555
thread.parent_id = 111
thread.parent.fetch_message = AsyncMock(return_value=parent_message)
thread.send = AsyncMock()
return thread
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_thread_create_passes_direction_to_backend_client(): async def test_thread_create_passes_direction_to_backend_client():
bot = MagicMock() bot = MagicMock()
backend_client = AsyncMock() backend_client = AsyncMock()
backend_client.create_item.return_value = {"id": 42}
cog = ThreadIngestCog(bot, _FakeConfig(), backend_client) cog = ThreadIngestCog(bot, _FakeConfig(), backend_client)
embed = build_ping_embed( embed = build_ping_embed(
@ -22,16 +36,32 @@ async def test_thread_create_passes_direction_to_backend_client():
tzinfo=__import__("datetime").timezone.utc), tzinfo=__import__("datetime").timezone.utc),
direction="outbound", direction="outbound",
) )
parent_message = MagicMock() thread = _make_thread(embed)
parent_message.embeds = [embed]
thread = MagicMock(spec=discord.Thread)
thread.id = 555
thread.parent_id = 111
thread.parent.fetch_message = AsyncMock(return_value=parent_message)
thread.send = AsyncMock()
await cog.on_thread_create(thread) await cog.on_thread_create(thread)
backend_client.create_item.assert_awaited_once() backend_client.create_item.assert_awaited_once()
assert backend_client.create_item.call_args.kwargs["direction"] == "outbound" assert backend_client.create_item.call_args.kwargs["direction"] == "outbound"
@pytest.mark.asyncio
async def test_thread_create_reply_includes_item_link():
bot = MagicMock()
backend_client = AsyncMock()
backend_client.create_item.return_value = {"id": 42}
cog = ThreadIngestCog(bot, _FakeConfig(), backend_client)
embed = build_ping_embed(
modality="lead_found", sender_id=None, snippet="found post",
raw_content="Someone posting about book donation",
captured_at=__import__("datetime").datetime(2026, 7, 13, 7, 0, 0,
tzinfo=__import__("datetime").timezone.utc),
direction="outbound",
)
thread = _make_thread(embed)
await cog.on_thread_create(thread)
thread.send.assert_awaited_once()
reply_text = thread.send.call_args.args[0]
assert "https://chorus.circuitforge.tech/?item=42" in reply_text

View file

@ -36,7 +36,13 @@ async function onSaved() {
await loadItems() await loadItems()
} }
onMounted(loadItems) onMounted(async () => {
await loadItems()
const itemId = new URLSearchParams(window.location.search).get('item')
if (itemId) {
await openItem(itemId)
}
})
</script> </script>
<template> <template>