diff --git a/README.md b/README.md index c919df5..8073964 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,14 @@ light/dark mode. Self-hostable via Node.js or Docker Compose. - **Pages** — Nested pages (`parent_id`) in workspaces or **books**, icon + cover, ordering, TipTap rich-text with **tables + checklists**, stored as JSON + HTML. - **Books** 📚 — Independent containers for pages, collapsible in the sidebar. -- **Tasks** ✅ — Kanban (todo/doing/done), priorities, deadlines, links to books/pages. +- **Tasks** ✅ — Kanban (todo/doing/doing→done), priorities, deadlines, **assignees**, + links to books/pages, **per-team boards**. +- **Teams** 👥 — Teams with managers/members, **own workspaces** (`TEAM_"name"` badge), + permission **groups** (e.g. Team Creators: teams.create/edit/invite, workspace.invite). +- **Invites + notifications** 🔔 — Pending invites with accept/decline, bell with unread + badge (polling + realtime push), notifications for invites and chat messages. +- **Team chat** 💬 — Realtime workspace channel via Socket.IO. +- **Uploads** 📤 — Avatar/background images uploadable (served from `/uploads`). - **Calendar** 🗓️ — Deadlines from tasks appear automatically. - **Time tracking** ⏱️ — Top-right timer, entries linkable to books, history list. - **Settings window** ⚙️ — Profile (name, email, avatar, password), appearance diff --git a/docs/API.md b/docs/API.md index e879885..821926c 100644 --- a/docs/API.md +++ b/docs/API.md @@ -84,7 +84,25 @@ Content fields: `content_json` (TipTap JSON), `content_html` (rendered HTML). ## Admin (requires `is_admin`) - `GET /admin/overview` → counts + recent users -- `GET /admin/users`, `PATCH /admin/users/:id {is_admin}`, `DELETE /admin/users/:id` +- `GET /admin/users`, `POST /admin/users {name,email,password,is_admin?}` (admins create + accounts; public registration is disabled once installed), `PATCH /admin/users/:id`, + `DELETE /admin/users/:id` + +## Invites & notifications + +- `POST /workspaces/:id/members {email,role}` → pending invite + notification + (workspace admin+ or `workspace.invite` permission) +- `GET /invites/mine`, `POST /invites/:id/accept`, `POST /invites/:id/decline` +- `GET /notifications/mine?unread=1` → `{ notifications, unread }` +- `PATCH /notifications/:id/read`, `POST /notifications/read-all` + +## Chat / teams / groups / upload + +- `GET /chat/:workspaceId`, `POST /chat/:workspaceId {body}`, `DELETE /chat/message/:id` +- `GET /teams/by-workspace/:id`, `POST /teams`, `PATCH/DELETE /teams/:id`, + team members CRUD, `POST /teams/:id/workspace {name}` (team workspace + badge) +- `GET /groups/by-workspace/:id` (seeds `Team Creators`), groups CRUD + members +- `POST /upload` (multipart image ≤5MB) → `{ url: /uploads/... }` ## Health diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 6eddb08..1565a97 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -4,21 +4,29 @@ import { useAuthStore } from './stores/auth'; import { useThemeStore } from './stores/theme'; import { useLocaleStore } from './stores/locale'; import { useUiStore } from './stores/ui'; +import { fileUrl } from './files'; import SettingsWindow from './components/SettingsWindow.vue'; import TimeTrackerButton from './components/TimeTrackerButton.vue'; +import Bell from './components/Bell.vue'; const auth = useAuthStore(); const theme = useThemeStore(); const locale = useLocaleStore(); const ui = useUiStore(); +const bgRaw = computed(() => auth.user?.bgImage || ''); +const bgIsImage = computed(() => /^(https?:\/\/|\/uploads|data:)/.test(bgRaw.value)); const bgStyle = computed(() => { - const bg = auth.user?.bgImage || ''; - if (!bg) return {}; - const style = { backgroundImage: bg.startsWith('http') ? `url("${bg}")` : bg }; - if (auth.user?.bgBlur !== false) style.filter = 'blur(14px) brightness(0.85)'; + if (!bgRaw.value) return {}; + const style = { + backgroundImage: bgIsImage.value ? `url("${fileUrl(bgRaw.value)}")` : bgRaw.value, + backgroundSize: 'cover', + backgroundPosition: 'center', + }; + if (auth.user?.bgBlur !== false) style.filter = 'blur(16px) brightness(0.9)'; return style; }); +const glassOn = computed(() => !!bgRaw.value && auth.user?.bgGlass !== false); onMounted(() => { locale.set(locale.locale); @@ -28,9 +36,10 @@ onMounted(() => {