diff --git a/ERRORS.md b/ERRORS.md deleted file mode 100644 index 7c4f4e7..0000000 --- a/ERRORS.md +++ /dev/null @@ -1,80 +0,0 @@ -# Monkey Test Report - -Date: 2025-12-29 - -## Summary - -Tested all pages and interactive elements across the application. - -## Issues Found and Fixed - -### ~~High Priority~~ ✅ FIXED - -#### ~~1. "untitled page" text displayed on all pages~~ - -- **Location**: Bottom of every page (svelte-announcer accessibility element) -- **Root cause**: No page titles set, causing SvelteKit's screen reader announcer to say "untitled page" -- **Fix**: Added `...` to all pages - -### ~~Medium Priority~~ ✅ FIXED - -#### ~~2. "Loading messages..." persists in empty channels~~ - -- **Location**: `apps/desktop/src/components/chat/MessageList.svelte` -- **Root cause**: Condition checked `messagesData.length > 0` instead of checking loading state -- **Fix**: Added proper `isLoading` check from useQuery hook - -#### ~~3. Add Member uses browser prompt() dialog~~ - -- **Location**: `apps/desktop/src/routes/orgs/[orgId]/settings/` -- **Root cause**: Used native `prompt()` and `confirm()` for user input -- **Fix**: Created proper DaisyUI modal in MemberList.svelte - -### ~~Low Priority~~ ✅ FIXED - -#### ~~4. DM search shows no results indication~~ - -- **Location**: `apps/desktop/src/components/dms/UserSearch.svelte` -- **Root cause**: No else condition for empty results -- **Fix**: Added "No users found" message when search completes with no results - -## Features Tested (All Working) - -- [x] Organization selection page -- [x] Organization switching -- [x] Channel navigation (general, test-channel) -- [x] Channel creation modal -- [x] Message input and sending -- [x] Message reactions (add/remove) -- [x] Message menu (Reply, Add Reaction, Show Reactions, Pin, Edit, Delete) -- [x] Message pinning -- [x] Search messages -- [x] Poll creation form -- [x] Emoji picker (all tabs, search, favorites) -- [x] User profile/personalization settings (name change, profile picture) -- [x] Organization settings (edit name/description) -- [x] Organization member list -- [x] New organization creation page -- [x] Signin redirect (when already logged in) - -## Files Modified - -1. `apps/desktop/src/routes/+page.svelte` - Added page title -2. `apps/desktop/src/routes/signin/+page.svelte` - Added page title -3. `apps/desktop/src/routes/signout/+page.svelte` - Added page title -4. `apps/desktop/src/routes/orgs/new/+page.svelte` - Added page title -5. `apps/desktop/src/routes/orgs/[orgId]/+page.svelte` - Added page title -6. `apps/desktop/src/routes/orgs/[orgId]/settings/+page.svelte` - Added page title, updated onAddMember callback -7. `apps/desktop/src/routes/orgs/[orgId]/personalization/+page.svelte` - Added page title -8. `apps/desktop/src/routes/orgs/[orgId]/chat/[channelId]/+page.svelte` - Added page title -9. `apps/desktop/src/components/chat/MessageList.svelte` - Fixed loading state logic -10. `apps/desktop/src/routes/orgs/[orgId]/settings/MemberList.svelte` - Added modal for Add Member -11. `apps/desktop/src/routes/orgs/[orgId]/settings/member-utils.ts` - Refactored to throw errors instead of using alerts -12. `apps/desktop/src/routes/orgs/[orgId]/settings/settings-controller.svelte.ts` - Updated to accept email parameter -13. `apps/desktop/src/components/dms/UserSearch.svelte` - Added "No users found" message - -## Test Environment - -- Browser: Chrome (via DevTools MCP) -- URL: http://localhost:5173 -- User: Dev User (dev@example.com) - Admin role diff --git a/README.md b/README.md index de2e869..b917ae0 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,9 @@ cp .env.sample .env ### 開発用サーバー ```sh -devenv up -d # run in background, logs at .devenv/processes.log -devenv processes down # kill the background service +bun up # バックグラウンドで起動 +bun down # 停止 +bun tail # ログを表示 ``` ## 注意点 diff --git a/TODOS.md b/TODOS.md deleted file mode 100644 index 5b0c610..0000000 --- a/TODOS.md +++ /dev/null @@ -1,46 +0,0 @@ -# TODOS - -Generated: 2025-12-24 - -## Batch 1: Critical Security Fixes - -- [ ] Fix SQL injection in message search (`apps/server/src/domains/messages/service.ts` - escape `ilike` pattern) -- [ ] Add file path sanitization (`apps/server/src/domains/files/service.ts` - prevent `../` traversal) -- [ ] Add WebSocket auth re-validation on subscribe (`apps/server/src/ws/index.ts` - check permissions on subscribe) -- [ ] Add rate limiting middleware (`apps/server/src/middleware/` - create rate-limit.ts) - -## Batch 2: High-Impact Performance Fixes - -- [ ] Fix N+1 query in message list (`apps/server/src/domains/messages/service.ts` - include reactions/attachments in query) -- [ ] Remove duplicate useQuery per message (`apps/desktop/src/components/MessageItem.svelte` - pass reactions as props) -- [ ] Deduplicate permission checks (`apps/server/src/domains/permissions/service.ts` - cache within request context) -- [ ] Lazy load CodeMirror and emoji-picker (`apps/desktop/src/components/` - use dynamic imports) - -## Batch 3: Code Quality & Dead Code Removal - -- [ ] Delete unused example/ directory (`.storybook`, `apps/desktop/src/example/`) -- [ ] Remove console.log/error statements (9 files - use proper logger or delete) -- [ ] Split large components: `MessageItem.svelte` (143 lines → ~50 lines each) -- [ ] Split large components: `ChannelList.svelte` (141 lines → ~50 lines each) - -## Batch 4: Accessibility Fixes - -- [ ] Add aria-labels to icon buttons (`apps/desktop/src/components/` - all IconButton components) -- [ ] Replace alert() with toast notifications (`apps/desktop/src/` - create toast utility) -- [ ] Add form labels for WCAG compliance (`apps/desktop/src/routes/` - all form inputs) -- [ ] Standardize placeholder text language (`apps/desktop/src/` - choose Japanese or English consistently) - -## Deferred (Needs User Confirmation) - -- [ ] DISABLE_AUTH flag - clarify production usage policy (`apps/server/src/middleware/auth.ts`) -- [ ] Add CSRF protection - confirm if needed for API-only backend (`apps/server/src/middleware/`) -- [ ] Add security headers - confirm headers policy (`apps/server/src/index.ts`) -- [ ] shadow-2xl usage - confirm if Clarity design principles apply (`apps/desktop/src/components/`) -- [ ] Empty state CTAs - requires design decisions (multiple files) - -## Rejected (Low Value / Out of Scope) - -- Duplicate logic in unread.ts - minimal impact, refactor during feature work -- Test coverage improvements - separate test-focused sprint needed -- Flaky waitForTimeout(500) - address when writing new E2E tests -- Page Object Model - requires significant test refactoring diff --git a/apps/desktop/src/components/channels/ChannelContextMenu.svelte b/apps/desktop/src/components/channels/ChannelContextMenu.svelte new file mode 100644 index 0000000..9c90608 --- /dev/null +++ b/apps/desktop/src/components/channels/ChannelContextMenu.svelte @@ -0,0 +1,142 @@ + + + e.key === "Escape" && onClose()} +/> + + diff --git a/apps/desktop/src/components/channels/ChannelGroup.svelte b/apps/desktop/src/components/channels/ChannelGroup.svelte new file mode 100644 index 0000000..c72e9fb --- /dev/null +++ b/apps/desktop/src/components/channels/ChannelGroup.svelte @@ -0,0 +1,136 @@ + + +{#if contextMenu} + onCreateChannel?.(group.id)} + onCreateGroup={() => onCreateGroup?.(group.id)} + onRename={() => onRename?.(group.id, group.name)} + onDelete={() => onDelete?.(group.id)} + onClose={() => (contextMenu = null)} + /> +{/if} + +
+ + + {#if !collapsed} + + {/if} +
diff --git a/apps/desktop/src/components/channels/ChannelGroupContextMenu.svelte b/apps/desktop/src/components/channels/ChannelGroupContextMenu.svelte new file mode 100644 index 0000000..44d9f18 --- /dev/null +++ b/apps/desktop/src/components/channels/ChannelGroupContextMenu.svelte @@ -0,0 +1,80 @@ + + + e.key === "Escape" && onClose()} +/> + + diff --git a/apps/desktop/src/components/channels/ChannelItem.svelte b/apps/desktop/src/components/channels/ChannelItem.svelte new file mode 100644 index 0000000..092c3c4 --- /dev/null +++ b/apps/desktop/src/components/channels/ChannelItem.svelte @@ -0,0 +1,73 @@ + + +{#if contextMenu} + onEdit?.(channel)} + onDelete={() => onDelete?.(channel.id)} + onMoveToGroup={(groupId) => onMoveToGroup?.(channel.id, groupId)} + onClose={() => (contextMenu = null)} + /> +{/if} + + + + 0 && "font-medium"]}> + {channel.name} + + {#if unreadCount > 0} + + {unreadCount > 99 ? "99+" : unreadCount} + + {/if} + diff --git a/apps/desktop/src/components/channels/ChannelList.controller.svelte.ts b/apps/desktop/src/components/channels/ChannelList.controller.svelte.ts new file mode 100644 index 0000000..8f570f3 --- /dev/null +++ b/apps/desktop/src/components/channels/ChannelList.controller.svelte.ts @@ -0,0 +1,163 @@ +import type { Channel, ChannelGroup } from "@packages/api-client"; +import { onMount } from "svelte"; +import { getApiClient, unwrapResponse, useQuery } from "@/lib/api.svelte"; +import { confirm } from "@/lib/confirm/confirm.svelte.ts"; +import { showToast } from "@/lib/toast/toast.svelte.ts"; +import { UnreadManager } from "@/lib/unread.svelte"; +import { useWebSocket } from "@/lib/websocket"; +import { + type GroupedChannels, + organizeChannelsIntoGroups, + useChannelGroupState, +} from "./channelGroups.svelte.ts"; + +export class ChannelListController { + #api = getApiClient(); + #getOrgId: () => string; + + channels; + channelGroups; + unreadManager; + groupState; + + #organized: GroupedChannels = $derived.by(() => + organizeChannelsIntoGroups( + this.channels.data ?? [], + this.channelGroups.data ?? [], + ), + ); + + get organized() { + return this.#organized; + } + + get rootGroups() { + return this.#organized.groups.filter((g) => g.parentGroupId === null); + } + + get ungroupedChannels() { + return this.#organized.channelsByGroup.get(null) ?? []; + } + + constructor(organizationId: () => string) { + this.#getOrgId = organizationId; + + this.channels = useQuery(async () => { + const response = await this.#api.channels.get({ + query: { organizationId: this.#getOrgId() }, + }); + return unwrapResponse(response); + }); + + this.channelGroups = useQuery(async () => { + const response = await this.#api["channel-groups"].get({ + query: { organizationId: this.#getOrgId() }, + }); + return unwrapResponse(response); + }); + + this.unreadManager = new UnreadManager(this.#api, organizationId); + this.groupState = useChannelGroupState(organizationId); + + useWebSocket("message:created", () => + this.unreadManager.fetchUnreadCounts(), + ); + onMount(() => this.unreadManager.fetchUnreadCounts()); + } + + async createGroup(name: string, parentGroupId: string | null) { + try { + await this.#api["channel-groups"].post({ + name, + organizationId: this.#getOrgId(), + parentGroupId: parentGroupId ?? undefined, + }); + this.channelGroups.refetch(); + showToast("Group created", "success"); + } catch (error) { + showToast("Failed to create group", "error"); + throw error; + } + } + + async renameGroup(groupId: string, newName: string) { + try { + await this.#api["channel-groups"]({ id: groupId }).patch({ + name: newName, + }); + this.channelGroups.refetch(); + showToast("Group renamed", "success"); + } catch (error) { + showToast("Failed to rename group", "error"); + throw error; + } + } + + async deleteGroup(groupId: string) { + const confirmed = await confirm({ + title: "Delete Group", + message: + "Are you sure you want to delete this group? Channels in this group will be ungrouped.", + confirmText: "Delete", + variant: "danger", + }); + if (!confirmed) return; + + try { + await this.#api["channel-groups"]({ id: groupId }).delete(); + this.channelGroups.refetch(); + showToast("Group deleted", "success"); + } catch (error) { + showToast("Failed to delete group", "error"); + throw error; + } + } + + async editChannel(channelId: string, name: string, description: string) { + try { + await this.#api.channels({ id: channelId }).patch({ + name, + description: description || null, + }); + this.channels.refetch(); + showToast("Channel updated", "success"); + } catch (error) { + showToast("Failed to update channel", "error"); + throw error; + } + } + + async moveChannelToGroup(channelId: string, groupId: string | null) { + try { + await this.#api.channels({ id: channelId }).group.patch({ groupId }); + this.channels.refetch(); + } catch (error) { + showToast("Failed to move channel", "error"); + throw error; + } + } + + async deleteChannel(channelId: string) { + const confirmed = await confirm({ + title: "Delete Channel", + message: + "Are you sure you want to delete this channel? All messages will be lost.", + confirmText: "Delete", + variant: "danger", + }); + if (!confirmed) return; + + try { + await this.#api.channels({ id: channelId }).delete(); + this.channels.refetch(); + showToast("Channel deleted", "success"); + } catch (error) { + showToast("Failed to delete channel", "error"); + throw error; + } + } + + refetchChannels() { + this.channels.refetch(); + } +} diff --git a/apps/desktop/src/components/channels/ChannelList.svelte b/apps/desktop/src/components/channels/ChannelList.svelte index 39c017e..ed6e1d9 100644 --- a/apps/desktop/src/components/channels/ChannelList.svelte +++ b/apps/desktop/src/components/channels/ChannelList.svelte @@ -1,18 +1,17 @@
-
Channels - +
+ + +
- -
-
- - Direct Messages - - -
+ modals.closeCreateChannel()} + onCreated={() => controller.refetchChannels()} + /> - {#if showUserSearch} -
- -
- {/if} + modals.closeCreateGroup()} + onCreate={(name, parentId) => controller.createGroup(name, parentId)} + /> -
- -
-
+ modals.closeRenameGroup()} + onRename={(id, name) => controller.renameGroup(id, name)} + /> + + modals.closeEditChannel()} + onSave={(id, name, desc) => controller.editChannel(id, name, desc)} + /> + + -