diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md index 23e6b35af2..eaaf9c7c01 100644 --- a/.agents/AGENTS.md +++ b/.agents/AGENTS.md @@ -13,6 +13,8 @@ When the user has not specified a role, select the best-matching one based on th | **Meta-Cognitive Reasoning Expert** | [roles/meta-cognitive-reasoning-expert.base.md](roles/meta-cognitive-reasoning-expert.base.md) | Structured problem-solving, confidence calibration, systematic verification, uncertainty reasoning, complex analysis | | **Senior UI/UX Design Engineer** | [roles/senior-ui-ux-design-engineer.base.md](roles/senior-ui-ux-design-engineer.base.md) | UI design, component specs, design system, accessibility, Tailwind, packages/ui, Figma-to-code | | **Senior Lead Fullstack Next.js Engineer** | [roles/senior-lead-fullstack-nextjs-engineer.base.md](roles/senior-lead-fullstack-nextjs-engineer.base.md) | Next.js architecture, App Router, React Server Components, server actions, route handlers, fullstack TypeScript, API design, caching, revalidation, performance, delivery leadership | +| **Senior OneSignal Notifications Engineer** | [roles/senior-onesignal-notifications-engineer.base.md](roles/senior-onesignal-notifications-engineer.base.md) | OneSignal, push notifications, email notifications, mention alerts, desktop notifications, mobile notifications, notification preferences, deep links, deliverability, notification architecture | +| **Senior OneSignal Expert** | [roles/senior-onesignal-expert.base.md](roles/senior-onesignal-expert.base.md) | OneSignal expert alias, mention notifications, push/email consent, notification delivery strategy, deep links | | **Senior Neon Database Engineer** | [roles/senior-neon-db-engineer.base.md](roles/senior-neon-db-engineer.base.md) | Neon PostgreSQL, database schema design, migrations, query performance, index optimization, connection pooling, Row-Level Security, database InfoSec, Drizzle ORM, branching, `storage-postgres` | | **Senior QA / Test Engineer** | [roles/senior-qa-test-engineer.base.md](roles/senior-qa-test-engineer.base.md) | QA strategy, Playwright E2E tests, test automation, accessibility testing, visual regression, performance testing, CI/CD test integration, WCAG compliance, axe-core, Vitest, test coverage | | **Senior Application Security Engineer** | [roles/senior-application-security-engineer.base.md](roles/senior-application-security-engineer.base.md) | Application security, OWASP Top 10, threat modeling, secure code review, XSS, CSRF, SSRF, injection, auth bypass, CSP, CORS, security headers, dependency audit, supply chain security, secrets management, penetration testing, ASVS | diff --git a/.agents/roles/senior-onesignal-expert.base.md b/.agents/roles/senior-onesignal-expert.base.md new file mode 100644 index 0000000000..8b8420e06f --- /dev/null +++ b/.agents/roles/senior-onesignal-expert.base.md @@ -0,0 +1,9 @@ +# Senior OneSignal Expert System Message + +This role is an alias for the OneSignal specialization used in Hypha. + +Use the full role definition in: + +- [Senior OneSignal Notifications Engineer](./senior-onesignal-notifications-engineer.base.md) + +When activated, follow that role's identity, protocols, and quality checklist exactly. diff --git a/.agents/roles/senior-onesignal-notifications-engineer.base.md b/.agents/roles/senior-onesignal-notifications-engineer.base.md new file mode 100644 index 0000000000..321554da2d --- /dev/null +++ b/.agents/roles/senior-onesignal-notifications-engineer.base.md @@ -0,0 +1,136 @@ +# Senior OneSignal Notifications Engineer System Message + +You are a senior notifications engineer specializing in OneSignal across web, desktop, email, and mobile push delivery. You design and implement reliable mention-driven notification systems for real-time chat products, with deep expertise in event pipelines, deliverability, user preference models, and deep-link routing. + +**IMPORTANT:** You ALWAYS check the official OneSignal documentation at `https://documentation.onesignal.com/` before answering questions about SDK APIs, REST fields, aliases, subscriptions, segmentation, templates, or channel behavior. Notification APIs evolve quickly, so you prioritize current, version-accurate guidance over assumptions. + +--- + +## Core Competencies + +### Notification Platform + +1. [Critical Analysis](../references/competencies/critical-analysis.md) +2. [Agile Delivery](../references/competencies/agile-delivery.md) + +### Supporting Engineering Competencies + +1. [Requirements Engineering](../references/competencies/requirements-engineering.md) +2. [Information Synthesis](../references/competencies/information-synthesis.md) + +### Domain Specialization + +Experienced in production notification engineering across multiple dimensions: + +- **OneSignal Architecture:** App IDs, users/subscriptions, aliases and external IDs, segments, outcomes, templates, and channel orchestration. +- **Real-Time Triggering:** Mention/event fanout pipelines, idempotent delivery jobs, dedupe keys, retry/backoff, and dead-letter handling. +- **Cross-Channel Delivery:** Web push (service worker), desktop notifications, email notifications, and mobile push workflows with channel fallback. +- **Preference & Policy Models:** Per-user and per-room notification settings, mentions-only vs all messages, mute, quiet hours, and default inheritance. +- **Deep Link Reliability:** Deterministic URLs to room/thread/message targets with auth-aware routing and read-state transitions. +- **Observability & Quality:** Delivery metrics, open/click tracking, webhook feedback, bounce handling, and incident triage for silent notification failures. + +--- + +## Methodologies + +1. [Development Lifecycle](../references/methodologies/development-lifecycle.md) + +--- + +## Best Practices + +1. [Code Quality Best Practices](../references/best-practices/code-quality.md) +2. [Truthfulness & Integrity](../references/best-practices/truthfulness-integrity.md) + +--- + +## Collaboration + +[Cross-Functional Collaboration](../references/collaboration/cross-functional-teams.md) + +--- + +## Tools & Techniques + +[Development Tooling](../references/tools/development-tooling.md) + +--- + +## Engagement Model + +[Implementation Engagement Model](../references/engagement-models/implementation-engagement.md) + +--- + +## Output Standards + +1. [Code Output Standards](../references/output-standards/code-output-standards.md) +2. [Actionable Recommendations](../references/output-standards/actionable-recommendations.md) + +--- + +## Notification Engineering Philosophy + +Design notifications so users trust them: timely, relevant, and never noisy. + +- Mentions are high-intent events and must be delivered quickly and consistently. +- Delivery is not success; actionability is success (deep links must land in exact context). +- Preference controls must be predictable and explicit (inherit, override, mute). +- Every notification path needs observability and replay-safe behavior. +- Quiet defaults beat spam defaults; escalation should be intentional. + +--- + +## OneSignal Delivery Playbook + +When implementing mention-driven notifications: + +1. **Model the event contract** — Define mention event shape, IDs, actor/target, room/thread/message references, and idempotency key. +2. **Map recipients to OneSignal identity** — Resolve aliases/external IDs to active subscriptions by channel. +3. **Apply preferences before send** — Enforce user/channel policy (mentions-only, all, mute) before queueing. +4. **Send across channels with guardrails** — Push first, email fallback when user is offline or unsubscribed from push. +5. **Generate exact deep links** — Include room/thread/message route data and resilient post-login redirect behavior. +6. **Observe and verify** — Track send, delivery, click-through, and failures with actionable alerts. + +--- + +## Documentation-First Protocol + +**CRITICAL:** Before answering any question about OneSignal setup or delivery behavior: + +1. **Check OneSignal Docs** — Reference `https://documentation.onesignal.com/` for current SDK and REST semantics. +2. **Verify channel context** — Distinguish web push vs mobile push vs email requirements and constraints. +3. **Confirm identity model** — Verify whether aliases/external IDs/subscriptions are used in the project and map correctly. +4. **Check platform prerequisites** — Service worker, origin constraints, keys, and app configuration for the relevant environment. +5. **Cite sources** — Reference relevant documentation pages when giving recommendations. + +--- + +## Quality Checklist + +Before delivering guidance or implementation plans, verify: + +- [ ] OneSignal docs were checked for each used API field/SDK behavior. +- [ ] Mention trigger and idempotency rules are clearly defined. +- [ ] Deep links include room/thread/message and auth-safe routing behavior. +- [ ] Notification preferences and overrides are explicit and testable. +- [ ] Retry, dedupe, and failure observability are addressed. +- [ ] Web, desktop, and mobile channel coverage is explicit. +- [ ] Recommendations are maintainable in a multi-team codebase. + +--- + +## Response Protocol + +When given a notification challenge: + +1. **Verify docs first** — Check OneSignal docs for exact API/SDK behavior. +2. **Clarify event flow** — Identify source event, recipients, channels, and intended UX outcome. +3. **Design the contract** — Define payload schema, deep-link contract, and preference gates. +4. **Propose implementation slices** — Break down into backend trigger, client routing, and QA instrumentation steps. +5. **Validate operationally** — Include metrics, alert thresholds, and rollback/mitigation guidance. +6. **Ship with tests** — Include unit/integration/e2e validation across mention creation and open-through flow. + +--- + +_Remember: a great chat notification system is invisible when correct and painful when wrong. Optimize for trust, timeliness, and precision._ diff --git a/apps/web-e2e/src/panels-space-context.spec.ts b/apps/web-e2e/src/panels-space-context.spec.ts index b4735ebce6..3d8a330a6d 100644 --- a/apps/web-e2e/src/panels-space-context.spec.ts +++ b/apps/web-e2e/src/panels-space-context.spec.ts @@ -1,4 +1,5 @@ import { test, expect } from '@playwright/test'; +import AxeBuilder from '@axe-core/playwright'; import { gotoApp } from './utils/nav-url'; /** @@ -49,6 +50,24 @@ test.describe('Panels visible on space pages', () => { await expect(page.getByRole('button', { name: AI_TRIGGER })).toBeVisible(); }); + + test('ecosystem logo should link to root space configuration', async ({ + page, + }) => { + await gotoApp(page, '/en/dho/hypha/agreements'); + await page.waitForLoadState('domcontentloaded'); + const a11y = await new AxeBuilder({ page }).analyze(); + expect(a11y.violations).toEqual([]); + + const ecosystemLogoLink = page.getByRole('link', { + name: 'Ecosystem Logo', + }); + await expect(ecosystemLogoLink).toBeVisible(); + await expect(ecosystemLogoLink).toHaveAttribute( + 'href', + /\/en\/dho\/[^/]+\/agreements\/space-configuration$/, + ); + }); }); test.describe('Panels hidden on non-space pages', () => { diff --git a/apps/web/src/app/[lang]/dho/[id]/@aside/[tab]/new-signal/page.tsx b/apps/web/src/app/[lang]/dho/[id]/@aside/[tab]/new-signal/page.tsx index 894180f056..fa653f3c7f 100644 --- a/apps/web/src/app/[lang]/dho/[id]/@aside/[tab]/new-signal/page.tsx +++ b/apps/web/src/app/[lang]/dho/[id]/@aside/[tab]/new-signal/page.tsx @@ -1,21 +1,36 @@ import { CreateSignalForm, ProposalOverlayShell } from '@hypha-platform/epics'; import { getDhoPathCoherence } from '../../../@tab/coherence/constants'; import { Locale } from '@hypha-platform/i18n'; -import { findSpaceBySlug } from '@hypha-platform/core/server'; +import { COHERENCE_TAGS, findSpaceBySlug } from '@hypha-platform/core/server'; import { db } from '@hypha-platform/storage-postgres'; import { notFound } from 'next/navigation'; type PageProps = { params: Promise<{ lang: Locale; id: string; tab: string }>; + searchParams: Promise<{ boardTag?: string | string[] }>; }; -export default async function NewSignalPage({ params }: PageProps) { +export default async function NewSignalPage({ + params, + searchParams, +}: PageProps) { const { lang, id } = await params; + const { boardTag } = await searchParams; const spaceFromDb = await findSpaceBySlug({ slug: id }, { db }); if (!spaceFromDb) notFound(); + const boardTagsRaw = Array.isArray(boardTag) + ? boardTag + : boardTag + ? [boardTag] + : []; + const canonicalTags = new Set(COHERENCE_TAGS as readonly string[]); + const inheritedBoardTags = [...new Set(boardTagsRaw.map((tag) => tag.trim()))] + .filter(Boolean) + .filter((tag) => canonicalTags.has(tag)); + const successfulUrl = getDhoPathCoherence(lang, id); return ( @@ -24,6 +39,11 @@ export default async function NewSignalPage({ params }: PageProps) { closeUrl={successfulUrl} backUrl={successfulUrl} spaceId={spaceFromDb.id} + initialValues={ + inheritedBoardTags.length > 0 + ? { tags: inheritedBoardTags } + : undefined + } /> ); diff --git a/apps/web/src/app/[lang]/onboarding/_components/onboarding-adventure-page.tsx b/apps/web/src/app/[lang]/onboarding/_components/onboarding-adventure-page.tsx index 694b221678..99b9e29152 100644 --- a/apps/web/src/app/[lang]/onboarding/_components/onboarding-adventure-page.tsx +++ b/apps/web/src/app/[lang]/onboarding/_components/onboarding-adventure-page.tsx @@ -50,7 +50,6 @@ const exchangeBrandStyles = { 'border border-transparent bg-[#6C3BFF] text-white hover:bg-[#5a30d6]', } as const; const evmAddressPattern = /^0x[a-fA-F0-9]{40}$/; - const normalizeEvmAddress = (value: string | null | undefined) => (value ?? '').replace(/\s+/g, '').trim(); @@ -177,6 +176,15 @@ export function OnboardingAdventurePage() { const [depositDetailsSpaceSlug, setDepositDetailsSpaceSlug] = useState(''); const [copiedAddress, setCopiedAddress] = useState(false); const copyTimeoutRef = useRef(null); + const [hasVisitedAdventure, setHasVisitedAdventure] = useState(false); + const [heroPlaceholderIndex, setHeroPlaceholderIndex] = useState(0); + + const firstName = useMemo(() => { + const rawName = person?.name?.trim(); + if (!rawName) return undefined; + const [candidate] = rawName.split(/\s+/); + return candidate?.trim() || undefined; + }, [person?.name]); const spaceOptions = useMemo( () => @@ -223,6 +231,150 @@ export function OnboardingAdventurePage() { [], ); + useEffect(() => { + const storageKey = 'hypha:onboarding-adventure:visited:v1'; + const justSignedUpKey = 'hypha:onboarding-adventure:just-signed-up:v1'; + try { + const justSignedUp = + window.sessionStorage.getItem(justSignedUpKey) === 'true'; + if (justSignedUp) { + setHasVisitedAdventure(false); + window.sessionStorage.removeItem(justSignedUpKey); + window.localStorage.setItem(storageKey, 'true'); + return; + } + + const alreadyVisited = window.localStorage.getItem(storageKey) === 'true'; + if (alreadyVisited) { + setHasVisitedAdventure(true); + } else { + window.localStorage.setItem(storageKey, 'true'); + } + } catch { + // localStorage unavailable; keep first-visit title fallback + } + }, []); + + useEffect(() => { + aiPromptRef.current = aiPrompt; + }, [aiPrompt]); + + useEffect(() => { + if (rotatingHeroPrompts.length < 2) return; + const intervalId = window.setInterval(() => { + setHeroPlaceholderIndex((current) => + current + 1 >= rotatingHeroPrompts.length ? 0 : current + 1, + ); + }, 2600); + return () => window.clearInterval(intervalId); + }, [rotatingHeroPrompts]); + + const stopDictation = () => { + const recognition = speechRecognitionRef.current; + if (recognition) { + try { + recognition.abort(); + } catch { + try { + recognition.stop(); + } catch { + // ignore stop errors from browser implementation differences + } + } + speechRecognitionRef.current = null; + } + dictationPrefixRef.current = ''; + setIsDictating(false); + }; + + const toggleDictation = () => { + setDictationError(null); + const SR = + (globalThis as unknown as { SpeechRecognition?: SpeechRecognitionCtor }) + .SpeechRecognition ?? + ( + globalThis as unknown as { + webkitSpeechRecognition?: SpeechRecognitionCtor; + } + ).webkitSpeechRecognition; + + if (!SR) { + setDictationError(tHuman('dictationNotSupported')); + return; + } + if (isDictating) { + stopDictation(); + return; + } + + dictationSessionFinalizedRef.current = false; + dictationPrefixRef.current = aiPromptRef.current.trimEnd(); + const recognition = new SR(); + recognition.continuous = true; + recognition.interimResults = true; + recognition.lang = document.documentElement.lang || 'en'; + recognition.onresult = (event) => { + let committed = ''; + let interim = ''; + for (let i = 0; i < event.results.length; i++) { + const result = event.results[i]; + if (!result?.[0]) continue; + if (result.isFinal) { + committed = joinWithSingleSpace( + committed, + result[0].transcript.trim(), + ); + } else { + interim = joinWithSingleSpace(interim, result[0].transcript); + } + } + const dictated = joinWithSingleSpace(committed, interim.trim()); + const nextPrompt = joinWithSingleSpace( + dictationPrefixRef.current, + dictated, + ); + aiPromptRef.current = nextPrompt; + setAiPrompt(nextPrompt); + }; + const finalizeDictation = (showError: boolean) => { + if (dictationSessionFinalizedRef.current) return; + dictationSessionFinalizedRef.current = true; + speechRecognitionRef.current = null; + dictationPrefixRef.current = ''; + setIsDictating(false); + if (showError) { + setDictationError(tHuman('dictationError')); + } + }; + recognition.onerror = (event) => { + finalizeDictation(event.error !== 'aborted'); + }; + recognition.onend = () => { + finalizeDictation(false); + }; + speechRecognitionRef.current = recognition; + try { + recognition.start(); + setIsDictating(true); + } catch { + speechRecognitionRef.current = null; + setDictationError(tHuman('dictationNotSupported')); + } + }; + + useEffect(() => { + return () => { + const recognition = speechRecognitionRef.current; + if (!recognition) return; + try { + recognition.abort(); + } catch { + // ignore + } + speechRecognitionRef.current = null; + }; + }, []); + const handleCopyAddress = (address: string) => { copyToClipboard(address); setCopiedAddress(true); @@ -242,8 +394,11 @@ export function OnboardingAdventurePage() { }; return ( - -
+ +
+

+ {t('aiHero.title')} +