From d9a5014c7cc4e00f81dc1aa5f80a33e624d12235 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 22 Apr 2026 14:04:40 +0200 Subject: [PATCH 1/3] chore(openspec): add-features-roadmap-menu change proposal Proposes a cross-repo Features & Roadmap menu item mounted in every Conduction app's NcAppNavigationSettings slot above the gear. - Features tab: shipped capabilities from openspec/specs/, extracted at build time into docs/features.json (committed, also powers Docusaurus public features page) - Roadmap tab: open GitHub issues from the app's own repo, sorted by reactions, with pipeline-label blocklist and full markdown body rendering - Suggest-feature modal launched from the route header and from any widget/page declaring a specRef via its NcActions menu - Extends existing GitHubHandler with listIssues() + createIssue(); user PAT preferred, server PAT fallback with attribution prefix - Out of scope: ADR-019 fleet rollout, Discussions, Accept->specter wiring Capabilities: - github-issue-proxy - features-roadmap-menu Validates strict. --- .../add-features-roadmap-menu/.openspec.yaml | 2 + .../add-features-roadmap-menu/design.md | 368 +++++++++++++++ .../add-features-roadmap-menu/proposal.md | 132 ++++++ .../specs/features-roadmap-menu/spec.md | 428 ++++++++++++++++++ .../specs/github-issue-proxy/spec.md | 327 +++++++++++++ .../add-features-roadmap-menu/tasks.md | 86 ++++ 6 files changed, 1343 insertions(+) create mode 100644 openspec/changes/add-features-roadmap-menu/.openspec.yaml create mode 100644 openspec/changes/add-features-roadmap-menu/design.md create mode 100644 openspec/changes/add-features-roadmap-menu/proposal.md create mode 100644 openspec/changes/add-features-roadmap-menu/specs/features-roadmap-menu/spec.md create mode 100644 openspec/changes/add-features-roadmap-menu/specs/github-issue-proxy/spec.md create mode 100644 openspec/changes/add-features-roadmap-menu/tasks.md diff --git a/openspec/changes/add-features-roadmap-menu/.openspec.yaml b/openspec/changes/add-features-roadmap-menu/.openspec.yaml new file mode 100644 index 0000000000..25345f4206 --- /dev/null +++ b/openspec/changes/add-features-roadmap-menu/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-22 diff --git a/openspec/changes/add-features-roadmap-menu/design.md b/openspec/changes/add-features-roadmap-menu/design.md new file mode 100644 index 0000000000..bff57a43d5 --- /dev/null +++ b/openspec/changes/add-features-roadmap-menu/design.md @@ -0,0 +1,368 @@ +# Design: Add Features & Roadmap Menu + +## Context + +Every Conduction Nextcloud app ships with two sources of truth that today are invisible to +users: + +1. **OpenSpec specs** under `openspec/specs/*/spec.md` describe the capabilities that are + actually built. Each spec has YAML frontmatter (`status: implemented | reviewed | redirect`), + a single `# Title` H1 that matches the capability directory, and a `## Purpose` section with + a short business summary. Today this tree is only read by developers and by the OpenSpec CLI. +2. **GitHub issues** in each app's repository capture the roadmap: bugs, enhancement requests, + epics. Reactions (`+1`) already act as an informal voting signal from users who discover the + issue tracker. + +The goal is to expose both of these inside the app itself — one menu entry, a dedicated route +with two tabs — and to add a submission path that lets users file feature requests straight +into the repo without leaving the product. + +**Scope pivot:** the first iteration of this design deferred user submissions as a follow-up. +The user reopened that decision because the submission path IS the critical value proposition +("the most important part"). Submission is therefore back in scope for this change; the +design below reflects that reopening. + +OpenRegister is the framework dependency shared by every Conduction Nextcloud app, so that is +where the backend (read proxy + submission endpoint) lives. `@conduction/nextcloud-vue` is +the shared Vue component library (currently exporting only settings primitives), so that is +where the UI component family lives. The build-time spec-to-manifest tool is introduced as a +third package, `@conduction/openspec-manifest`, because it must be usable from any app's +build without dragging in the full UI library. A fourth package, +`@conduction/docusaurus-features`, renders the same manifest on each app's public +Docusaurus site. + +## Goals + +- One menu entry → one dedicated route with two tabs, every Conduction app. Consistent + placement above the Settings gear in `NcAppNavigationSettings`. +- Features surfaced from OpenSpec specs at build time (no runtime filesystem walk, no backend + read path for feature metadata). Same manifest powers the in-app view and the Docusaurus + public page. +- Roadmap items fetched live from GitHub with a short cache, full markdown rendering (XSS- + sanitized), and a blocklist that hides pipeline/workflow labels. +- Submission directly to GitHub Issues, preferring the user's own token for authorship and + falling back to the app-level token with an authorship prefix when the user has none. +- Widgets and pages may declaratively opt into a `specRef` — reusing the ADR-008 capability- + slug convention — so the "Suggest feature" action appears contextually wherever the user + is working. +- Reuse the existing `GitHubHandler` + both PAT stores; reuse the existing admin UI for PAT + configuration; no new secret storage. +- Ship the pilot wiring on OpenRegister itself in this change. + +## Non-Goals + +- GitHub Discussions integration — user explicitly chose Issues. +- "Accept feature → auto-generate spec" pipeline (future: specter integration). +- Fleet-wide adoption across every Conduction app (future: ADR-019 in hydra). +- Editing or moderating issues from inside the app (create + read only; no close, no comment). +- Private / enterprise repos in this iteration. Public repos only. +- Webpack plugin wrapper for the manifest generator (CLI only in this iteration). + +## Decisions + +### D1. Runtime storage: none (thin proxy) + +We do NOT persist GitHub issues into an OpenRegister register, nor do we persist a per-instance +copy of features. Alternatives considered: + +- **Per-instance register of features and issues, synced nightly.** Rejected: creates drift, + requires a sync worker, duplicates data we already have at source, and was explicitly + rejected by the user. +- **In-app static JSON only for features, no roadmap at all.** Rejected: roadmap is the main + engagement driver and is cheap if proxied with caching. +- **Chosen:** thin cached proxy for roadmap, build-time JSON bundle for features, direct + proxy for submissions. + +Rationale: minimizes blast radius, zero new data model, survives backend outages for features +(since they're bundled into JS), and single-source-of-truth for both. + +### D2. GitHub read auth: reuse `openregister::github_api_token` (IAppConfig) + +Alternatives: + +- Per-user PAT only. Rejected for read: the roadmap is public data; burdening every user with + configuring a PAT defeats the point of "works out of the box." +- Anonymous read (no auth). Rejected: 60 req/hour per IP is not enough for a component that + polls on panel open; will rate-limit quickly across a fleet of apps. +- **Chosen:** reuse the admin-configured app-level PAT for reads. Admin configures once; all + users benefit. + +### D3. Features source: build-time manifest from `openspec/specs/` + +- Runtime endpoint reading specs from the installed app's filesystem. Rejected: couples the + backend to the app's deployment layout. +- Hand-authored JSON curated per release. Rejected: guaranteed to drift. +- **Chosen:** `@conduction/openspec-manifest` CLI runs as a `prebuild` step, writes + `docs/features.json`, both webpack/vite and Docusaurus consume that same file. + +### D4. Feature filter: `status: implemented` OR `status: reviewed` + +- `implemented` only. Rejected: `reviewed` is strictly more finished, not less. +- Include `redirect`. Rejected: redirects are capability re-homing markers, not features. +- Include drafts / planned. Rejected: the Features tab must show what actually ships. +- **Chosen:** `status ∈ {implemented, reviewed}`. + +### D5. Docs link default + +- Require every spec to add a `docsUrl:` frontmatter key. Rejected: high-friction migration. +- No link at all. Rejected: users who want detail should be able to get it. +- **Chosen:** auto-compute against the repo's default branch resolved from `origin/HEAD`, + never the currently checked-out branch. Specs can opt in to an override via a `docsUrl:` + frontmatter key. + +### D6. Manifest generator as a standalone npm package + +- Ship as a dev-dependency module inside `@conduction/nextcloud-vue`. Rejected: couples build + tooling to the UI library; every app consumes the UI lib but only some apps have OpenSpec. +- Inline script per app, copy-pasted. Rejected: DRY violation across ten-plus apps. +- **Chosen:** new `@conduction/openspec-manifest` package with a `build` CLI. + +### D7. Component home: `@conduction/nextcloud-vue` + +The `@conduction/nextcloud-vue` library currently exports `CnSettingsCard` and +`CnSettingsSection`. The new `CnFeaturesAndRoadmapLink`, `FeaturesAndRoadmapView`, +`SuggestFeatureModal`, and `useSpecRef` additions set the convention for a new component +category: "cross-app surfaces." + +### D8. i18n + +The component ships with Dutch (nl) and English (en) strings for all chrome (route title, +tab labels, modal fields, empty states, toasts, error hints). Feature titles and summaries +pass through unchanged — specs are single-source. + +### D9. Cache TTL: 15 minutes in-memory, global (per-instance) + +- Per-user cache. Rejected: same data for everyone; no personalization. +- Longer TTL (1 hour). Rejected: roadmap engagement benefits from fresh-feeling data. +- Shorter TTL (1 min). Rejected: wastes rate-limit budget. +- **Chosen:** 15 min in-memory cache, keyed by `(repo, state, sort, per_page)`, global per + instance. + +### D10. Graceful degradation when PAT is missing + +The read endpoint returns `200 { items: [], hint: "github_pat_not_configured" }` rather than +`403` or `500`. Features tab is unaffected. + +### D11. Pilot scope: OpenRegister only + +- Ship adoption into every Conduction app in this single change. Rejected: huge blast + radius for a first-time shared component. +- **Chosen:** wire only OpenRegister in this change. Fleet rollout is a separate hydra + ADR-019. + +### D12. UI pattern: full route, not side panel + +- Side panel (`NcAppSidebar`). Considered; would be less intrusive but provides a cramped + surface for browsing + submitting feature requests, and awkward on mobile. +- Modal. Rejected: too restrictive for two-tab browsing. +- **Chosen:** dedicated Vue route (e.g. `/features-roadmap`). The + `NcAppNavigationSettings` entry is an `` that navigates to this + route. Provides a richer surface for browsing Features, the Roadmap, and filing new + requests from one page. + +### D13. Submission destination: GitHub Issues (not Discussions) + +- Discussions. Rejected: harder to link to the roadmap tab, which reads Issues; user + explicitly preferred Issues. +- **Chosen:** Issues. Direct roadmap visibility; the new submission shows up immediately in + the same Roadmap tab where it was filed. + +### D14. Authorship fallback: user-PAT preferred, server-PAT fallback with attribution + +- PAT-required (every submitter must configure their own PAT). Rejected: heavy UX friction + for a feature whose point is low-friction. +- Server-PAT only. Rejected: every issue would appear to be filed by the bot account; + attribution lost. +- **Chosen:** prefer `openregister::github_token` (IConfig per-user); fall back to + `openregister::github_api_token` (IAppConfig) when the user has no token; on fallback, + prefix the issue body with + `> Submitted by **** via \n\n---\n\n` so + traceability survives. + +### D15. Markdown rendering: `marked` + `DOMPurify` + +- Plain-text body rendering (no markdown). Rejected: GitHub issue bodies are commonly + markdown; plain rendering looks broken (raw `*`, `#`, code fences). +- Iframe-embed GitHub's render. Rejected: XSS-free but no style control, slow, and breaks + dark mode theming. +- First-paragraph-only + "Read more on GitHub." Rejected by the user in favor of full + markdown. +- **Chosen:** `marked` to parse GitHub-flavored markdown, `DOMPurify` with a strict + allowlist to sanitize (no `"` +- **THEN** the rendered DOM SHALL NOT contain any `