From 3b2c393db38c5432e753b86ac6b5ea75cc0bd9fc Mon Sep 17 00:00:00 2001 From: KT <677465+kevintseng@users.noreply.github.com> Date: Sun, 16 Aug 2026 19:55:33 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(dashboard):=20one=20notice=20at=20a=20?= =?UTF-8?q?time=20=E2=80=94=20the=20banner=20wall=20becomes=20a=20priority?= =?UTF-8?q?=20slot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Doctor, Onboarding and Insights banners could stack three deep above the nav. They now share a notice slot: each banner still decides its own eligibility (an ineligible banner renders no DOM), App orders them by priority — Doctor (broken install) > Onboarding (empty library) > Insights (pending proposals) — and one stylesheet rule shows only the slot's first rendered child, keeping the losers out of the viewport AND the accessibility tree until the winner clears. A dedicated test pins both halves of the mechanism: the DOM priority order (including the next-in-line takeover when the winner is dismissed) and the CSS rule itself, which happy-dom cannot observe at runtime. --- dashboard/src/App.tsx | 15 +++- dashboard/src/styles/global.css | 10 +++ tests/dashboard/notice-slot.test.tsx | 117 +++++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 tests/dashboard/notice-slot.test.tsx diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index 27948fa7..10a22b5a 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -167,9 +167,18 @@ export function App() { return (
- - setTab('Home')} /> - + {/* The notice slot: one banner at a time. Each banner self-decides + eligibility (ineligible = no DOM), and DOM order IS the priority — + Doctor (broken install) > Onboarding (empty library) > Insights + (pending proposals). The stylesheet shows only the slot's first + rendered child; the rest wait in the tree for the winner to clear + (dismissal or the condition resolving). Three banners could + previously stack into a wall above the nav. */} +
+ + + setTab('Home')} /> +
setTab(k as Tab)} /> {/* Each panel is the tabpanel for its TabNav tab: id + role + aria-labelledby wire the roving-tablist relationship (see TabNav). */} diff --git a/dashboard/src/styles/global.css b/dashboard/src/styles/global.css index 80fc6da3..a9040394 100644 --- a/dashboard/src/styles/global.css +++ b/dashboard/src/styles/global.css @@ -155,6 +155,16 @@ body { } .theme-btn:hover { border-color: var(--text-3); } +/* ---- Notice slot ---- */ +/* One notice at a time. The banners inside self-decide eligibility (an + ineligible banner renders no DOM node), so the slot's FIRST rendered + child is the highest-priority applicable notice — App.tsx orders them + Doctor > Onboarding > Insights. display:none keeps the losers out of + the accessibility tree as well as the viewport; they surface the + moment the winner clears. tests/dashboard/notice-slot.test.tsx pins + both this rule and the DOM order. */ +.notice-slot > * ~ * { display: none; } + /* Nav */ .nav { display: flex; diff --git a/tests/dashboard/notice-slot.test.tsx b/tests/dashboard/notice-slot.test.tsx new file mode 100644 index 00000000..9ce686d0 --- /dev/null +++ b/tests/dashboard/notice-slot.test.tsx @@ -0,0 +1,117 @@ +// @vitest-environment happy-dom +// +// The notice slot: at most ONE banner interrupts at a time, by priority +// Doctor > Onboarding > Insights. The mechanism is split across two +// places, so this file pins both halves: +// +// 1. App.tsx renders the three banners inside `.notice-slot` in priority +// order — each banner self-decides eligibility and renders no DOM +// when ineligible, so document order IS the priority order. +// 2. global.css hides every slot child after the first rendered one. +// happy-dom does not compute stylesheet cascade, so the rule is +// pinned at the source: delete or loosen it and this file goes red +// even though every DOM assertion would still pass. +// +// All network is stubbed — nothing here touches ~/.memesh or any config. + +import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest'; +import { render, waitFor } from '@testing-library/preact'; +import { readFileSync } from 'fs'; +import { t } from '../../dashboard/src/lib/i18n'; +import { App } from '../../dashboard/src/App'; + +function jsonResponse(body: unknown): Response { + return new Response(JSON.stringify(body), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); +} + +/** Every banner eligible at once: doctor FAILs, the library is empty, and + * two dream proposals wait. (Insights additionally needs the active tab + * to not be Home — pinned via the stored tab below.) */ +function stubAllBannersEligible() { + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input: RequestInfo | URL) => { + const url = String(input); + if (url.includes('/v1/doctor')) { + return jsonResponse({ + success: true, + data: { + status: 'FAIL', + checks: [{ id: 'db', label: 'Database', status: 'fail', summary: 'db unreadable' }], + }, + }); + } + if (url.includes('/v1/dream/proposals')) { + return jsonResponse({ success: true, data: [{ id: 1, status: 'pending' }, { id: 2, status: 'pending' }] }); + } + if (url.includes('/v1/health')) { + return jsonResponse({ success: true, data: { status: 'ok', version: 't', entity_count: 0 } }); + } + if (url.includes('/v1/config')) { + return jsonResponse({ success: true, data: {} }); + } + return jsonResponse({ success: true, data: [] }); + }); +} + +describe('the notice slot shows one banner at a time, by priority', () => { + beforeEach(() => { + localStorage.clear(); + sessionStorage.clear(); + // Insights self-suppresses on Home (its content lives there); park the + // app on Memories so all three banners are eligible simultaneously. + localStorage.setItem('memesh.tab', 'Memories'); + }); + afterEach(() => { + vi.restoreAllMocks(); + localStorage.clear(); + sessionStorage.clear(); + }); + + it('renders all eligible banners inside the slot in Doctor > Onboarding > Insights order', async () => { + stubAllBannersEligible(); + const { container } = render(); + + const slot = container.querySelector('.notice-slot'); + expect(slot, 'App must render the notice slot').not.toBeNull(); + + await waitFor(() => { + // All three eligible banners have landed in the slot. + expect(slot!.children.length).toBe(3); + }); + + // Document order is the priority order — the stylesheet shows only the + // first child, so getting this order wrong silently changes which + // notice the user sees. + const [first, second, third] = [...slot!.children]; + expect(first.textContent).toContain('db unreadable'); + expect(second.textContent).toContain(t('onboarding.title')); + expect(third.textContent).toContain( + t('banner.pendingInsights', { n: 2, s: 's' }), + ); + }); + + it('the next notice in line takes the slot when the winner is not eligible', async () => { + stubAllBannersEligible(); + // The doctor banner's dismissal signature matches the stubbed failing + // check, so the highest-priority notice is out of the running from the + // first render — Onboarding must be the slot's first child. + localStorage.setItem('memesh.doctorBanner.dismissedSig', 'db:fail::'); + const { container } = render(); + + const slot = container.querySelector('.notice-slot')!; + await waitFor(() => { + expect(slot.children.length).toBe(2); + }); + expect(slot.children[0].textContent).toContain(t('onboarding.title')); + }); + + it('the stylesheet hides every slot child after the first', () => { + const css = readFileSync('dashboard/src/styles/global.css', 'utf8'); + // The one-notice rule: any .notice-slot child with a preceding sibling + // is display:none. Whitespace-tolerant, but the selector and the + // declaration must both survive. + expect(css).toMatch(/\.notice-slot\s*>\s*\*\s*~\s*\*\s*\{\s*display:\s*none;?\s*\}/); + }); +}); From 4a162080aeca53adcfd6fffd8c1c8aefef12c69a Mon Sep 17 00:00:00 2001 From: KT <677465+kevintseng@users.noreply.github.com> Date: Sun, 16 Aug 2026 19:56:28 +0800 Subject: [PATCH 2/2] docs(changelog): the notice slot; regenerate the packaged bundle --- CHANGELOG.md | 5 +++++ dashboard/dist/index.html | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 521ed82c..5f4c84ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,11 @@ All notable changes to MeMesh are documented here. its own **Project** tab. Old `?tab=` deep links and stored tab choices migrate to the surface that absorbed them, so a bookmarked `?tab=Browse` opens Memories instead of nothing. +- **One notice at a time.** The Doctor, Onboarding and Insights banners + could stack three deep above the nav. They now share a priority slot — + Doctor (broken install) > Onboarding (empty library) > Insights + (pending proposals) — showing exactly one; the next in line surfaces + the moment the winner is dismissed or its condition clears. ## [4.6.0] — 2026-08-16 diff --git a/dashboard/dist/index.html b/dashboard/dist/index.html index 1684cbb5..93219d80 100644 --- a/dashboard/dist/index.html +++ b/dashboard/dist/index.html @@ -26,8 +26,8 @@ ${v(F)}`}catch{}}const P=n(co[i]),B=`https://github.com/PCIRCLE-AI/memesh/issues/new?title=${encodeURIComponent(`[${P}] `)}&body=${encodeURIComponent(S)}&labels=${encodeURIComponent(f)}`;window.open(B,"_blank"),u(""),o(!1)}finally{g(!1)}}};return e(Z,{children:[e("button",{ref:b,class:"fb-btn",onClick:()=>o(!s),"aria-haspopup":"dialog","aria-expanded":s,"aria-controls":"fb-panel",children:n("feedback.button")}),s&&e("div",{class:"fb-panel",ref:l,role:"dialog",id:"fb-panel","aria-labelledby":"fb-title",children:[e("h3",{class:"fb-title",id:"fb-title",children:n("feedback.title")}),e("div",{class:"fb-types",children:Yr.map(f=>e("label",{class:`fb-type ${i===f?"selected":""}`,children:[e("input",{type:"radio",name:"fb-type",value:f,checked:i===f,onChange:()=>r(f)}),n(co[f])]},f))}),e("textarea",{ref:y,class:"fb-desc","aria-label":n("feedback.descLabel"),placeholder:n("feedback.placeholder"),value:a,onInput:f=>u(f.target.value)}),e("label",{class:"fb-sys-row",children:[e("input",{type:"checkbox",checked:m,onChange:()=>c(!m)}),n("feedback.includeSys")]}),e("button",{class:"btn btn-primary fb-submit",onClick:w,disabled:d,children:n(d?"feedback.submitting":"feedback.submit")})]})]})}function Jr({currentToken:t,onSubmit:s,rejected:o=!1}){const[i,r]=M(t??""),[a,u]=M(!1);function m(l){l.preventDefault(),u(!0),i.trim()&&s(i.trim())}const c=a&&!i.trim(),d=o&&!c,g=c||d?"auth-prompt-error":void 0;return e("div",{class:"auth-prompt-shell","data-testid":"auth-prompt",children:e("form",{class:"auth-prompt-card",onSubmit:m,children:[e("h1",{children:n("auth.title")}),e("p",{children:n("auth.intro")}),e("label",{children:[e("span",{children:n("auth.tokenLabel")}),e("input",{type:"password",autocomplete:"off",value:i,onInput:l=>r(l.target.value),placeholder:n("auth.tokenPlaceholder"),"aria-invalid":g!==void 0,"aria-describedby":g,autofocus:!0})]}),c&&e("p",{class:"auth-prompt-error",id:g,role:"alert",children:n("auth.empty")}),d&&e("p",{class:"auth-prompt-error",id:g,role:"alert",children:n("auth.invalid")}),e("button",{type:"submit",class:"auth-prompt-submit",children:n("auth.submit")})]})})}const mo="memesh.onboardingDismissed";function Qr({health:t}){const[s,o]=M(()=>{try{return localStorage.getItem(mo)==="true"}catch{return!1}}),[i,r]=M(null),[a,u]=M("");if(ee(()=>{t&&t.entity_count>0&&o(!0)},[t?.entity_count]),!t||t.entity_count>0||s)return null;function m(){o(!0);try{localStorage.setItem(mo,"true")}catch{}}async function c(){u(""),r("seed");try{await Y("POST","/v1/demo/seed"),window.dispatchEvent(new Event("memesh:data-changed"))}catch(g){u(Se(g))}finally{r(null)}}async function d(){if(confirm(n("onboarding.resetConfirm"))){u(""),r("reset");try{await Y("POST","/v1/demo/reset"),window.dispatchEvent(new Event("memesh:data-changed"))}catch(g){u(Se(g))}finally{r(null)}}}return e("div",{role:"region","aria-label":n("onboarding.title"),style:{position:"relative",margin:"12px auto 8px",maxWidth:920,padding:"14px 18px",border:"1px solid rgba(143, 242, 92, 0.28)",borderRadius:"var(--radius)",background:"var(--life-soft)",color:"var(--text-1)"},children:[e("button",{type:"button",onClick:m,"aria-label":n("onboarding.dismiss"),style:{position:"absolute",top:8,right:10,background:"transparent",border:"none",color:"var(--text-3)",fontSize:18,lineHeight:1,cursor:"pointer",padding:4},children:"×"}),e("div",{style:{fontSize:14,fontWeight:600,color:"var(--text-0)",marginBottom:6},children:n("onboarding.title")}),e("div",{style:{fontSize:13,lineHeight:1.55,color:"var(--text-1)"},children:n("onboarding.body")}),e("div",{style:{fontSize:11,lineHeight:1.55,color:"var(--text-3)",marginTop:4},children:n("onboarding.llmHint")}),e("div",{style:{display:"flex",flexWrap:"wrap",gap:10,marginTop:12,alignItems:"center"},children:[e("button",{type:"button",class:"btn btn-primary",onClick:c,disabled:i!==null,style:{minWidth:160},children:n(i==="seed"?"onboarding.seedingButton":"onboarding.seedButton")}),e("span",{style:{fontSize:12,color:"var(--text-3)"},children:n("onboarding.seedHint")})]}),e("details",{style:{marginTop:10,fontSize:12,color:"var(--text-3)"},children:[e("summary",{style:{cursor:"pointer"},children:n("onboarding.cliReference")}),e("div",{style:{display:"flex",flexWrap:"wrap",gap:12,marginTop:8},children:[e("code",{style:{padding:"6px 10px",background:"rgba(0, 0, 0, 0.3)",border:"1px solid rgba(143, 242, 92, 0.20)",borderRadius:"var(--radius-xs)",color:"var(--life)",fontFamily:"var(--mono)",fontSize:12},children:"memesh demo"}),e("span",{style:{fontSize:12,color:"var(--text-3)"},children:n("onboarding.hintDemo")})]}),e("div",{style:{display:"flex",flexWrap:"wrap",gap:12,marginTop:8,alignItems:"center"},children:[e("code",{style:{padding:"6px 10px",background:"rgba(0, 0, 0, 0.3)",border:"1px solid var(--border-subtle)",borderRadius:"var(--radius-xs)",color:"var(--text-2)",fontFamily:"var(--mono)",fontSize:12},children:"memesh demo --reset --yes"}),e("button",{type:"button",class:"btn",onClick:d,disabled:i!==null,style:{fontSize:11,padding:"4px 10px"},children:n(i==="reset"?"onboarding.resettingButton":"onboarding.resetButton")}),e("span",{style:{fontSize:12,color:"var(--text-3)"},children:n("onboarding.hintReset")})]})]}),a&&e("div",{role:"alert",style:{marginTop:10,fontSize:12,color:"var(--danger)"},children:a})]})}const uo="memesh.doctorBanner.dismissedSig";function bs(t,s,o){const i=n(t,o);return i===t?s:i}function Zr(t){return t.code?bs(`doctor.msg.${t.code}.summary`,t.summary,t.params):t.summary}function ea(t){if(t.fix)return t.code?bs(`doctor.msg.${t.code}.fix`,t.fix,t.params):t.fix}function ta(t){return bs(`doctor.label.${t.id}`,t.label)}const sa=new Set(["update-status.no-cache","update-status.stale","update-status.deprecation-unknown","hook-activity.not-wired","shell-cli.not-on-path","skills-manifest.missing-dev","install-channel.unknown","http-probe.no-server","readme-parity.unreadable","readme-parity.drift"]);function oa(t){if(t.status==="fail")return!0;if(t.status!=="warn"||t.code&&sa.has(t.code)||!t.fix)return!1;const s=t.fix.trim().toLowerCase();return!(!s||s==="no action needed"||s.startsWith("no action"))}function na(){const[t,s]=M(null),[o,i]=M(()=>{try{return localStorage.getItem(uo)??""}catch{return""}});if(ee(()=>{let b=!0;const y=()=>{Y("GET","/v1/doctor").then(v=>{b&&s(Array.isArray(v?.checks)?v:null)}).catch(()=>{})};y();const h=()=>y();return window.addEventListener("memesh:data-changed",h),()=>{b=!1,window.removeEventListener("memesh:data-changed",h)}},[]),!t||t.status==="PASS")return null;const r=t.checks.filter(oa);if(r.length===0)return null;const a=r.map(b=>`${b.id}:${b.status}:${b.code??""}:${b.params?.hook??""}`).sort().join("|");if(a===o)return null;function u(){i(a);try{localStorage.setItem(uo,a)}catch{}}function m(){const b=r.map(w=>{const f=w.status==="fail"?"❌":"⚠️",S=w.fix?` _Fix: ${w.fix}_`:"";return`- ${f} **${w.label}**: ${w.summary}${S}`}),y=`${n("doctorBanner.preambleForIssue")} ${b.join(` -`)}`,v=`https://github.com/PCIRCLE-AI/memesh/issues/new?title=${encodeURIComponent("[Bug] memesh doctor reported issues")}&body=${encodeURIComponent(y)}&labels=${encodeURIComponent("feedback,from-dashboard,bug,doctor-warning")}`;window.open(v,"_blank")}const c=t.status==="FAIL",d=c?"var(--danger)":"var(--warning)",g=c?"var(--danger-soft)":"var(--warning-soft)";return e("div",{role:"alert",style:{position:"relative",margin:"12px auto 0",maxWidth:920,padding:"12px 16px",border:`1px solid ${d}`,borderRadius:"var(--radius)",background:g,color:"var(--text-1)"},children:[e("button",{type:"button",onClick:u,"aria-label":n("doctorBanner.dismiss"),style:{position:"absolute",top:6,right:8,background:"transparent",border:"none",color:"var(--text-3)",fontSize:18,lineHeight:1,cursor:"pointer",padding:4},children:"×"}),e("div",{style:{fontSize:13,fontWeight:600,color:d,marginBottom:6},children:n(c?"doctorBanner.failTitle":"doctorBanner.warnTitleSoft")}),e("ul",{style:{margin:"6px 0 10px",paddingLeft:18,fontSize:12,lineHeight:1.5,color:"var(--text-2)"},children:[r.slice(0,3).map(b=>{const y=ea(b);return e("li",{children:[e("strong",{children:[ta(b),":"]})," ",Zr(b),y&&e(Z,{children:[" — ",e("em",{style:{color:"var(--text-3)"},children:y})]})]},b.id)}),r.length>3&&e("li",{style:{color:"var(--text-3)"},children:n("doctorBanner.moreCount",{n:r.length-3})})]}),c&&e("div",{style:{display:"flex",gap:8,alignItems:"center",flexWrap:"wrap"},children:[e("button",{type:"button",class:"btn",onClick:m,style:{fontSize:12,padding:"4px 12px"},children:n("doctorBanner.getHelp")}),e("span",{style:{fontSize:11,color:"var(--text-3)"},children:n("doctorBanner.helpHint")})]})]})}const ho="memesh.insightsBanner.dismissed";function ra({currentTab:t,onNavigateToInsights:s}){const[o,i]=M(0),[r,a]=M(()=>{try{return sessionStorage.getItem(ho)==="true"}catch{return!1}});if(ee(()=>{let c=!0;const d=()=>{Y("GET","/v1/dream/proposals?status=pending").then(l=>{if(!c)return;const b=Array.isArray(l)?l:l?.data??[];i(b.length)}).catch(()=>{})};d();const g=()=>d();return window.addEventListener("memesh:data-changed",g),()=>{c=!1,window.removeEventListener("memesh:data-changed",g)}},[]),t==="Home"||r||o===0)return null;function u(c){c.stopPropagation(),a(!0);try{sessionStorage.setItem(ho,"true")}catch{}}const m=n("banner.pendingInsights",{n:o,s:o===1?"":"s"});return e("div",{role:"button","aria-label":n("banner.viewAll"),onClick:s,onKeyDown:c=>{(c.key==="Enter"||c.key===" ")&&(c.preventDefault(),s())},tabIndex:0,style:{position:"relative",margin:"12px auto 0",maxWidth:920,padding:"10px 40px 10px 16px",border:"1px solid rgba(143, 242, 92, 0.32)",borderRadius:"var(--radius)",background:"var(--life-soft)",color:"var(--text-1)",cursor:"pointer",fontSize:13,lineHeight:1.5,display:"flex",alignItems:"center",justifyContent:"space-between",gap:8},children:[e("span",{style:{flex:1},children:[e("span",{style:{marginRight:6},"aria-hidden":"true",children:"💡"}),m]}),e("button",{type:"button",onClick:u,"aria-label":n("banner.dismiss"),style:{position:"absolute",top:6,right:8,background:"transparent",border:"none",color:"var(--text-3)",fontSize:18,lineHeight:1,cursor:"pointer",padding:4},children:"×"})]})}const Oo=["Home","Memories","Project","Graph","Settings"],aa={Home:"tab.home",Memories:"tab.memories",Project:"tab.project",Graph:"tab.graph",Settings:"tab.settings"},ia={Insights:"Home",Analytics:"Home",Search:"Memories",Browse:"Memories",Manage:"Memories",Lessons:"Memories"},Bo="memesh.tab";function la(){const t=s=>s?Oo.includes(s)?s:ia[s]??null:null;try{const s=new URLSearchParams(window.location.search),o=t(s.get("tab"));if(o)return o;const i=t(localStorage.getItem(Bo));if(i)return i}catch{}return"Home"}function ca(){const[t,s]=M(()=>mn()),[o,i]=M(la),[r,a]=M(()=>new Set);ee(()=>{a(f=>f.has(o)?f:new Set(f).add(o))},[o]);const u=f=>r.has(f)||o===f;ee(()=>{try{localStorage.setItem(Bo,o)}catch{}},[o]);const[m,c]=M(null),[d,g]=M(""),[l,b]=M(!1);ee(()=>{const f=()=>b(!0);return window.addEventListener("memesh:auth-required",f),()=>window.removeEventListener("memesh:auth-required",f)},[]);const[y,h]=M(!1),v=Ce(()=>{Y("GET","/v1/health").then(f=>{c(f),g(""),b(!1)}).catch(f=>{if(f instanceof fs){h(is()!==null),b(!0),g("");return}g(f.message)})},[]);ee(()=>{v();const f=()=>v();return window.addEventListener("memesh:data-changed",f),()=>window.removeEventListener("memesh:data-changed",f)},[v]);const w=Oo.map(f=>({key:f,label:n(aa[f])}));return l?e(Jr,{currentToken:is(),rejected:y,onSubmit:f=>{vn(f),h(!1),b(!1),v()}}):e("div",{class:"shell",children:[e(pn,{health:m,error:d}),e(na,{}),e(ra,{currentTab:o,onNavigateToInsights:()=>i("Home")}),e(Qr,{health:m}),e(fn,{tabs:w,active:o,onSelect:f=>i(f)}),e("div",{class:"main",children:[e("div",{id:"panel-Home",role:"tabpanel","aria-labelledby":"tab-Home",class:`panel ${o==="Home"?"active":""}`,children:o==="Home"&&e(er,{})}),e("div",{id:"panel-Memories",role:"tabpanel","aria-labelledby":"tab-Memories",class:`panel ${o==="Memories"?"active":""}`,children:u("Memories")&&e(vr,{health:m})}),e("div",{id:"panel-Project",role:"tabpanel","aria-labelledby":"tab-Project",class:`panel ${o==="Project"?"active":""}`,children:u("Project")&&e(Lr,{health:m})}),e("div",{id:"panel-Graph",role:"tabpanel","aria-labelledby":"tab-Graph",class:`panel ${o==="Graph"?"active":""}`,children:o==="Graph"&&e(Gr,{})}),e("div",{id:"panel-Settings",role:"tabpanel","aria-labelledby":"tab-Settings",class:`panel ${o==="Settings"?"active":""}`,children:o==="Settings"&&e(Dr,{locale:t,onLocaleChange:s})})]}),e(Xr,{health:m})]})}on(e(ca,{}),document.getElementById("app")); - +`)}`,v=`https://github.com/PCIRCLE-AI/memesh/issues/new?title=${encodeURIComponent("[Bug] memesh doctor reported issues")}&body=${encodeURIComponent(y)}&labels=${encodeURIComponent("feedback,from-dashboard,bug,doctor-warning")}`;window.open(v,"_blank")}const c=t.status==="FAIL",d=c?"var(--danger)":"var(--warning)",g=c?"var(--danger-soft)":"var(--warning-soft)";return e("div",{role:"alert",style:{position:"relative",margin:"12px auto 0",maxWidth:920,padding:"12px 16px",border:`1px solid ${d}`,borderRadius:"var(--radius)",background:g,color:"var(--text-1)"},children:[e("button",{type:"button",onClick:u,"aria-label":n("doctorBanner.dismiss"),style:{position:"absolute",top:6,right:8,background:"transparent",border:"none",color:"var(--text-3)",fontSize:18,lineHeight:1,cursor:"pointer",padding:4},children:"×"}),e("div",{style:{fontSize:13,fontWeight:600,color:d,marginBottom:6},children:n(c?"doctorBanner.failTitle":"doctorBanner.warnTitleSoft")}),e("ul",{style:{margin:"6px 0 10px",paddingLeft:18,fontSize:12,lineHeight:1.5,color:"var(--text-2)"},children:[r.slice(0,3).map(b=>{const y=ea(b);return e("li",{children:[e("strong",{children:[ta(b),":"]})," ",Zr(b),y&&e(Z,{children:[" — ",e("em",{style:{color:"var(--text-3)"},children:y})]})]},b.id)}),r.length>3&&e("li",{style:{color:"var(--text-3)"},children:n("doctorBanner.moreCount",{n:r.length-3})})]}),c&&e("div",{style:{display:"flex",gap:8,alignItems:"center",flexWrap:"wrap"},children:[e("button",{type:"button",class:"btn",onClick:m,style:{fontSize:12,padding:"4px 12px"},children:n("doctorBanner.getHelp")}),e("span",{style:{fontSize:11,color:"var(--text-3)"},children:n("doctorBanner.helpHint")})]})]})}const ho="memesh.insightsBanner.dismissed";function ra({currentTab:t,onNavigateToInsights:s}){const[o,i]=M(0),[r,a]=M(()=>{try{return sessionStorage.getItem(ho)==="true"}catch{return!1}});if(ee(()=>{let c=!0;const d=()=>{Y("GET","/v1/dream/proposals?status=pending").then(l=>{if(!c)return;const b=Array.isArray(l)?l:l?.data??[];i(b.length)}).catch(()=>{})};d();const g=()=>d();return window.addEventListener("memesh:data-changed",g),()=>{c=!1,window.removeEventListener("memesh:data-changed",g)}},[]),t==="Home"||r||o===0)return null;function u(c){c.stopPropagation(),a(!0);try{sessionStorage.setItem(ho,"true")}catch{}}const m=n("banner.pendingInsights",{n:o,s:o===1?"":"s"});return e("div",{role:"button","aria-label":n("banner.viewAll"),onClick:s,onKeyDown:c=>{(c.key==="Enter"||c.key===" ")&&(c.preventDefault(),s())},tabIndex:0,style:{position:"relative",margin:"12px auto 0",maxWidth:920,padding:"10px 40px 10px 16px",border:"1px solid rgba(143, 242, 92, 0.32)",borderRadius:"var(--radius)",background:"var(--life-soft)",color:"var(--text-1)",cursor:"pointer",fontSize:13,lineHeight:1.5,display:"flex",alignItems:"center",justifyContent:"space-between",gap:8},children:[e("span",{style:{flex:1},children:[e("span",{style:{marginRight:6},"aria-hidden":"true",children:"💡"}),m]}),e("button",{type:"button",onClick:u,"aria-label":n("banner.dismiss"),style:{position:"absolute",top:6,right:8,background:"transparent",border:"none",color:"var(--text-3)",fontSize:18,lineHeight:1,cursor:"pointer",padding:4},children:"×"})]})}const Oo=["Home","Memories","Project","Graph","Settings"],aa={Home:"tab.home",Memories:"tab.memories",Project:"tab.project",Graph:"tab.graph",Settings:"tab.settings"},ia={Insights:"Home",Analytics:"Home",Search:"Memories",Browse:"Memories",Manage:"Memories",Lessons:"Memories"},Bo="memesh.tab";function la(){const t=s=>s?Oo.includes(s)?s:ia[s]??null:null;try{const s=new URLSearchParams(window.location.search),o=t(s.get("tab"));if(o)return o;const i=t(localStorage.getItem(Bo));if(i)return i}catch{}return"Home"}function ca(){const[t,s]=M(()=>mn()),[o,i]=M(la),[r,a]=M(()=>new Set);ee(()=>{a(f=>f.has(o)?f:new Set(f).add(o))},[o]);const u=f=>r.has(f)||o===f;ee(()=>{try{localStorage.setItem(Bo,o)}catch{}},[o]);const[m,c]=M(null),[d,g]=M(""),[l,b]=M(!1);ee(()=>{const f=()=>b(!0);return window.addEventListener("memesh:auth-required",f),()=>window.removeEventListener("memesh:auth-required",f)},[]);const[y,h]=M(!1),v=Ce(()=>{Y("GET","/v1/health").then(f=>{c(f),g(""),b(!1)}).catch(f=>{if(f instanceof fs){h(is()!==null),b(!0),g("");return}g(f.message)})},[]);ee(()=>{v();const f=()=>v();return window.addEventListener("memesh:data-changed",f),()=>window.removeEventListener("memesh:data-changed",f)},[v]);const w=Oo.map(f=>({key:f,label:n(aa[f])}));return l?e(Jr,{currentToken:is(),rejected:y,onSubmit:f=>{vn(f),h(!1),b(!1),v()}}):e("div",{class:"shell",children:[e(pn,{health:m,error:d}),e("div",{class:"notice-slot",children:[e(na,{}),e(Qr,{health:m}),e(ra,{currentTab:o,onNavigateToInsights:()=>i("Home")})]}),e(fn,{tabs:w,active:o,onSelect:f=>i(f)}),e("div",{class:"main",children:[e("div",{id:"panel-Home",role:"tabpanel","aria-labelledby":"tab-Home",class:`panel ${o==="Home"?"active":""}`,children:o==="Home"&&e(er,{})}),e("div",{id:"panel-Memories",role:"tabpanel","aria-labelledby":"tab-Memories",class:`panel ${o==="Memories"?"active":""}`,children:u("Memories")&&e(vr,{health:m})}),e("div",{id:"panel-Project",role:"tabpanel","aria-labelledby":"tab-Project",class:`panel ${o==="Project"?"active":""}`,children:u("Project")&&e(Lr,{health:m})}),e("div",{id:"panel-Graph",role:"tabpanel","aria-labelledby":"tab-Graph",class:`panel ${o==="Graph"?"active":""}`,children:o==="Graph"&&e(Gr,{})}),e("div",{id:"panel-Settings",role:"tabpanel","aria-labelledby":"tab-Settings",class:`panel ${o==="Settings"?"active":""}`,children:o==="Settings"&&e(Dr,{locale:t,onLocaleChange:s})})]}),e(Xr,{health:m})]})}on(e(ca,{}),document.getElementById("app")); +