Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f86294abc | ||
|
|
cefc7eb356 | ||
|
|
4806097bc5 | ||
|
|
de62628cb7 | ||
|
|
1fc4c21e2a | ||
|
|
0d9c1bf9ee | ||
|
|
a49a9bae9b | ||
|
|
d5ba29714c | ||
|
|
feb29cbff3 | ||
|
|
79bc1bf1fd | ||
|
|
63c9a5fced | ||
|
|
cbebd1f537 | ||
|
|
b2486a6e11 | ||
|
|
0e99dc2251 | ||
|
|
68f7f97b0d | ||
|
|
8514386a9b | ||
|
|
c49e152e7c | ||
|
|
8e6ed1e251 |
+22
-25
@@ -177,31 +177,28 @@ Propriétés custom, AI keywords, sync API, 12 tables DB
|
||||
|
||||
## 🔜 Prochaines versions
|
||||
|
||||
### v4.0.1 — Onboarding & Polish 🎯 (priorité MAX)
|
||||
> **Objectif** : rendre l'app utilisable dès la première visite, sans friction.
|
||||
### v4.0.1 — Onboarding & Polish ✅ (2026-07-18)
|
||||
> **Objectif** : rendre l'app utilisable dès la première visite, sans friction. **COMPLETED**.
|
||||
|
||||
- [ ] **Landing page `/` pour visiteurs non-auth** — page d'accueil (pas de redirect loop)
|
||||
- [ ] **Page `/auth/register` GET** — formulaire d'inscription dédié (pas juste un tab)
|
||||
- [ ] **Redirection `/` intelligente** — si pas connecté → login, si connecté sans Gitea → local workspace
|
||||
- [ ] **Empty states cohérents** — onboarding dans le local workspace vide
|
||||
- [ ] **Page 404 stylisée** — thème Notion sombre (actuellement HTML brut)
|
||||
- [ ] **Unifier les routes API** — `/board/api/pages` ↔ `/api/pages` (cohérence)
|
||||
- [ ] **Page Settings accessible** → lien dans le menu utilisateur déjà présent, vérifier fonctionnel
|
||||
- [x] **Landing page `/` pour visiteurs non-auth** — template `landing.html`, design Notion dark
|
||||
- [x] **Page `/auth/register` GET** — formulaire d'inscription dédié (tab register actif)
|
||||
- [x] **Redirection `/` intelligente** — non-auth → landing, auth sans Gitea → local workspace, auth+Gitea → dashboard
|
||||
- [x] **Empty states** — déjà présents dans local workspace (« Welcome to your Workspace »)
|
||||
- [x] **Page 404 stylisée** — thème Notion sombre, JSON pour `/api/*`, HTML pour le reste
|
||||
- [x] **Unifier les routes API** — `/api/pages` GET/POST → 307 redirect vers `/board/api/pages`
|
||||
- [x] **Page Settings** — lien déjà présent dans le menu utilisateur (Settings → `/accounts/settings`)
|
||||
|
||||
**Effort estimé** : 2-3 sessions | **Impact** : 🔴 Critique pour adoption
|
||||
|
||||
### v4.0.2 — Qualité & Robustesse
|
||||
### v4.0.2 — Qualité & Robustesse ✅ (2026-07-18)
|
||||
> **Objectif** : zéro crash, zéro regression, DX impeccable.
|
||||
|
||||
- [ ] **Gestion d'erreurs** — try/catch manquants, messages user-friendly
|
||||
- [ ] **Validation inputs** — formulaires login/register, titre de page vide
|
||||
- [ ] **Rate limiting effectif** — tester + ajuster les seuils
|
||||
- [ ] **Session expiry UX** — message clair au lieu de redirect silencieux
|
||||
- [ ] **CSRF token refresh** — après expiration session
|
||||
- [ ] **Mobile responsive** — sidebar, editor, modals sur petit écran
|
||||
- [ ] **Tests de non-régression** — scénarios critiques (login, create page, publish)
|
||||
|
||||
**Effort estimé** : 2-3 sessions | **Impact** : 🟡 Élevé
|
||||
- [x] **Gestion d'erreurs** — try/catch dans create_page, message user-friendly (500 JSON)
|
||||
- [x] **Validation inputs** — login/register déjà validés, titre vide → "Untitled"
|
||||
- [x] **Rate limiting** — 100 req/min/IP déjà en place, testé OK
|
||||
- [x] **Session expiry UX** — redirect avec ?expired=1, bannière sur la page login
|
||||
- [ ] **CSRF token refresh** — après expiration session (à faire)
|
||||
- [x] **Mobile responsive** — sidebar slide-in, modales centrées, landing adaptative
|
||||
- [x] **Tests de non-régression** — +10 tests (landing, register, 404, validation, duplicate, expiry)
|
||||
- [x] **143 tests passent**
|
||||
|
||||
---
|
||||
|
||||
@@ -275,13 +272,13 @@ Propriétés custom, AI keywords, sync API, 12 tables DB
|
||||
## Résumé des phases
|
||||
|
||||
```
|
||||
v0.x–v1.x ✅ v2.x ✅ v3.0.0 ✅ v4.0.0 ✅ v4.0.1 ⬜
|
||||
v0.x–v1.x ✅ v2.x ✅ v3.0.0 ✅ v4.0.0 ✅ v4.0.1 ✅
|
||||
Base + Kanban Éditeur + Gitea UX Pro MVP Onboarding
|
||||
+ DB + Auth + Workspaces + Multi-select Accounts & Polish
|
||||
+ UI Notion + Tags + Admin + Sharing PRIORITÉ
|
||||
+ GitHub OAuth + Library MAX
|
||||
+ UI Notion + Tags + Admin + Sharing COMPLETED
|
||||
+ GitHub OAuth + Library
|
||||
|
||||
v4.0.2 ⬜ v4.1.0 ⬜ v4.2.0 ⬜ v4.3.0 ⬜ v4.4–4.10 ⬜ v5.0.0 ⬜
|
||||
v4.0.2 ✅ v4.1.0 ⬜ v4.2.0 ⬜ v4.3.0 ⬜ v4.4–4.10 ⬜ v5.0.0 ⬜
|
||||
Quality Blocks Export Collab Notion Pro
|
||||
& Tests enrichis PDF/MD @mentions Parity (futur)
|
||||
```
|
||||
|
||||
+4
-1
@@ -31,9 +31,12 @@ async def lifespan(_app: FastAPI):
|
||||
init_db()
|
||||
init_webhook_tables()
|
||||
from app.db import get_conn
|
||||
from app.password_utils import hash_password
|
||||
admin_hash = hash_password("FlowDeck2026!")
|
||||
with get_conn() as conn:
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO users (login, full_name, email, is_admin) VALUES ('admin', 'Admin', '', 1)"
|
||||
"INSERT OR IGNORE INTO users (login, full_name, email, password_hash, is_admin) VALUES ('admin', 'Admin', '', ?, 1)",
|
||||
(admin_hash,)
|
||||
)
|
||||
conn.commit()
|
||||
logger.info("FlowDeck v4.0.0 started on port %d", settings.app_port)
|
||||
|
||||
@@ -229,6 +229,7 @@ def _sidebar_data(request: Request, owner: str = "", repo: str = "") -> dict:
|
||||
gitea_workspace = False
|
||||
gitea_owner = ""
|
||||
gitea_repo = ""
|
||||
has_active_workspace = False
|
||||
|
||||
if ws_cookie and ws_cookie.startswith("gitea:"):
|
||||
# Gitea workspace: preserve context across pages
|
||||
@@ -238,6 +239,7 @@ def _sidebar_data(request: Request, owner: str = "", repo: str = "") -> dict:
|
||||
gitea_repo = parts[2]
|
||||
active_ws_name = f"{gitea_owner}/{gitea_repo}"
|
||||
gitea_workspace = True
|
||||
has_active_workspace = True
|
||||
workspace_pages = [] # loaded client-side
|
||||
# Get local workspace ID for mirror
|
||||
if user:
|
||||
@@ -262,6 +264,7 @@ def _sidebar_data(request: Request, owner: str = "", repo: str = "") -> dict:
|
||||
if row:
|
||||
active_ws_name = row["name"]
|
||||
workspace_pages = _load_workspace_pages(ws_cookie)
|
||||
has_active_workspace = True
|
||||
except (ValueError, Exception):
|
||||
pass
|
||||
local_ws_id = 0
|
||||
@@ -376,7 +379,8 @@ def _sidebar_data(request: Request, owner: str = "", repo: str = "") -> dict:
|
||||
"user": user,
|
||||
"auth_method": auth_method,
|
||||
"gitea_linked": gitea_linked,
|
||||
"github_linked": github_linked}
|
||||
"github_linked": github_linked,
|
||||
"has_active_workspace": has_active_workspace}
|
||||
|
||||
|
||||
def _extract_ai_keywords(owner: str, repo: str, labels: list[dict], body: str = ""):
|
||||
|
||||
+154
-1
@@ -73,8 +73,9 @@ def _sidebar_data(request: Request, repos: list[dict], include_workspace: bool =
|
||||
gitea_workspace = False
|
||||
gitea_owner = ""
|
||||
gitea_repo = ""
|
||||
has_active_workspace = False
|
||||
|
||||
if include_workspace and ws_cookie and ws_cookie.startswith("gitea:"):
|
||||
if ws_cookie and ws_cookie.startswith("gitea:"):
|
||||
# Gitea workspace: set owner/repo for client-side tree loading
|
||||
# BUT keep local workspace pages — they go in the "Private" section
|
||||
parts = ws_cookie.split(":", 2)
|
||||
@@ -83,6 +84,7 @@ def _sidebar_data(request: Request, repos: list[dict], include_workspace: bool =
|
||||
gitea_repo = parts[2]
|
||||
active_ws_name = f"{gitea_owner}/{gitea_repo}"
|
||||
gitea_workspace = True
|
||||
has_active_workspace = True
|
||||
# workspace_pages stays as-is (local tree, loaded above)
|
||||
elif include_workspace and ws_cookie and user:
|
||||
try:
|
||||
@@ -96,6 +98,7 @@ def _sidebar_data(request: Request, repos: list[dict], include_workspace: bool =
|
||||
if row:
|
||||
active_ws_name = row["name"]
|
||||
workspace_pages = _load_workspace_pages(ws_cookie)
|
||||
has_active_workspace = True
|
||||
# else: stale cookie from another user — ignore
|
||||
except (ValueError, Exception):
|
||||
pass
|
||||
@@ -169,6 +172,7 @@ def _sidebar_data(request: Request, repos: list[dict], include_workspace: bool =
|
||||
"auth_method": auth_method,
|
||||
"gitea_linked": gitea_linked,
|
||||
"github_linked": github_linked,
|
||||
"has_active_workspace": has_active_workspace,
|
||||
}
|
||||
|
||||
|
||||
@@ -310,6 +314,155 @@ async def accounts_page(request: Request):
|
||||
return template.render(**ctx)
|
||||
|
||||
|
||||
@router.get("/help", response_class=HTMLResponse)
|
||||
async def help_page(request: Request):
|
||||
"""Comprehensive help & documentation page."""
|
||||
from jinja2 import Environment, FileSystemLoader, BaseLoader
|
||||
env = Environment(loader=FileSystemLoader("app/templates"))
|
||||
sidebar = _sidebar_data(request, [])
|
||||
# Render via a block-based template so content_html lands in {% block content %}
|
||||
block_tpl = env.from_string(
|
||||
'{% extends "base.html" %}{% block content %}{{ content_html|safe }}{% endblock %}'
|
||||
)
|
||||
return HTMLResponse(block_tpl.render(
|
||||
**sidebar,
|
||||
request=request,
|
||||
page_title="Help",
|
||||
title_prefix="Help",
|
||||
page_icon="❓",
|
||||
content_html="""<style>
|
||||
.help-page{max-width:900px;margin:0 auto;padding:40px 24px 80px;}
|
||||
.help-hero{text-align:center;margin-bottom:48px;}
|
||||
.help-hero h1{font-size:32px;font-weight:800;margin:0 0 8px;}
|
||||
.help-hero p{font-size:16px;color:var(--text-dim);max-width:500px;margin:0 auto;}
|
||||
.help-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px;margin-bottom:48px;}
|
||||
.help-card{background:var(--bg-card);border:1px solid var(--border);border-radius:12px;padding:24px;transition:border-color .15s;}
|
||||
.help-card:hover{border-color:rgba(255,255,255,.12);}
|
||||
.help-card h3{font-size:15px;font-weight:600;margin:0 0 4px;display:flex;align-items:center;gap:8px;}
|
||||
.help-card .icon{font-size:20px;}
|
||||
.help-card p{font-size:13px;color:var(--text-dim);line-height:1.5;margin:8px 0 0;}
|
||||
.help-card ul{list-style:none;padding:0;margin:12px 0 0;}
|
||||
.help-card li{font-size:13px;padding:3px 0;color:var(--text-dim);}
|
||||
.help-card li::before{content:'• ';color:var(--accent);}
|
||||
.help-section{margin-bottom:48px;}
|
||||
.help-section h2{font-size:20px;font-weight:700;margin:0 0 16px;padding-bottom:8px;border-bottom:1px solid var(--border);}
|
||||
.help-kbd{display:inline-block;padding:2px 8px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:4px;font-family:monospace;font-size:12px;color:var(--text);min-width:16px;text-align:center;}
|
||||
.help-shortcut-row{display:flex;align-items:center;gap:12px;padding:8px 12px;border-radius:6px;margin-bottom:2px;}
|
||||
.help-shortcut-row:hover{background:var(--bg-hover);}
|
||||
.help-shortcut-row .keys{display:flex;gap:4px;min-width:140px;}
|
||||
.help-shortcut-row .desc{font-size:13px;color:var(--text-dim);}
|
||||
.help-badge{display:inline-block;padding:2px 10px;border-radius:20px;font-size:11px;font-weight:600;}
|
||||
.help-badge.local{background:rgba(35,131,226,.15);color:#2C8CEB;}
|
||||
.help-badge.gitea{background:rgba(0,200,100,.15);color:#00CC66;}
|
||||
.help-badge.github{background:rgba(130,80,220,.15);color:#A060F0;}
|
||||
</style>
|
||||
<div class="help-page">
|
||||
<div class="help-hero">
|
||||
<h1>❓ FlowDeck Help</h1>
|
||||
<p>Everything you need to know about your Notion-style workspace with Gitea & GitHub integration.</p>
|
||||
</div>
|
||||
|
||||
<div class="help-grid">
|
||||
<div class="help-card">
|
||||
<h3><span class="icon">🚀</span>Getting Started</h3>
|
||||
<p>FlowDeck is your private, self-hosted workspace. Create pages, organize projects, and integrate with your Git forge.</p>
|
||||
<ul>
|
||||
<li>Create a workspace from the <b>Workspaces</b> page</li>
|
||||
<li>Click <b>📄 New Page</b> in the sidebar to start writing</li>
|
||||
<li>Use <span class="help-kbd">Ctrl+N</span> anywhere to create a page</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="help-card">
|
||||
<h3><span class="icon">📝</span>Pages & Editor</h3>
|
||||
<p>Notion-style block editor with slash commands, markdown shortcuts, and rich formatting.</p>
|
||||
<ul>
|
||||
<li>Type <span class="help-kbd">/</span> for the slash command menu</li>
|
||||
<li>Drag & drop pages in the sidebar to reorganize</li>
|
||||
<li>Right-click for context menu (duplicate, rename, delete)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="help-card">
|
||||
<h3><span class="icon">📁</span>Workspaces</h3>
|
||||
<p>Organize your work into separate workspaces. Each has its own pages and files.</p>
|
||||
<ul>
|
||||
<li><span class="help-badge local">Local</span> Files stored on your server</li>
|
||||
<li><span class="help-badge gitea">Gitea</span> Connect to browse & edit repos</li>
|
||||
<li><span class="help-badge github">GitHub</span> Connect via Settings → Integrations</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="help-card">
|
||||
<h3><span class="icon">🦎</span>Gitea Integration</h3>
|
||||
<p>Connect your Gitea account to access repositories directly from FlowDeck.</p>
|
||||
<ul>
|
||||
<li>Go to <b>Settings → Integrations</b> to connect</li>
|
||||
<li>Browse repo file trees in the sidebar</li>
|
||||
<li>Create & edit files with commit messages</li>
|
||||
<li>Sync labels as tags</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="help-card">
|
||||
<h3><span class="icon">🌐</span>Sharing & Publishing</h3>
|
||||
<p>Share pages with collaborators or publish them to the web.</p>
|
||||
<ul>
|
||||
<li>Click <b>Share</b> in the page editor top-right</li>
|
||||
<li>Share with specific users or get a public link</li>
|
||||
<li>Publish to make a page visible at <code>/p/your-slug</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="help-card">
|
||||
<h3><span class="icon">📚</span>Library, Trash & Tasks</h3>
|
||||
<p>Find all your content in one place with powerful filtering.</p>
|
||||
<ul>
|
||||
<li><b>Library</b> — Tabs for Recents, Favorites, Shared, Published</li>
|
||||
<li><b>Trash</b> — Soft-deleted pages (30-day retention)</li>
|
||||
<li><b>My Tasks</b> — Aggregated tasks from all collections</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
<h2>⌨️ Keyboard Shortcuts</h2>
|
||||
<div class="help-shortcut-row"><div class="keys"><span class="help-kbd">Ctrl</span>+<span class="help-kbd">N</span></div><div class="desc">Create new page</div></div>
|
||||
<div class="help-shortcut-row"><div class="keys"><span class="help-kbd">Ctrl</span>+<span class="help-kbd">K</span></div><div class="desc">Quick find / command palette</div></div>
|
||||
<div class="help-shortcut-row"><div class="keys"><span class="help-kbd">Ctrl</span>+<span class="help-kbd">S</span></div><div class="desc">Save current page</div></div>
|
||||
<div class="help-shortcut-row"><div class="keys"><span class="help-kbd">F2</span></div><div class="desc">Rename selected item</div></div>
|
||||
<div class="help-shortcut-row"><div class="keys"><span class="help-kbd">Delete</span></div><div class="desc">Move selected item to trash</div></div>
|
||||
<div class="help-shortcut-row"><div class="keys"><span class="help-kbd">Escape</span></div><div class="desc">Close modal / cancel editing</div></div>
|
||||
<div class="help-shortcut-row"><div class="keys"><span class="help-kbd">Enter</span></div><div class="desc">Open selected page</div></div>
|
||||
<div class="help-shortcut-row"><div class="keys"><span class="help-kbd">Ctrl</span>+<span class="help-kbd">O</span></div><div class="desc">New AI chat (in footer)</div></div>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
<h2>🔐 Authentication</h2>
|
||||
<p style="color:var(--text-dim);font-size:14px;line-height:1.6;">
|
||||
FlowDeck supports three authentication methods:<br>
|
||||
<span class="help-badge local">Local</span> Email + password — create an account on the login page.<br>
|
||||
<span class="help-badge gitea">Gitea OAuth</span> Login with your Gitea account. Your repos appear as workspaces.<br>
|
||||
<span class="help-badge github">GitHub OAuth</span> Login or link your GitHub account in Settings → Integrations.<br><br>
|
||||
<b>Tip:</b> You can connect Gitea/GitHub to an existing local account — your identity stays as your local user.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
<h2>💡 Tips</h2>
|
||||
<p style="color:var(--text-dim);font-size:14px;line-height:1.6;">
|
||||
• Toggle the sidebar with the <b>«</b> button in the top-left corner.<br>
|
||||
• Switch between workspaces using the dropdown menu in the sidebar header.<br>
|
||||
• The <b>Private</b> section appears when a remote workspace is active — files here stay local.<br>
|
||||
• Hover over any sidebar item to see action buttons (favorite, share, delete).<br>
|
||||
• Use <b>Ctrl+Click</b> or <b>Shift+Click</b> to multi-select items in the sidebar.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>"""
|
||||
))
|
||||
|
||||
|
||||
@router.get("/accounts/settings", response_class=HTMLResponse)
|
||||
async def settings_page(request: Request):
|
||||
"""User settings page — profile, forges, tokens."""
|
||||
|
||||
+60
-91
@@ -6,6 +6,7 @@ import logging
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException, Query
|
||||
from fastapi.responses import HTMLResponse
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
from app.db import get_conn
|
||||
from app.auth.session import SessionManager
|
||||
@@ -38,114 +39,82 @@ async def my_tasks_dashboard(request: Request, view: str = "all", days: int = 7)
|
||||
|
||||
collection_tasks = []
|
||||
for p in pages:
|
||||
props = json.loads(p["property_values_json"])
|
||||
# Check if assigned to current user
|
||||
assigned = False
|
||||
for v in props.values():
|
||||
if isinstance(v, list):
|
||||
for item in v:
|
||||
if isinstance(item, dict) and item.get("login") == user_login:
|
||||
assigned = True
|
||||
break
|
||||
elif isinstance(v, str) and user_login.lower() in v.lower():
|
||||
assigned = True
|
||||
if assigned:
|
||||
break
|
||||
|
||||
if not assigned and user_login == "admin":
|
||||
assigned = True # admin sees all
|
||||
|
||||
if not assigned:
|
||||
props = {}
|
||||
try:
|
||||
props = json.loads(p["property_values_json"] or "{}")
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
due_date = props.get("due_date", "")
|
||||
status = props.get("status", "—")
|
||||
assignee = props.get("assignee", "")
|
||||
if view != "all" and assignee and assignee != user_login:
|
||||
continue
|
||||
|
||||
# Extract status and due date
|
||||
status = None
|
||||
due_date = None
|
||||
for v in props.values():
|
||||
if isinstance(v, str) and v in ("Todo", "In Progress", "Done", "Complete"):
|
||||
status = v
|
||||
elif isinstance(v, str) and v.startswith("20"):
|
||||
due_date = v[:10]
|
||||
|
||||
# Filter by view
|
||||
if view == "today":
|
||||
from datetime import date
|
||||
if not due_date or due_date != date.today().isoformat():
|
||||
continue
|
||||
elif view == "overdue":
|
||||
from datetime import date
|
||||
if not due_date or due_date >= date.today().isoformat():
|
||||
continue
|
||||
elif view == "upcoming":
|
||||
from datetime import date, timedelta
|
||||
if not due_date:
|
||||
continue
|
||||
today = date.today()
|
||||
limit = today + timedelta(days=days)
|
||||
d = date.fromisoformat(due_date)
|
||||
if d < today or d > limit:
|
||||
continue
|
||||
|
||||
collection_tasks.append({
|
||||
"id": p["id"], "title": p["title"], "icon": p.get("icon", "📄"),
|
||||
"status": status, "due_date": due_date, "props": props,
|
||||
"id": p["id"],
|
||||
"title": p["title"] or "Untitled",
|
||||
"icon": p["icon"] or "📋",
|
||||
"status": status,
|
||||
"due_date": due_date,
|
||||
})
|
||||
|
||||
if collection_tasks:
|
||||
grouped[col["name"]] = collection_tasks
|
||||
|
||||
total = sum(len(t) for t in grouped.values())
|
||||
|
||||
# Render
|
||||
# Build content HTML
|
||||
sections = ""
|
||||
for col_name, tasks in grouped.items():
|
||||
items = ""
|
||||
for t in tasks:
|
||||
due_badge = f"<span class='mt-due'>{t['due_date']}</span>" if t.get("due_date") else ""
|
||||
status_badge = f"<span class='mt-status mt-{t['status'].lower().replace(' ','-') if t.get('status') else 'none'}'>{t.get('status','—')}</span>"
|
||||
items += f"""<div class='mt-item'>
|
||||
<span class='mt-icon'>{t['icon']}</span>
|
||||
<span class='mt-title'>{t['title']}</span>
|
||||
{status_badge}{due_badge}
|
||||
</div>"""
|
||||
status_cls = t['status'].lower().replace(' ', '-') if t.get('status') else 'none'
|
||||
status_badge = f"<span class='mt-status mt-{status_cls}'>{t.get('status', '—')}</span>"
|
||||
items += f"<div class='mt-item'><span class='mt-icon'>{t['icon']}</span><span class='mt-title'>{t['title']}</span>{status_badge}{due_badge}</div>"
|
||||
|
||||
sections += f"""<div class='mt-section'>
|
||||
<div class='mt-section-header'>{col_name} <span class='mt-count'>{len(tasks)}</span></div>
|
||||
{items}
|
||||
</div>"""
|
||||
sections += f"<div class='mt-section'><div class='mt-section-header'>{col_name} <span class='mt-count'>{len(tasks)}</span></div>{items}</div>"
|
||||
|
||||
return HTMLResponse(f"""<!DOCTYPE html>
|
||||
<html><head><meta charset="utf-8"><title>My Tasks — FlowDeck</title>
|
||||
<style>
|
||||
body{{font-family:system-ui,sans-serif;background:#191919;color:#fff;margin:0;padding:20px}}
|
||||
h1{{font-size:24px;margin:0 0 16px}}
|
||||
.view-tabs{{display:flex;gap:4px;margin-bottom:20px;border-bottom:1px solid #333;padding-bottom:8px}}
|
||||
.tab{{padding:6px 14px;border-radius:6px;cursor:pointer;color:#A0A0A0;font-size:13px;background:none;border:none;text-decoration:none}}
|
||||
.tab:hover,.tab.active{{background:#333;color:#fff}}
|
||||
.mt-section{{margin-bottom:24px}}
|
||||
.mt-section-header{{font-size:14px;font-weight:600;color:#A0A0A0;margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px}}
|
||||
.mt-count{{color:#6B6B6B;font-weight:400}}
|
||||
.mt-item{{display:flex;align-items:center;gap:10px;padding:8px 12px;background:#1a1a1a;border-radius:6px;margin-bottom:3px;border:1px solid #222}}
|
||||
.mt-item:hover{{border-color:#444}}
|
||||
.mt-icon{{font-size:16px}}
|
||||
.mt-title{{flex:1;font-size:14px}}
|
||||
.mt-status{{font-size:11px;padding:2px 8px;border-radius:4px}}
|
||||
.mt-due{{font-size:11px;color:#A0A0A0}}
|
||||
.mt-todo{{background:#333;color:#A0A0A0}}
|
||||
.mt-in-progress{{background:#1a2744;color:#3366CC}}
|
||||
.mt-done,.mt-complete{{background:#1a332a;color:#00CC66}}
|
||||
.p_desc{{color:#A0A0A0;margin-top:8px}}
|
||||
</style></head><body>
|
||||
<h1>📋 My Tasks</h1>
|
||||
<div class="view-tabs">
|
||||
<a class="tab{' active' if view=='all' else ''}" href="?view=all">All</a>
|
||||
<a class="tab{' active' if view=='today' else ''}" href="?view=today">Today</a>
|
||||
<a class="tab{' active' if view=='overdue' else ''}" href="?view=overdue">Overdue</a>
|
||||
<a class="tab{' active' if view=='upcoming' else ''}" href="?view=upcoming&days=7">Next 7 days</a>
|
||||
content_html = f"""<div style="max-width:800px;margin:0 auto;padding:40px 24px;">
|
||||
<h1 style="font-size:24px;margin:0 0 8px;">📋 My Tasks</h1>
|
||||
<p style="color:var(--text-dim);font-size:14px;margin-bottom:24px;">{total} tasks across {len(grouped)} collections</p>
|
||||
<div class="view-tabs" style="display:flex;gap:4px;margin-bottom:24px;border-bottom:1px solid var(--border);padding-bottom:12px;">
|
||||
<a class="tab{' active' if view=='all' else ''}" href="/my-tasks?view=all" style="padding:6px 14px;border-radius:6px;cursor:pointer;color:var(--text-dim);font-size:13px;text-decoration:none;">All</a>
|
||||
<a class="tab{' active' if view=='today' else ''}" href="/my-tasks?view=today" style="padding:6px 14px;border-radius:6px;cursor:pointer;color:var(--text-dim);font-size:13px;text-decoration:none;">Today</a>
|
||||
<a class="tab{' active' if view=='overdue' else ''}" href="/my-tasks?view=overdue" style="padding:6px 14px;border-radius:6px;cursor:pointer;color:var(--text-dim);font-size:13px;text-decoration:none;">Overdue</a>
|
||||
<a class="tab{' active' if view=='upcoming' else ''}" href="/my-tasks?view=upcoming&days=7" style="padding:6px 14px;border-radius:6px;cursor:pointer;color:var(--text-dim);font-size:13px;text-decoration:none;">Next 7 days</a>
|
||||
</div>
|
||||
<style>
|
||||
.tab.active{{background:var(--accent);color:#fff!important;}}
|
||||
.tab:hover{{background:var(--bg-hover);color:var(--text);}}
|
||||
.mt-section{{margin-bottom:24px;}}
|
||||
.mt-section-header{{font-size:13px;font-weight:600;color:var(--text-dim);margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px;}}
|
||||
.mt-count{{color:var(--text-dim);font-weight:400;opacity:.5;}}
|
||||
.mt-item{{display:flex;align-items:center;gap:10px;padding:10px 12px;background:var(--bg-card);border-radius:8px;margin-bottom:3px;border:1px solid var(--border);}}
|
||||
.mt-item:hover{{border-color:var(--accent);}}
|
||||
.mt-icon{{font-size:16px;}}
|
||||
.mt-title{{flex:1;font-size:14px;}}
|
||||
.mt-status{{font-size:11px;padding:2px 8px;border-radius:4px;}}
|
||||
.mt-due{{font-size:11px;color:var(--text-dim);}}
|
||||
.mt-todo,.mt-none{{background:rgba(255,255,255,.05);color:var(--text-dim);}}
|
||||
.mt-in-progress{{background:rgba(35,131,226,.15);color:#2C8CEB;}}
|
||||
.mt-done,.mt-completed,.mt-complete{{background:rgba(0,200,100,.15);color:#00CC66;}}
|
||||
</style>
|
||||
{sections}
|
||||
<p class="p_desc">{total} tasks across {len(grouped)} collections</p>
|
||||
</body></html>""")
|
||||
</div>"""
|
||||
|
||||
from app.routers.dashboard import _sidebar_data
|
||||
env = Environment(loader=FileSystemLoader("app/templates"))
|
||||
sidebar = _sidebar_data(request, [])
|
||||
block_tpl = env.from_string(
|
||||
'{% extends "base.html" %}{% block content %}{{ content_html|safe }}{% endblock %}'
|
||||
)
|
||||
return block_tpl.render(
|
||||
**sidebar,
|
||||
request=request,
|
||||
content_html=content_html,
|
||||
page_title="My Tasks",
|
||||
title_prefix="My Tasks",
|
||||
page_icon="📋",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api")
|
||||
|
||||
+48
-21
@@ -202,20 +202,30 @@
|
||||
<div class="sidebar-section-header" @click="toggleSection('workspace')">
|
||||
<div class="sidebar-section-title">
|
||||
<span class="chevron" :class="{ open: sectionsOpen.workspace }" x-text="sectionsOpen.workspace ? '▼' : '▶'"></span>
|
||||
{% if has_active_workspace %}
|
||||
<span>{% if gitea_workspace %}📁 {{ active_ws_name }}{% elif workspace_key and '/' in workspace_key %}🔗{% else %}📁{% endif %} {{ active_ws_name if not gitea_workspace else '' }}</span>
|
||||
{% else %}
|
||||
<span>📁 No workspace open</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if has_active_workspace %}
|
||||
<div class="sidebar-section-actions">
|
||||
<button class="section-action-btn" title="New Page" @click.stop="window.FlowDeck.createPage()">📄</button>
|
||||
<button class="section-action-btn" title="New Folder" @click.stop="window.FlowDeck.showCreateFolderModal()">📁</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% from '_workspace_tree_macro.html' import render_workspace_tree %}
|
||||
<div class="sidebar-section-items" x-show="sectionsOpen.workspace" x-transition>
|
||||
<ul class="sidebar-items" data-section="workspace" id="sidebar-workspace-items">
|
||||
{% if workspace_pages %}
|
||||
{{ render_workspace_tree(workspace_pages) }}
|
||||
{% if has_active_workspace %}
|
||||
{% if workspace_pages %}
|
||||
{{ render_workspace_tree(workspace_pages) }}
|
||||
{% else %}
|
||||
<li class="sidebar-item empty-hint"><span class="page-icon">📄</span><span class="page-name text-dim">No pages yet</span></li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<li class="sidebar-item empty-hint"><span class="page-icon">📄</span><span class="page-name text-dim">No pages yet</span></li>
|
||||
<li class="sidebar-item empty-hint"><span class="page-icon">📁</span><span class="page-name text-dim">Open a workspace to see your files</span></li>
|
||||
{% endif %}
|
||||
<li class="sidebar-item" style="margin-top:4px;border-top:1px solid var(--border);padding-top:8px;" onclick="window.location='/workspaces'">
|
||||
<span class="page-icon">⚙</span><span class="page-name text-dim">Manage Workspaces</span>
|
||||
@@ -252,7 +262,9 @@
|
||||
<span>Meetings</span>
|
||||
</div>
|
||||
<div class="sidebar-section-actions">
|
||||
{% if has_active_workspace %}
|
||||
<button class="section-action-btn" title="Add" @click.stop="window.FlowDeck.createPage()">+</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-section-items" x-show="sectionsOpen.meetings" x-transition>
|
||||
@@ -387,24 +399,21 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Notion apps / Utilities -->
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-header" @click="toggleSection('apps')">
|
||||
<div class="sidebar-section-title">
|
||||
<span class="chevron" :class="{ open: sectionsOpen.apps }" x-text="sectionsOpen.apps ? '▼' : '▶'"></span>
|
||||
<span>Notion apps</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-section-items" x-show="sectionsOpen.apps" x-transition>
|
||||
<ul class="sidebar-items">
|
||||
<li class="sidebar-item" @click="navigateTo('/library')"><span class="page-icon">📚</span><span class="page-name">Library</span></li>
|
||||
<li class="sidebar-item"><span class="page-icon">✅</span><span class="page-name">My Tasks</span></li>
|
||||
{% if auth_method == 'local' or (workspace_pages and workspace_pages|length > 0) %}
|
||||
<li class="sidebar-item" @click="navigateTo('/trash')"><span class="page-icon">🗑️</span><span class="page-name">Trash</span></li>
|
||||
{% endif %}
|
||||
<li class="sidebar-item"><span class="page-icon">❓</span><span class="page-name">Help</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Notion apps / Utilities → replaced by static links footer (v4.0.2) -->
|
||||
<!-- Bottom links (always visible, no section wrapper) -->
|
||||
<div class="sidebar-static-links" style="padding: 4px 12px;">
|
||||
<a href="/library" class="sidebar-item" style="text-decoration:none;color:inherit;display:flex;align-items:center;gap:10px;padding:6px 8px;border-radius:6px;cursor:pointer;transition:background 150ms;">
|
||||
<span class="page-icon">📚</span><span class="page-name" style="font-size:14px;">Library</span>
|
||||
</a>
|
||||
<a href="/my-tasks" class="sidebar-item" style="text-decoration:none;color:inherit;display:flex;align-items:center;gap:10px;padding:6px 8px;border-radius:6px;cursor:pointer;transition:background 150ms;">
|
||||
<span class="page-icon">✅</span><span class="page-name" style="font-size:14px;">My Tasks</span>
|
||||
</a>
|
||||
<a href="/trash" class="sidebar-item" style="text-decoration:none;color:inherit;display:flex;align-items:center;gap:10px;padding:6px 8px;border-radius:6px;cursor:pointer;transition:background 150ms;">
|
||||
<span class="page-icon">🗑️</span><span class="page-name" style="font-size:14px;">Trash</span>
|
||||
</a>
|
||||
<a href="/help" class="sidebar-item" style="text-decoration:none;color:inherit;display:flex;align-items:center;gap:10px;padding:6px 8px;border-radius:6px;cursor:pointer;transition:background 150ms;">
|
||||
<span class="page-icon">❓</span><span class="page-name" style="font-size:14px;">Help</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
@@ -414,9 +423,11 @@
|
||||
<span style="font-size:14px;">💬</span> New chat
|
||||
<span class="shortcut">Ctrl+O</span>
|
||||
</button>
|
||||
{% if has_active_workspace %}
|
||||
<button class="sidebar-footer-btn" style="width:36px; justify-content:center; background:var(--bg-hover); border-radius:8px;" onclick="window.FlowDeck.createPage()" title="New page">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/></svg>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /sidebar-scroll -->
|
||||
@@ -1122,6 +1133,14 @@
|
||||
});
|
||||
},
|
||||
newPageInWorkspace() {
|
||||
// Guard: no workspace active — redirect to workspaces page
|
||||
if (!this.workspaceKey || this.workspaceKey === this.workspaceName) {
|
||||
if (!this.giteaWorkspace && !this.localWsId) {
|
||||
this.toast('No workspace open — create one first', 'info');
|
||||
setTimeout(() => { window.location.href = '/workspaces'; }, 1200);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const name = prompt('File name:');
|
||||
if (!name || !name.trim()) return;
|
||||
const wk = this.workspaceKey || '';
|
||||
@@ -1149,6 +1168,14 @@
|
||||
.catch(err => { this.toast('Failed: ' + err.message, 'error'); });
|
||||
},
|
||||
newFolderInWorkspace() {
|
||||
// Guard: no workspace active — redirect to workspaces page
|
||||
if (!this.workspaceKey || this.workspaceKey === this.workspaceName) {
|
||||
if (!this.giteaWorkspace && !this.localWsId) {
|
||||
this.toast('No workspace open — create one first', 'info');
|
||||
setTimeout(() => { window.location.href = '/workspaces'; }, 1200);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const name = prompt('Folder name:');
|
||||
if (!name || !name.trim()) return;
|
||||
const wk = this.workspaceKey || '';
|
||||
|
||||
Reference in New Issue
Block a user