update #1

Merged
crowetic merged 42 commits from :update into main 2025-09-10 18:50:55 +00:00
292 changed files with 27662 additions and 12569 deletions
+10
View File
@@ -0,0 +1,10 @@
# .editorconfig — Q-Blog
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
+15
View File
@@ -0,0 +1,15 @@
# Phase 0: limit lint to harness/docs/scripts; app code will be addressed in Phase 13 sweeps.
node_modules/
dist/
coverage/
.vite/
.gitea/
# Ignore full app sources during Phase 0
src/**
# Type definitions are not linted
**/*.d.ts
# Build artifacts & caches
*.log
+11
View File
@@ -0,0 +1,11 @@
# Copy this file to .gitea.env and fill real values.
# Keep .gitea.env untracked (it is listed in .gitignore).
GITEA_BASE_URL=https://gitea.qortal.link
# Personal access token with releases:write permission
GITEA_TOKEN=
# Repository owner and name
OWNER=
REPO=
+39
View File
@@ -0,0 +1,39 @@
---
name: Bug report
about: Something is broken or wrong
labels: fix
---
## Summary
What happened vs expected?
## Environment
Browser/OS, screen reader if relevant
## Steps to Reproduce
1.
2.
3.
## Expected vs Actual
- Expected:
- Actual:
## Screenshots/Logs
(attach if possible, redact sensitive info)
## Impact
P0 | P1 | P2
## Checks
- [ ] Keyboard path verified (if UI)
- [ ] A11y names/labels/states present
- [ ] Error includes recovery path
- [ ] Repro is deterministic
+29
View File
@@ -0,0 +1,29 @@
---
name: Feature request
about: Propose a capability or improvement
labels: feat
---
## Problem / Outcome
What user problem are we solving?
## Proposal
Short description, acceptance criteria, and non-goals.
## A11y & Quality
- Keyboard/focus implications
- Labels/roles/states
- Tests (unit/component) needed
## Dependencies
Related issues/ADRs
## Definition of Done
- [ ] Acceptance criteria met
- [ ] Tests/docs updated
- [ ] Quality gates green
+26
View File
@@ -0,0 +1,26 @@
## Summary
What this PR changes and why.
## Scope
- [ ] Code
- [ ] Tests
- [ ] Docs
- [ ] A11y
- [ ] Build/Config
## Verification
Commands run, screenshots, and notes. Include keyboard path if UI.
## Risk & Rollback
Potential impacts; how to revert if needed.
## Checklist
- [ ] Typecheck + ESLint green
- [ ] Vitest green (incl. axe smoke where relevant)
- [ ] No new `any` in public props; no blanket disables
- [ ] Docs/ADRs updated if behavior changes
+29
View File
@@ -0,0 +1,29 @@
{
"labels": [
{ "name": "feat", "color": "#36a64f" },
{ "name": "fix", "color": "#d73a49" },
{ "name": "docs", "color": "#0e8a16" },
{ "name": "test", "color": "#5319e7" },
{ "name": "a11y", "color": "#795548" },
{ "name": "perf", "color": "#1f77b4" },
{ "name": "security", "color": "#b60205" },
{ "name": "chore", "color": "#c0c0c0" },
{ "name": "editor", "color": "#2196f3" },
{ "name": "lists", "color": "#00bcd4" },
{ "name": "blogs", "color": "#8bc34a" },
{ "name": "members", "color": "#009688" },
{ "name": "routing", "color": "#3f51b5" },
{ "name": "state", "color": "#9c27b0" },
{ "name": "build", "color": "#607d8b" },
{ "name": "tests", "color": "#673ab7" },
{ "name": "docs-area", "color": "#4caf50" },
{ "name": "P0", "color": "#e91e63" },
{ "name": "P1", "color": "#ff9800" },
{ "name": "P2", "color": "#ffc107" },
{ "name": "XS", "color": "#e0f7fa" },
{ "name": "S", "color": "#b2ebf2" },
{ "name": "M", "color": "#80deea" },
{ "name": "L", "color": "#4dd0e1" },
{ "name": "XL", "color": "#26c6da" }
]
}
+111
View File
@@ -0,0 +1,111 @@
name: CI
on:
push:
branches: ['update', 'main', 'master']
pull_request:
branches: ['update', 'main', 'master']
workflow_dispatch: {}
jobs:
build_test:
# Relax label matching so any self-hosted runner can pick this up.
runs-on: ['self-hosted']
timeout-minutes: 20
steps:
- name: Show runner context
shell: bash
run: |
set -x
echo "RUNNER_NAME=${RUNNER_NAME:-}"
echo "RUNNER_OS=${RUNNER_OS:-}"
echo "RUNNER_ARCH=${RUNNER_ARCH:-}"
echo "RUNNER_LABELS=${RUNNER_LABELS:-}"
echo "GITHUB_REPOSITORY=${GITHUB_REPOSITORY:-}"
echo "GITHUB_REF=${GITHUB_REF:-}"
echo "GITHUB_SHA=${GITHUB_SHA:-}"
# Manual checkout (no marketplace actions)
- name: Checkout (manual)
shell: bash
run: |
set -euo pipefail
REPO="${GITHUB_REPOSITORY:-}"
SHA="${GITHUB_SHA:-}"
SERVER="${GITHUB_SERVER_URL:-${GITEA_SERVER_URL:-https://gitea.qortal.link}}"
if [ -z "$REPO" ] || [ -z "$SHA" ]; then
echo "::error::Missing GITHUB_REPOSITORY or GITHUB_SHA; cannot checkout"
exit 1
fi
echo "Server: $SERVER"
echo "Repo: $REPO"
echo "SHA: $SHA"
if [ ! -d .git ]; then
git init .
git remote add origin "$SERVER/$REPO"
fi
git fetch --no-tags --depth=1 origin "$SHA"
git checkout -qf FETCH_HEAD
git --no-pager log -1 --oneline
- name: Use Node (nvm if available)
shell: bash
run: |
set -e
NODE_VERSION="20"
if [ -f ".nvmrc" ]; then NODE_VERSION="$(cat .nvmrc | tr -d '\r\n')"; fi
echo "Node target: $NODE_VERSION"
if command -v nvm >/dev/null 2>&1; then
. "$HOME/.nvm/nvm.sh" || true
nvm install "$NODE_VERSION"
nvm use "$NODE_VERSION"
else
echo "nvm not present; using system node: $(node -v 2>/dev/null || echo 'missing')"
fi
node -v || true
npm -v || true
- name: Install deps (ci or fallback to i)
shell: bash
run: |
set -e
if [ -f package-lock.json ]; then
npm ci || npm i
else
npm i
fi
- name: Typecheck (tsc)
shell: bash
env:
CI: true
run: |
npm run typecheck
- name: Lint (Phase 0 scope)
shell: bash
env:
CI: true
run: |
npm run lint:phase0
- name: Format check (Prettier)
shell: bash
env:
CI: true
run: |
npm run format
- name: Tests (vitest run)
shell: bash
run: |
npm test --silent -- --run || npm run test -- --run
# Show summary and ensure coverage thresholds are enforced by Vitest config
if [ -d coverage ]; then ls -lah coverage; fi
- name: Build (vite)
shell: bash
env:
CI: true
run: |
npm run build
+42 -5
View File
@@ -1,16 +1,44 @@
# --- Q-Blog canonical ignore (Phase 1) ---
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Packages & archives
node_modules/
*.zip
node_modules
dist
dist-ssr
*.local
*.tgz
# Build outputs
dist/
dist-ssr/
storybook-static/
# Test & coverage
coverage/
.nyc_output/
playwright-report/
test-results/
# Tool caches
.vite/
.eslintcache
.cache/
.tmp/
tmp/
*.tsbuildinfo
# Reports (local scans, not tracked)
reports/
# Env files
.env
.env.local
.env.*.local
.gitea.env
# Editor directories and files
.vscode/*
@@ -22,3 +50,12 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# Releases
release/
release-*/
.runner
act_runner
# More
.tools/
+3
View File
@@ -0,0 +1,3 @@
fund=false
audit=true
save-exact=false
+1
View File
@@ -0,0 +1 @@
v20.16.0
+5 -8
View File
@@ -1,10 +1,7 @@
{
"printWidth": 80,
"singleQuote": false,
"trailingComma": "es5",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"arrowParens": "avoid",
"tabWidth": 2,
"semi": true
"printWidth": 100,
"singleQuote": true,
"semi": true,
"trailingComma": "all",
"arrowParens": "always"
}
+1
View File
@@ -0,0 +1 @@
* @greenflame089
+24
View File
@@ -0,0 +1,24 @@
# Contributing — Q-Blog (Phase 0)
## Prereqs
- Node 20.16.x (`nvm use`), pnpm or npm
- `pnpm install` or `npm ci`
## Useful scripts
- `pnpm typecheck` — TS noEmit
- `pnpm lint` — ESLint flat config (jsx-a11y)
- `pnpm test` / `pnpm test:run` — Vitest (JSDOM), coverage v8
- `pnpm build` — Vite build if configured
## PR checklist (Phase 0)
- [ ] Lints, types, tests green locally
- [ ] a11y smoke (jest-axe) passes
- [ ] Docs updated if behavior/contract changed
- [ ] Small, vertical slice; clear “How to verify”
## Commit style
Conventional-ish short prefix is fine (`fix:`, `feat:`, `docs:`). Keep subjects concise.
+34
View File
@@ -0,0 +1,34 @@
# Q-Blog Patch-0 Bundle — Manifest
_Where each file belongs (relative to repo root)._
## Tracker & Workflow
- `.gitea/labels.json` — label set for tracker import.
- `.gitea/ISSUE_TEMPLATE/bug_report.md` — bug template.
- `.gitea/ISSUE_TEMPLATE/feature_request.md` — feature template.
- `.gitea/PULL_REQUEST_TEMPLATE.md` — PR template.
- `milestone_patch0.json` — milestone payload for Patch 0.
- `issues/issues_patch0_phase1-3.json` — machine-readable issues for bootstrap script.
- `issues/*.md` — human-readable issue texts (copy/paste).
- `scripts/tracker/bootstrap_patch0.sh` — creates labels, milestone, issues via Gitea API.
- `scripts/tracker/README.md` — bootstrap instructions.
## Docs (foundation)
- `docs/Q-Blog_Project_Instructions.md` — canonical Project Instructions.
- `docs/QUALITY_CHARTER.md` — SLOs/gates and acceptance policy.
- `docs/PATCH0_PLAN.md` — Patch 0 scope and acceptance.
- `docs/DECISIONS/ADR-TEMPLATE.md` — ADR skeleton for future choices.
- `docs/VISION_PRFAQ.md` — narrative and FAQ for 1.0.
- `docs/USER_JOURNEYS.md` — personas and protected flows.
- `docs/GLOSSARY_DOMAIN.md` — shared vocabulary & invariants.
- `docs/RISKS_ASSUMPTIONS.md` — risks and mitigations.
- `docs/ROADMAP_DEPENDENCIES.md` — phase dependencies and gates.
- `docs/SECURITY_PRIVACY_POSTURE.md` — principles & checklist.
- `docs/A11Y_MANUAL_CHECKLIST.md` — manual keyboard/a11y tests.
## Planning aides
- `qblog-structure-and-edit-plan.md` — full file list with edit/new notes.
- `qblog-phase-file-guide.md` — phase-by-phase file plan.
+7
View File
@@ -0,0 +1,7 @@
runner:
capacity: 1
labels:
- 'self-hosted'
- 'linux'
- 'x64'
- 'ubuntu-latest:host'
+27
View File
@@ -0,0 +1,27 @@
# Q-Blog — Accessibility Manual Test Checklist
_Generated 2025-08-16 23:27Z_
## Global
- Tab through top-level navigation, header, main, footer; **skip link** focuses main.
- Focus is always visible; Escape closes modals/popovers and returns focus to invoker.
## Read Post
- Heading hierarchy is logical; images have meaningful `alt` or `alt=""` (decorative).
- Landmarks correctly identify regions; links have descriptive names.
## Create/Edit Post
- Labels programmatic; errors linked via `aria-describedby`.
- Toolbar buttons expose role/name/state; disabled vs. pressed states clear.
- Live region announces save start/success/failure; respects `prefers-reduced-motion`.
## Manage Blogs
- Switcher is keyboard-reachable; selection updates page content and title.
## Collaboration
- Without permission, destructive controls are absent or disabled; UI explains restrictions.
+452
View File
@@ -0,0 +1,452 @@
# Q-Blog v0.0.1 — Source Audit
_Generated: 2025-08-21T19:40:03_
## Inventory Summary
- Extracted to: `/mnt/data/q-blog_v0.0.1_source`
- Total files: **257**
- Total size: **2.47 MB**
- Top-level directories:
- src: 121 files
- scripts: 42 files
- docs: 31 files
- (root): 21 files
- issues: 15 files
- reports: 12 files
- .gitea: 5 files
- tests: 5 files
- pr: 4 files
- public: 1 files
- File types (top 20):
- .tsx: 76
- .md: 59
- .sh: 40
- .ts: 33
- .json: 9
- .txt: 9
- .png: 8
- (noext): 5
- .ttf: 5
- .css: 2
- .js: 2
- .svg: 2
- .bak: 1
- .html: 1
- .ico: 1
- .log: 1
- .mjs: 1
- .yaml: 1
- .yml: 1
Inventory CSV: `/mnt/data/q-blog_inventory_v0.0.1.csv`
## Key Files Presence
- package.json: ✅
- pnpm-lock.yaml: ❌
- yarn.lock: ❌
- package-lock.json: ✅
- tsconfig.json: ✅
- vite.config.ts: ✅
- vitest.config.ts: ✅
- jest.config.js: ❌
- README.md: ❌
- docs/ARCHITECTURE.md: ❌
- docs/TESTING.md: ✅
- docs/ACCESSIBILITY.md: ❌
- docs/SECURITY.md: ❌
- docs/USER_JOURNEYS.md: ✅
- docs/GLOSSARY.md: ❌
- docs/ROADMAP_DEPENDENCIES.md: ✅
- docs/DECISIONS/README.md: ❌
- src/main.tsx: ✅
- src/App.tsx: ✅
- src/index.css: ✅
- src/routes.tsx: ❌
- src/router.tsx: ❌
- src/components/BlogSwitcher.tsx: ❌
- src/components/PostEditor.tsx: ❌
- src/components/PostList.tsx: ❌
- src/components/MembersPanel.tsx: ❌
- src/components/HeaderNav.tsx: ❌
- src/store/index.ts: ❌
- src/store/api.ts: ❌
- src/store/slices/postsSlice.ts: ❌
- src/store/slices/blogsSlice.ts: ❌
- src/store/slices/authSlice.ts: ❌
- src/i18n.ts: ❌
- src/theme.ts: ❌
- tests/setup.ts: ✅
- tests/App.test.tsx: ❌
- tests/components/PostEditor.test.tsx: ❌
- scripts/release/create-gitea-release.sh: ✅
- scripts/release/build-archive.sh: ✅
- docs/RELEASE_NOTES_v0.0.1.md: ✅
- .gitea/workflows/ci.yml: ✅
- .gitea/workflows/release.yml: ❌
## package.json Snapshot
```json
{
"name": "q-blog",
"version": "0.0.1",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint .",
"format": "prettier --check .",
"format:write": "prettier --write .",
"test": "vitest",
"test:run": "vitest run",
"typecheck": "tsc -p tsconfig.json -noEmit",
"lint:phase0": "LINT_SCOPE=phase0 eslint .",
"lint:full": "LINT_SCOPE=full eslint .",
"check": "npm run typecheck && npm run lint:phase0 && npm run test:run",
"scan:phase1": "bash scripts/dev/phase1-scan.sh",
"report:phase1": "bash scripts/dev/phase1-report.sh"
},
"dependencies": {
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@mui/icons-material": "^5.11.11",
"@mui/material": "^5.11.13",
"@reduxjs/toolkit": "^1.9.3",
"@types/react-grid-layout": "^1.3.2",
"axios": "^1.3.4",
"compressorjs": "^1.2.1",
"localforage": "^1.10.0",
"moment": "^2.29.4",
"philliplm-react-modern-audio-player": "^1.4.6",
"react": "^18.2.0",
"react-copy-to-clipboard": "^5.1.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-grid-layout": "^1.3.4",
"react-intersection-observer": "^9.4.3",
"react-masonry-css": "^1.0.16",
"react-redux": "^8.0.5",
"react-resize-detector": "^8.0.4",
"react-router-dom": "^6.9.0",
"react-toastify": "^9.1.2",
"react-virtuoso": "^4.3.3",
"short-unique-id": "^4.4.4",
"slate": "^0.91.4",
"slate-history": "^0.86.0",
"slate-react": "^0.91.11",
"ts-key-enum": "^2.0.12"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@mui/types": "^7.2.3",
"@testing-library/jest-dom": "^6.7.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^24.3.0",
"@types/react": "^18.3.23",
"@types/react-copy-to-clipboard": "^5.0.4",
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react-swc": "^3.2.0",
"@vitest/coverage-v8": "^3.2.4",
"axe-core": "^4.10.3",
"eslint": "^9.33.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react-hooks": "^5.2.0",
"jest-axe": "^10.0.0",
"jsdom": "^26.1.0",
"msw": "^2.10.5",
"prettier": "^2.8.8",
"typescript": "^4.9.5",
"typescript-eslint": "^8.39.1",
"vite": "^4.2.0",
"vitest": "^3.2.4",
"worker-loader": "^3.0.8"
}
}
```
## tsconfig.json (key fields)
```json
{}
```
## Tooling Signals in Source
- redux_toolkit: 6 files
- rtk_query: 0 files
- slate: 6 files
- mui: 51 files
- i18n: 0 files
- vite: 0 files
- dompurify: 0 files
- zod: 0 files
- jest_axe: 0 files
- msw: 0 files
- rtl: 0 files
- virtualize: 0 files
- sample `mui`: src/App.tsx, src/pages/BlogIndividualPost/BlogIndividualPost.tsx, src/pages/BlogIndividualProfile/BlogIndividualProfile.tsx, src/pages/CreatePost/CreatePost.tsx, src/pages/CreatePost/CreatePostMinimal.tsx, src/pages/CreatePost/CreatePost-styles.ts ...
- sample `slate`: src/pages/CreatePost/CreatePostMinimal.tsx, src/pages/CreatePost/CreatePostBuilder.tsx, src/pages/EditPost/EditPost.tsx, src/components/editor/BlogEditor.tsx, src/components/editor/ReadOnlySlate.tsx, src/components/editor/customTypes.ts
- sample `redux_toolkit`: src/state/store.ts, src/state/features/globalSlice.ts, src/state/features/blogSlice.ts, src/state/features/authSlice.ts, src/state/features/mailSlice.ts, src/state/features/notificationsSlice.ts
## Routes & Landmarks (heuristic)
- src/main.tsx: h1=0, header=False, nav=False, main=False, footer=False
- src/App.tsx: h1=0, header=False, nav=False, main=False, footer=False
- src/pages/BlogIndividualPost/BlogIndividualPost.tsx: h1=0, header=False, nav=False, main=False, footer=False
- src/pages/BlogIndividualProfile/BlogIndividualProfile.tsx: h1=0, header=False, nav=False, main=False, footer=False
- src/pages/CreatePost/CreatePost.tsx: h1=0, header=False, nav=False, main=False, footer=False
- src/pages/CreatePost/CreatePostMinimal.tsx: h1=0, header=False, nav=False, main=False, footer=False
- src/pages/CreatePost/CreatePost-styles.ts: h1=0, header=False, nav=False, main=False, footer=False
- src/pages/CreatePost/CreatePostBuilder.tsx: h1=0, header=False, nav=False, main=False, footer=False
- src/pages/CreatePost/components/Toolbar/EditorToolbar.tsx: h1=0, header=False, nav=False, main=False, footer=False
- src/pages/CreatePost/components/Navbar/NavbarBuilder.tsx: h1=0, header=False, nav=False, main=False, footer=False
- src/pages/EditPost/EditPost.tsx: h1=0, header=False, nav=False, main=False, footer=False
- src/pages/Home/Home.tsx: h1=0, header=False, nav=False, main=False, footer=False
- src/pages/CreateEditProfile/CreatEditProfile.tsx: h1=0, header=False, nav=False, main=False, footer=False
- src/pages/BlogList/PostPreview-styles.ts: h1=0, header=False, nav=False, main=False, footer=False
- src/pages/BlogList/PostPreview.tsx: h1=0, header=False, nav=False, main=False, footer=False
- src/pages/BlogList/BlogList.tsx: h1=0, header=False, nav=False, main=False, footer=False
## Release Scripts — quick notes
### scripts/release/create-gitea-release.sh (excerpt)
```bash
#!/usr/bin/env bash
set -euo pipefail
# create-gitea-release.sh
# Creates/updates a Gitea release for a given version/tag and uploads zips from release/.
#
# Env required: GITEA_BASE_URL, GITEA_TOKEN, OWNER, REPO
# Usage:
# bash scripts/release/create-gitea-release.sh 0.0.1 \
# --title "Phase 0 — v0.0.1" \
# --notes docs/RELEASE_NOTES_v0.0.1.md \
# --branch update \
# [--draft] [--prerelease] [--assets 'release/*.zip']
#
# Defaults:
# title: "v<version>"
# notes: docs/RELEASE_NOTES_v<version>.md if exists, else empty
# branch: current git branch (fallback: main)
# assets: release/*.zip
#
# Requires: curl, jq
require() { command -v "$1" >/dev/null 2>&1 || { echo "Missing required command: $1" >&2; exit 1; }; }
require curl
require jq
: "${GITEA_BASE_URL:?Set GITEA_BASE_URL (e.g., https://gitea.example.com)}"
: "${GITEA_TOKEN:?Set GITEA_TOKEN}"
: "${OWNER:?Set OWNER}"
: "${REPO:?Set REPO}"
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <version|vX.Y.Z> [--title TITLE] [--notes FILE] [--branch BRANCH] [--draft] [--prerelease] [--assets GLOB]" >&2
exit 2
fi
VER_RAW="$1"; shift
TAG="${VER_RAW}"
[[ "${TAG}" != v* ]] && TAG="v${TAG}"
TITLE="${TAG}"
NOTES_FILE=""
BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo main)"
DRAFT=false
PRERELEASE=false
ASSETS_GLOB="release/*.zip"
while [[ $# -gt 0 ]]; do
case "$1" in
--title) TITLE="${2:-${TITLE}}"; shift 2;;
--notes) NOTES_FILE="${2:-}"; shift 2;;
--branch) BRANCH="${2:-${BRANCH}}"; shift 2;;
--draft) DRAFT=true; shift;;
--prerelease) PRERELEASE=true; shift;;
--assets) ASSETS_GLOB="${2:-${ASSETS_GLOB}}"; shift 2;;
*) echo "Unknown arg: $1" >&2; exit 2;;
esac
done
if [[ -z "${NOTES_FILE}" ]]; then
CANDIDATE="docs/RELEASE_NOTES_${TAG}.md"
if [[ -f "${CANDIDATE}" ]]; then
NOTES_FILE="${CANDIDATE}"
else
CANDIDATE="docs/RELEASE_NOTES_${TAG#v}.md"
[[ -f "${CANDIDATE}" ]] && NOTES_FILE="${CANDIDATE}" || NOTES_FILE=""
fi
fi
BODY=""
if [[ -n "${NOTES_FILE}" && -f "${NOTES_FILE}" ]]; then
BODY="$(cat "${NOTES_FILE}")"
fi
API="${GITEA_BASE_URL%/}/api/v1"
auth() { curl -sS -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json"; }
auth_up() { curl -sS -H "Authorization: token ${GITEA_TOKEN}"; }
echo "== Looking up release by tag ${TAG} =="
GET_URL="${API}/repos/${OWNER}/${REPO}/releases/tags/${TAG}"
set +e
EXIST_JSON="$(auth GET "${GET_URL}" 2>/dev/null)"
RC=$?
set -e
RELEASE_ID=""
if [[ ${RC} -eq 0 && -n "${EXIST_JSON}" && "$(echo "${EXIST_JSON}" | jq -r '.id // empty')" != "" ]]; then
RELEASE_ID="$(echo "${EXIST_JSON}" | jq -r '.id')"
echo "Found existing release id=${RELEASE_ID}; will PATCH"
PAYLOAD="$(jq -n \
--arg name "${TITLE}" \
--arg body "${BODY}" \
--argjson draft ${DRAFT} \
--argjson prerelease ${PRERELEASE} \
'{name:$name, body:$body, draft:$draft, prerelease:$prerelease}')"
REL_JSON="$(echo "${PAYLOAD}" | auth -X PATCH "${API}/repos/${OWNER}/${REPO}/releases/${RELEASE_ID}" -d @-)"
else
echo "No existing release; creating"
PAYLOAD="$(jq -n \
--arg tag_name "${TAG}" \
--arg target_commitish "${BRANCH}" \
--arg name "${TITLE}" \
--arg body "${BODY}" \
--argjson draft ${DRAFT} \
--argjson prerelease ${PRERELEASE} \
'{tag_name:$tag_name, target_commitish:$target_commitish, name:$name, body:$body, draft:$draft, prerelease:$prerelease}')"
REL_JSON="$(echo "${PAYLOAD}" | auth -X POST "${API}/repos/${OWNER}/${REPO}/releases" -d @-)"
RELEASE_ID="$(echo "${REL_JSON}" | jq -r '.id // empty')"
fi
if [[ -z "${RELEASE_ID}" ]]; then
echo "ERROR: could not determine release id" >&2
echo "${REL_JSON:-"(no server response)"}" >&2
exit 1
fi
HTML_URL="$(echo "${REL_JSON}" | jq -r '.html_url // .url // empty')"
echo "Release ready (id=${RELEASE_ID}) ${HTML_URL}"
echo "== Uploading assets from ${ASSETS_GLOB} =="
```
### scripts/release/build-archive.sh (excerpt)
```bash
#!/usr/bin/env bash
set -euo pipefail
# build-archive.sh
# Creates source zip (always) and an optional dist zip if --with-build succeeds.
# Usage:
# bash scripts/release/build-archive.sh [--with-build] [--outdir release] [--name q-blog]
#
# Notes:
# - By default, NO TypeScript build is executed to avoid failing on app code during Phase 0.
# - Pass --with-build to attempt `npm ci` + `npm run build`; if dist/ exists afterwards, a dist zip is created.
WITH_BUILD=0
OUTDIR="release"
NAME=""
while [[ $# -gt 0 ]]; do
case "$1" in
--with-build) WITH_BUILD=1; shift;;
--outdir) OUTDIR="${2:-release}"; shift 2;;
--name) NAME="${2:-}"; shift 2;;
*) echo "Unknown arg: $1" >&2; exit 2;;
esac
done
require() { command -v "$1" >/dev/null 2>&1 || { echo "Missing required command: $1" >&2; exit 1; }; }
require jq
require zip
if [[ -z "${NAME}" ]]; then
if [[ -f package.json ]]; then
NAME="$(jq -r '.name // empty' package.json)"
fi
[[ -z "${NAME}" ]] && NAME="$(basename "$PWD")"
fi
if [[ -f package.json ]]; then
VERSION="$(jq -r '.version // "0.0.0"' package.json)"
else
VERSION="0.0.0"
fi
mkdir -p "${OUTDIR}"
TAG="v${VERSION}"
SRC_ZIP="${OUTDIR}/${NAME}-${TAG}-src.zip"
DIST_ZIP="${OUTDIR}/${NAME}-${TAG}-dist.zip"
echo "== Creating source archive =="
# Use zip with excludes (works even without git)
zip -q -9 -r "${SRC_ZIP}" . \
-x "node_modules/*" ".git/*" "${OUTDIR}/*" "dist/*" ".vite/*" "coverage/*" "*.log" "*.zip"
if [[ ${WITH_BUILD} -eq 1 ]]; then
echo "== Build step (best-effort) =="
if jq -e '.scripts.build' package.json >/dev/null 2>&1; then
# prefer ci if present, otherwise install
if jq -e '.scripts.ci' package.json >/dev/null 2>&1; then
npm run ci || true
else
(command -v npm >/dev/null 2>&1 && npm ci) || (npm install || true)
fi
npm run build || echo "(build failed or not configured — continuing)"
else
echo "No build script in package.json — skipping build"
fi
if [[ -d dist ]]; then
echo "== Creating dist archive =="
(cd dist && zip -q -9 -r "../${DIST_ZIP##*/}" .)
else
echo "dist/ not found — skipping dist zip."
fi
else
echo "(Skipping build; source zip only. Use --with-build to attempt a build.)"
fi
echo "Artifacts:"
echo " - ${SRC_ZIP}"
[[ -f "${DIST_ZIP}" ]] && echo " - ${DIST_ZIP}"
```
**Potential issues detected:**
- Release script may not validate required args (title/notes); add guards or defaults to avoid empty curl parameters.
## Docs vs Code — spot verification
- README.md mentions Redux Toolkit: ️ code present but not documented
- README.md mentions RTK Query: —
- README.md mentions Slate editor: ️ code present but not documented
- README.md mentions MUI v5: ️ code present but not documented
- README.md mentions i18n: —
- README.md mentions DOMPurify: —
- README.md mentions Zod validation: —
- README.md mentions MSW/jest-axe: —
## Priority Findings & Questions
1. Content sanitization (DOMPurify) not detected in source scan. Verify sanitize-on-save and sanitize-on-render are implemented.
2. Zod validation not detected; confirm validation at form and API boundaries or update docs.
3. Testing stack may be incomplete (MSW/jest-axe). Ensure test coverage for autosave, a11y smoke, and error paths.
4. RTK Query not detected; if used, ensure endpoints are defined with blog-scoped cache keys.
## Next Steps (thin vertical slice)
1. Run `pnpm i` (or npm/yarn) and `pnpm test` to confirm toolchain and a11y smoke tests.
2. Verify PostEditor autosave path, live region announcements, and sanitize pipeline.
3. Resolve release script guard/arg issues; add CI job to create release on tag.
4. Pick a small feature change and map the affected slice (state → API → UI → tests).
+6
View File
@@ -0,0 +1,6 @@
## v0.2.2 — 2025-08-22
- Wiki edit flow: no blog required for editors; publish BLOG_POST only
- Auth: auto-run on mount; button shows loading; deduped dispatch
- Names: loading/error/empty states in popover
- Tests: coverage for edit flow, auth UI, names UI
+16
View File
@@ -0,0 +1,16 @@
QBlog v0.3.0 — Enhancements
- Image Lightbox: Click images in blog posts to open a fullwindow overlay for easier viewing. Close by clicking anywhere or pressing Escape.
- ScrollToTop Button: A floating button appears after you scroll down and returns you to the top smoothly when clicked.
- Bookmarks: Save links to QBlogs and individual posts. Access saved items from the new Bookmarks menu entry and manage them on the Bookmarks page.
- On first load, existing Favorites are migrated into Bookmarks automatically and then cleared from the old storage.
Developer Notes
- New components: `src/components/common/ImageLightbox.tsx`, `src/components/common/ScrollToTop.tsx`.
- New slice: `src/state/features/bookmarksSlice.ts` added to `src/state/store.ts`.
- New page: `src/pages/Bookmarks/Bookmarks.tsx` and route `/bookmarks` wired in `src/App.tsx`.
- `BlogIndividualPost` and `BlogIndividualProfile` updated with bookmark toggles.
- `Navbar` now shows Bookmarks (Favorites became Bookmarks). Post list cards now use Bookmarks.
- The main header now has: QBlog logo, search bar, notifications bell, a Main Menu (Create Blog/Post, My Blogs, view toggle, Subscriptions, Bookmarks, Blocked Names, QMail), and the Name selector dropdown (for switching names only).
- `ResponsiveImage` now forwards `alt` to the underlying `<img>` for improved a11y.
+19
View File
@@ -0,0 +1,19 @@
# CI on Gitea (no marketplace)
- Gitea runner may not auto-checkout; and `actions/checkout` might be unavailable.
- Workflow includes a **manual checkout** step using `$GITHUB_SERVER_URL` and `$GITHUB_REPOSITORY`.
- Runner label used: `ubuntu-latest:host` (matches your self-hosted runner config).
- Trigger: `push` (main/master/update) + `pull_request`.
- Gate: `npm run check` (vitest smoke + eslint phase0 scope).
If multiple workflows exist, run:
```bash
bash scripts/dev/ci-ensure-one-workflow.sh
git commit -m "ci: ensure single workflow"
git push
```
Troubleshooting:
- Failure: `package.json not found` → manual checkout couldn't determine ref; ensure repo is public or set a read token as secret env `GIT_READ_URL` and replace the clone URL accordingly.
+51
View File
@@ -0,0 +1,51 @@
# CI on Gitea (self-hosted runner)
This repo uses a self-hosted Gitea Actions runner. The workflow lives in **.gitea/workflows/ci-no-marketplace.yml** and targets the labels:
- `self-hosted`
- `linux`
- `ubuntu-latest:host`
## Runner config example (no Docker)
Create **config.yaml** next to the act runner binary/service:
```yaml
runner:
capacity: 1
labels:
- 'self-hosted'
- 'linux'
- 'x64'
- 'ubuntu-latest:host'
```
Restart the runner service after changes.
## Lockfile policy
When `package-lock.json` exists, CI runs **npm ci** (fast, reproducible). Without a lockfile, CI falls back to **npm install**. Prefer committing a lockfile for stable builds.
## Common gotchas
- **Node setup order** — Node must be set _before_ installs. The workflow ensures this.
- **Dirty action cache** — If you see messages like
`Unable to pull refs/heads/v4: worktree contains unstaged changes`,
clean the runner's cached actions (see troubleshooting).
## Local preflight
```bash
# run the same checks locally
npm run lint:phase0
npm test -- --run
```
## Release environment
For local release scripts (e.g., creating a Gitea release), place secrets in a local `.gitea.env` file at the repo root and source it when running scripts. Do not commit this file.
- Use `.gitea.env.example` as a template.
- Ensure `.gitea.env` remains ignored via `.gitignore`.
If a token was ever committed, rotate it in Gitea and consider cleaning it from history.
+39
View File
@@ -0,0 +1,39 @@
# CI Runner Troubleshooting (Quick)
If a workflow shows **Waiting** indefinitely, its almost always one of:
1. **No runner matches labels** This workflow requires only the `self-hosted` label.
Ensure your runner config includes it:
```yaml
runner:
capacity: 1
labels: ['self-hosted', 'linux', 'x64']
```
2. **Runner offline** start the runner process and watch logs:
```bash
./run.sh # or the systemd service you configured
```
You should see “listening for jobs”.
3. **Another job occupying capacity** your runner has `capacity: 1`.
Cancel the stuck job in Actions UI.
4. **Repo visibility / permissions** ensure Actions are enabled for this repo and the runner is allowed to pick jobs from it.
This repos workflow doesnt use marketplace actions, Docker, or services—so a plain runner works.
## Verify via API (optional)
```bash
# runs list
curl -s -H "Authorization: token $GITEA_TOKEN" \
"$GITEA_BASE_URL/api/v1/repos/$OWNER/$REPO/actions/runs?limit=5" | jq .
# a specific run
curl -s -H "Authorization: token $GITEA_TOKEN" \
"$GITEA_BASE_URL/api/v1/repos/$OWNER/$REPO/actions/runs/{run_id}" | jq .
```
+41
View File
@@ -0,0 +1,41 @@
# CI Troubleshooting
## 1) "Unable to pull refs/heads/v4: worktree contains unstaged changes"
The runner caches cloned **actions** (e.g., `actions/checkout@v4`). If those folders get modified, auto-update fails.
**Fix (on the runner host):** stop the runner, then clean caches.
```bash
# stop your gitea act runner first
# common cache locations (try any that exist)
CANDIDATES=(
"$HOME/.cache/act/actions"
"$HOME/.cache/actions"
"$HOME/_actions"
"/var/lib/act_runner/data/actions"
"/var/lib/act_runner/_actions"
)
for d in "${CANDIDATES[@]}"; do
if [ -d "$d" ]; then
echo "Cleaning $d"
rm -rf "$d"
fi
done
# start the runner again
```
## 2) npm errors during "Install dependencies"
- Ensure **package-lock.json** is committed.
- If you purposely avoid a lockfile, CI will run `npm install` as a fallback.
- Verify Node/npm versions (workflow sets Node 20).
## 3) Workflow not triggering
- In the repo: **Settings → Actions** must be enabled.
- Ensure the file lives at: **.gitea/workflows/ci-no-marketplace.yml**.
- The runner must advertise labels that the workflow requires.
@@ -0,0 +1,28 @@
# ADR 0005 — Allow Multiple Blogs per Name (Plan A)
Date: 2025-08-21
Status: Accepted
## Context
The product contract states a **Name** can own multiple **Blogs**. The UI currently assumes one blog per name (single “My Blog” button; `/{name}` → single blog).
## Decision
- Introduce a **User Blogs** page at `/{name}/blogs` listing all blogs for a Name.
- Change `/{name}` behavior to **smart redirect**:
- If exactly one blog → redirect to that blogs posts route.
- Else → show `/{name}/blogs`.
- Replace “My Blog” button with a **“My Blogs” dropdown** listing all blogs and a “Create new” action.
- Keep existing blog-scoped routes; no schema changes.
## Consequences
- Minimal code churn; consistent with routing & scoping contracts.
- Slight change in navigation expectations for multi-blog Names (mitigated by redirect rule).
- Adds one new page and augments header menu; no backend migration.
## Alternatives Considered
- **Ultra-minimal**: only a dropdown; no new page. Rejected to avoid discoverability issues.
- **Always list page**: `/{name}` → blogs list even for single blog. Rejected for extra click in common case.
@@ -0,0 +1,26 @@
# ADR 0006 — Wiki Mode (Multi-Editor via Visible Revisions)
Date: 2025-08-22
Status: Accepted
## Context
Clone-on-edit bug revealed a path to collaborative editing. We want to formalize this as Wiki mode.
## Decision
- Add per-blog wikiEnabled toggle.
- Add Name-based whitelist/blacklist with precedence: blacklist > whitelist > global.
- Posts remain immutable; revisions link via originPostId/parentPostId/lineageBlogId.
- Canonical revision chosen client-side by resolver.
## Consequences
- Multiple editors may contribute visible revisions.
- Owner can revoke visibility instantly by changing lists.
- Legacy posts unaffected (wikiEnabled missing = false).
## Alternatives
- Server-side canonical logic (not possible in Qortal).
- Per-revision ACLs (heavier; unnecessary for v1).
@@ -0,0 +1,19 @@
# ADR-0007 — Wiki Edit Flow & Auth Adjustments (v0.2.2)
**Status:** Accepted
**Date:** 2025-08-22
## Context
Users could not edit others posts on wiki-enabled blogs unless they owned a blog. Auth feedback was unclear; initial auth no longer triggered on first load.
## Decision
1. **Edit flow:** In edit mode, do not require `currentBlog`. Publish only `service: BLOG_POST` under the editors Name using the original identifier. Never publish `service: BLOG` from edit screens.
2. **Auth:** Trigger `askForAccountInformation()` on initial load (idempotent). Show loading states for Authenticate button and names popover.
## Consequences
- Allows true wiki behavior; avoids accidental “create a blog first” blocker.
- Clear, accessible feedback during auth; fewer duplicate requests.
- Tests added to prevent regressions.
+26
View File
@@ -0,0 +1,26 @@
# ADR-XXXX — Title
Status: Proposed | Accepted | Superseded | Deprecated
Date: 2025-08-16 23:43Z
## Context
What problem are we solving? What constraints apply?
## Options
- Option A — pros/cons
- Option B — pros/cons
- (Optional) Option C — pros/cons
## Decision
Which option and why.
## Consequences
Positive/negative outcomes, follow-ups, measurable impacts.
## References
Links to issues, PRs, specs; related ADRs.
+33
View File
@@ -0,0 +1,33 @@
# Developer Guide — Multiple Blogs per Name (Plan A)
_Generated 2025-08-21_
**Start here** to implement the multi-blog feature. This guide links to the overview, technical plan, and ADR; then gives a fast manual test checklist.
## Read these first
- `docs/features/FEATURE_MULTIBLOG_OVERVIEW.md`
- `docs/features/TECH_IMPL_MULTIBLOG.md`
- `docs/DECISIONS/ADR-0005-multiple-blogs-per-name.md`
## Quick path (tasks)
1. Add `/{name}/blogs` route and component (`UserBlogs.tsx`).
2. Add smart redirect for `/{name}` → single blog → `/{name}/{blog}/posts`, else `/{name}/blogs`.
3. Convert Header “My Blog” to **“My Blogs” dropdown** (0 blogs → “Create Blog” button).
4. Wire `listBlogsByName(nameId)` query if missing; use it in Header + UserBlogs.
5. Tests: header menu behavior, redirect, UserBlogs rendering + empty states, basic a11y smoke.
## Manual test checklist (click-through)
- As a new user (0 blogs):
- Header shows **Create Blog**; clicking creates first blog.
- Visiting `/{name}` navigates to `/{name}/blogs` with empty state + CTA.
- As a user with 1 blog:
- Visiting `/{name}` lands on that blogs posts (history replaced).
- Header shows **My Blogs**; menu lists 1 blog + “Create new blog”.
- As a user with 2+ blogs:
- Header menu lists all blogs; selecting one routes to `/{name}/{blog}/posts` and marks it active.
- `/{name}/blogs` lists all blogs; shows Edit/Create when viewing own name.
- Keyboard:
- Menu opens with Enter/Space; items navigable with arrows; ESC closes and returns focus to trigger.
+21
View File
@@ -0,0 +1,21 @@
# Gitea environment quick setup
1. Create `.gitea.env` in repo root (or reuse your existing one):
```
GITEA_BASE_URL=https://gitea.qortal.link
GITEA_TOKEN=your_40_char_token
OWNER=greenflame089
REPO=q-blog
```
2. Verify:
```
bash scripts/tracker/with_env.sh .gitea.env bash scripts/tracker/verify_phase0.sh
```
Notes:
- `verify_phase0.sh` will auto-load `.gitea.env` if env vars are missing.
- If you already exported vars in your shell, you can call the script directly.
+23
View File
@@ -0,0 +1,23 @@
# Q-Blog — Glossary & Domain Canon
_Generated 2025-08-16 23:27Z_
**Name** — The account identity under which blogs are created.
**Blog** — A container for posts; owned by a Name; has **handle**, **title**, **visibility**.
**Handle** — Human-friendly unique identifier per Name (slug rules).
**Post** — Content item; belongs to exactly one Blog (immutable link).
**Role**`owner | editor | author`; defines allowed operations.
**Membership** — Name ↔ Blog relationship with role.
**Revision** — Monotonically increasing number used for concurrency control.
**Invite** — Time-limited token that assigns a role on acceptance.
### Invariants
- A Posts `blogId` does not change after creation.
- (`nameId`, `blogHandle`) is unique.
- All write operations require a role check (server-enforced).
### Identifier & URL Guidance
- Canonical blog URL: `/{nameHandle}/{blogHandle}/…` (conceptual).
- Slugs are normalized to lowercase, ASCII, hyphen-separated; collisions rejected with a helpful message.
+26
View File
@@ -0,0 +1,26 @@
# Patch 0 — Orientation & Quality Bar
_Generated 2025-08-16 23:43Z_
## Goal
Ratify the Quality Charter and spin up the basic governance scaffolding before touching code.
## Tasks
1. **Adopt Quality Charter** — review, adjust targets if needed, and sign.
2. **Label set** — create canonical labels in the tracker (Area, Type, Priority, Size).
3. **Templates** — add Issue + PR templates with acceptance sections and a11y/security checks.
4. **Milestone** — create `Patch 0` milestone with this checklist as description.
5. **Backlog triage** — seed initial issues for Phase 13 planning (docs, harness, correctness).
## Label Set (proposed)
- **Type:** feat, fix, chore, docs, test, a11y, perf, security
- **Area:** editor, lists, blogs, members, routing, state, build, tests, docs
- **Priority:** P0, P1, P2
- **Size:** XS, S, M, L, XL
## Acceptance
- Charter signed; templates merged; labels exist; milestone created; 58 seeded issues for Phase 13.
+21
View File
@@ -0,0 +1,21 @@
# Phase 0 Closeout Checklist
**Goal:** repository hygiene, quality bar, and scaffolding. No behavior changes.
## Must-haves
- [ ] **Tracker**: canonical labels deduped; Phase 0 milestone open; kickoff issues present.
- [ ] **Docs**: Quality Charter, Project Instructions, Testing, Releasing.
- [ ] **Harness**: Vitest + RTL + MSW + jest-axe; `tests/axe-smoke.test.tsx` passing.
- [ ] **Lint**: ESLint flat config working; Phase 0 scope excludes `src/**`.
- [ ] **CI**: Gitea workflow runs test + lint(phase0) on PRs.
- [ ] **Version**: bump to `0.0.1` via `scripts/release/bump-version.sh phase0`.
## Nice-to-haves
- [ ] ENV helper scripts committed (`scripts/tracker/*`).
- [ ] Status doc updated (`docs/PHASE0_STATUS.md`).
## Release notes
Create `docs/RELEASE_NOTES_v0.0.1.md` with one-liners (hygiene, docs, harness, CI).
+29
View File
@@ -0,0 +1,29 @@
# Phase 0 — Orientation & Quality Bar — **Completed**
**Date:** 2025-08-17 04:57Z
## What we delivered
- **Tracker hygiene:** canonical labels; duplicate cleanup helpers; milestone created; seed issues for Phase 13.
- **CI (self-hosted):** single workflow (`.gitea/workflows/ci.yml`) using only built-in steps; verified green.
- **Dev harness:** Vitest + @testing-library/react + jest-axe; a11y smoke `tests/axe-smoke.test.tsx` passing.
- **Lint posture:** ESLint flat config + phase-scoped lint (Phase 0 ignores `src/**`); scripts to narrow/expand scope.
- **Docs & instructions:** project instructions, CONTRIBUTING, RELEASING, CI runner notes, Phase plans.
- **Utility scripts:** tracker bootstrap/verify; label dedupe; PR helpers.
## Acceptance recap
- Tests run and pass in CI.
- Lint in Phase 0 scope is clean.
- One CI workflow active; runner picks up jobs.
- Milestone & initial issues exist.
## Next (Phase 1 preview)
- Remove remaining `@ts-nocheck` (scripted) and start type hygiene sweeps.
- Fix MUI v4 imports, a11y `<img alt>` gaps, and hooks-in-non-components issues.
- Begin file-scoped lint expansion behind small PRs.
---
**Status:**_Phase 0 complete_ — propose version bump to **0.0.1** and tag release.
+16
View File
@@ -0,0 +1,16 @@
# Phase 1 — Next Steps (Small, Mergeable PRs)
1. **MUI v4 → v5 import fix**
- Run: `bash scripts/dev/phase1/fix-mui-imports.sh --dry` then `--apply`.
- Commit only the touched files + `reports/phase1-*/notes.txt` (optional).
2. **Remove TS suppressions**
- Run: `bash scripts/dev/phase1/fail-on-suppressions.sh` (or `--allow=1` while fixing one file per PR).
- Convert ignored sections to proper types or refactor.
3. **A11y: `<img>` alt text**
- Run: `bash scripts/dev/phase1/list-imgs-missing-alt.sh` and fix each instance (`alt` or `alt=""` if decorative).
Keep each PR focused (15 files), branch from `update`, and let CI run.
+33
View File
@@ -0,0 +1,33 @@
# Phase 1 — Baseline Scan & Planning
Goal: capture a **ground-truth snapshot** of technical debt (imports, ts-nochecks, any-usage, a11y hotspots) without changing app code.
## Deliverables
- `reports/phase1-*/` folder with raw findings and a concise `summary.md`.
- Package scripts: `scan:phase1`, `report:phase1`.
## What we scan
- **MUI v4 imports** (`@material-ui/*`) — should move to `@mui/material` & friends.
- **TS suppression** (`@ts-nocheck`, `@ts-ignore`) — create removal plan.
- **`any` usage** — count & top files.
- **A11y (img alt)** — naive grep for `<img` without `alt=`.
- **Hooks misuse** — list all `useDispatch|useSelector` occurrences for manual triage.
- **WebWorkers globals** — `self`, `XMLHttpRequest`, `fetch` in `src/webworkers/*`.
## How to run
```bash
# 1) patch scripts into package.json
scripts/dev/patch-phase1-scripts.sh
# 2) run scan
npm run scan:phase1
# 3) generate summary
npm run report:phase1
# 4) open the report
sed -n '1,200p' reports/phase1-*/summary.md
```
+23
View File
@@ -0,0 +1,23 @@
# Phase 1 — Step 1 (Tiny PRs)
1. **Remove `@ts-nocheck`**
```bash
bash scripts/dev/phase1/tsnocheck-remove.sh
git push origin HEAD
```
2. **Alt text audit**
```bash
bash scripts/dev/phase1/list-imgs-missing-alt.sh
bash scripts/dev/phase1/alt-fix-todo.sh
# open the generated reports/*/alt-fix-todo.md and address each item
```
3. **MUI v4 imports → v5** (preview then apply)
```bash
bash scripts/dev/phase1/fix-mui-imports.sh --dry
bash scripts/dev/phase1/fix-mui-imports.sh
git push origin HEAD
```
+18
View File
@@ -0,0 +1,18 @@
# Phase 1 — Tiny PR checklist
1. **Remove `// @ts-nocheck` (2 files)**
- Dry run: `bash scripts/dev/phase1/remove-ts-nocheck.sh`
- Apply: `bash scripts/dev/phase1/remove-ts-nocheck.sh --apply`
- Commit on `update`: `git add -A && git commit -m "phase1: drop ts-nocheck headers (no behavior change)" && git push`
2. **MUI v4 → v5 imports (if any)**
- Dry: `bash scripts/dev/phase1/fix-mui-imports.sh --dry`
- Apply in a tiny batch (13 files): `--apply`, commit & push.
3. **A11y: <img alt>**
- `bash scripts/dev/phase1/list-imgs-missing-alt.sh`
- Fix each hit with meaningful `alt`, or `alt=""` when decorative.
Keep PRs tiny and non-breaking so upstream can merge at any time.
+267
View File
@@ -0,0 +1,267 @@
**TL;DR:** Below is a higher-level, technical “how well do it” roadmap for Phases 010. It explains approaches, contracts, guardrails, and sequencing—no filenames or concrete code yet.
---
# Q-Blog → 1.0.0: Technical Roadmap (Conceptual “How”)
## Phase 0 — Orientation & Quality Bar
**How:**
- Define protected user journeys and SLIs/SLOs (e.g., time-to-interactive, save success rate, keyboard reachability).
- Adopt a single “definition of done” template (accessibility, tests, docs, telemetry hook, error state).
- Establish a lightweight decision record format (context → options → decision → impact) for any cross-cutting choice.
---
## Phase 1 — Project Docs & Working Agreements
**How:**
- Author concise, source-of-truth docs: a high-level architecture map (UI/state/data flows), a testing strategy pyramid, an accessibility standard, and contribution rules.
- Record initial decisions (editor stack, state mgmt, theming, data shape conventions).
- Add a changelog discipline (human-readable entries grouped by “Added/Changed/Fixed/Removed/Docs/Tests”).
- Define semantic labels for issues/PRs (type, area, effort) to enable predictable triage.
---
## Phase 2 — Test Harness & Linting Baseline
**How:**
- Testing layers:
- **Unit & component:** Vitest + JSDOM + React Testing Library; global test setup for matchers/mocks.
- **Integration/smoke (later):** small number of Playwright/Happy-path flows (create/edit/publish).
- Coverage:
- Enforce thresholds at the package level; exclude generated/fixture code; surface a coverage report artifact.
- Linting/formatting:
- ESLint (flat config) with TypeScript, React, and **jsx-a11y**; Prettier for formatting; consistent import ordering.
- Fast feedback:
- Watch mode locally; pre-push hooks run **lint + typecheck + tests (changed files)**.
- CI scaffolding (conceptual):
- Separate jobs for install/cache, lint/typecheck, unit/component tests, and (later) e2e smoke; cache node modules; artifact uploads for coverage.
---
## Phase 3 — Correctness Sweep
**How:**
- Dependency hygiene:
- Align UI toolkit imports to current major; remove legacy namespaces; verify peer-dep ranges; lock versions for determinism.
- Type safety:
- Replace broad `any`/suppressed regions with minimal interfaces and discriminated unions where helpful.
- Introduce a “strict mode budget”: upgrades gated by keeping type errors at zero.
- State/data flows:
- Standardize async lifecycles (`idle/loading/success/error`) and empty states; unify error objects (message + code + recoverability).
- Image/media safety:
- Enforce text alternatives policy; handle failed loads with fallbacks; guard upload constraints (size/type).
- Cross-browser sanity:
- Run a small matrix (Chromium/WebKit/Firefox) in CI for a smoke render test.
---
## Phase 4 — Accessibility Baseline
**How:**
- Semantics & landmarks:
- Use structural regions (header/nav/main/footer) and a skip-to-content anchor; ensure a single H1 per view.
- Keyboard & focus:
- Trap focus in modals/popovers, restore focus on close, visible outlines; consistent Tab/Shift+Tab order; Esc closes transient UI.
- Names/roles/states:
- Deterministic accessible names for controls; toggles use `pressed` state; forms have programmatic labels and error/help text wiring.
- Live updates & motion:
- Polite live region for async progress; respect `prefers-reduced-motion`; avoid seizure risk (no flashing).
- Testing:
- Automated axe checks in component tests for critical views; a short manual keyboard checklist per protected journey.
---
## Phase 5 — UX & IA Touch-up
**How:**
- Navigation model:
- Clarify primary vs. secondary navigation; ensure routes map cleanly to the data model (current blog scope).
- Consistency system:
- Define tokens for spacing/typography/contrast; standardize button/empty/error patterns; unify iconography.
- Editor ergonomics:
- Deterministic toolbar enable/disable; clear state for draft vs. published; undo/redo affordances; autosave with visible status.
- Content lifecycle:
- Uniform toasts/banners for success/failure; retry affordances on transient failures; protective confirms on destructive actions.
---
## Phase 6 — Data Model Extension: Multiple Blogs per Name
**How:**
- Domain relationships:
- **Name** (account) 1..N **Blog**; **Post** 1..1 **Blog**; keep **Post → Blog** immutable after creation (prevents cross-blog drift).
- Identifiers & URLs:
- Introduce stable blog handles (human-friendly, unique per name) that compose into routes; add collision policy and normalization rules.
- State management:
- Represent “current blog” as first-class app state; selector scoping so lists/metrics derive from the active blog.
- Migrations:
- Backfill: create a default blog per name and attach historical posts; record migration version on the data root to allow repeatable ops.
- UX:
- Blog switcher in global nav; “create blog” guided flow with handle validation and preview; clearly scoped creation forms.
- Indexing & constraints (backend/API expectations):
- Unique composite index (name, blog handle); foreign key from posts to blog id; server rejects cross-blog updates.
---
## Phase 7 — Permissions Model: Shared Blogs
**How:**
- Role model (lean start):
- **Owner** (full control), **Editor** (edit any post, manage drafts), **Author** (create/edit own posts), **Viewer** (public, implicit).
- Invitations:
- Tokenized invite flow with expiration; acceptance binds a name to a role on a blog; owner can revoke.
- Enforcement:
- All write endpoints check role → operation matrix; UI gates controls (disable/hide) but never trusts the client.
- Attribution & activity:
- Store author id and updated-by on posts; optional lightweight activity log for edits/publishes; surface attribution in UI.
- Conflicts:
- Last-writer-wins with ETag/if-match semantics or revision numbers; reject stale writes; small conflict UI with diff view (stretch goal).
---
## Phase 8 — Performance & Resilience
**How:**
- Rendering:
- Keep lists virtualized; hydrate only whats visible; memoize high-churn components; defer non-critical work with idle callbacks.
- Network:
- Structured fetch layer with timeouts, abort support, and retry/backoff on idempotent GETs; idempotency keys on create/update to prevent dupes.
- Perceived speed:
- Optimistic UI where safe (e.g., draft saves); skeletons/spinners with progress semantics; prefetch likely next views.
- Failure handling:
- Standard error surface (banner/toast + inline details); retry path; offline detection with queued drafts.
- Metrics:
- Track Web Vitals and action latencies; set budgets (e.g., LCP, INP) and regressions fail CI perf checks (can be a nightly).
---
## Phase 9 — Internationalization & Theming Consistency
**How:**
- Strings:
- Centralize UI strings under a namespace scheme; ensure interpolation is safe; default English bundle with stubs for others.
- Formatting:
- Use Intl APIs for dates/numbers/plurals; avoid manual formatting; prepare RTL-safe layouts (logical properties).
- Themes:
- Single source for color/spacing/typography tokens; verify contrast programmatically; support `prefers-color-scheme`.
---
## Phase 10 — Observability, Security & Release Readiness
**How:**
- Observability:
- Error boundaries with user-friendly fallback views; structured client logs (level, code, context) routed to a collector; anonymized action breadcrumbs for repro.
- Security posture:
- Input validation at boundaries; sanitize rich text; content-security-policy hardening; permission checks at every write; least-privilege tokens.
- Migrations & backups:
- Versioned migrations with idempotent steps; pre-prod dry run; exportable backup of critical entities; rollback procedure.
- Release discipline:
- SemVer tags; changelog; migration notes; upgrade guide; “go/no-go” checklist (tests green, a11y checks pass, metrics within budget).
---
## Cross-cutting Technical Principles
- **Contracts first:** Define data and role contracts before wiring UI; document them in the repo and tests.
- **Small vertical slices:** Ship changes end-to-end (schema → API → state → UI → tests) in thin increments.
- **One source of truth:** Centralize constants (roles, statuses, limits) and import them everywhere.
- **Guardrails by default:** Lint/type/test/a11y checks run locally and in CI; merges require green gates.
- **Telemetry with restraint:** Collect only whats necessary for quality; feature-flag any new collection.
---
## Assumptions & Risks
- We control both client and API contracts (or can negotiate them).
- Current data fits a clean migration to per-name default blogs without manual reconciliation.
- Editor content can be sanitized without breaking required formatting.
- Minimal E2E is enough for confidence; we can expand later if flakiness appears.
+364
View File
@@ -0,0 +1,364 @@
**TL;DR:** Below is a phase-by-phase implementation guide: what libraries well use, what kinds of tests well write, what contracts/functions well define, and how well validate each step. Still high-level—no filenames or code dropped—yet detailed enough to steer day-to-day work.
---
# Q-Blog → 1.0.0 Implementation Plan (Technical, High-Level)
## Phase 0 — Orientation & Quality Bar
**Libraries / tools**
- Issue labels & PR templates (tracker-native).
- Metrics: Web Vitals (`web-vitals`), synthetic a11y checks (`axe-core` via CI).
**Artifacts**
- **Quality charter**: SLOs (e.g., “save post success ≥ 99.9%”), accessibility bar (keyboard-only paths pass), perf budgets (LCP/INP).
- **Protected journeys** list: _read post, create post, edit post, manage blogs_.
**Validation**
- Signed-off charter; journeys ratified in a short doc.
---
## Phase 1 — Project Docs & Working Agreements
**Libraries / tools**
- Diagrams as code (Mermaid in docs renderer).
- Conventional commits (tooling optional).
**Artifacts**
- **Architecture map** (UI ↔ state ↔ API), **Testing pyramid**, **A11y standard**, **Contributing rules**, **Changelog categories**.
- **Decision Record template** (context → options → decision → impact).
**Validation**
- New contributor dry run: “can run + test + understand contracts in <30 min.”
---
## Phase 2 — Test Harness & Linting Baseline
**Libraries**
- Unit/Component: **Vitest**, **@testing-library/react**, **@testing-library/user-event**, **@testing-library/jest-dom**.
- A11y lint/checks: **eslint-plugin-jsx-a11y**, **axe-core** or **jest-axe**.
- Mocking: **MSW** (Mock Service Worker) for API.
- Types: TypeScript strict mode.
- Lint/format: **eslint** (flat config), **@typescript-eslint** plugin, **prettier**.
**Key setup**
- Global test setup (RTL matchers, MSW server, clock/timers).
- Coverage thresholds (e.g., 80/70/70 lines/branches/functions to start).
- CI lanes: install/cache → typecheck → lint → unit/component → (later) e2e smoke.
**Tests to seed**
- Smoke renders: home, post list, editor screen.
- Tiny interaction: typing in editor, enabling/disabling toolbar control, save button disabled until valid.
**Validation**
- Green **typecheck + lint + tests** on clean checkout; coverage reports generated.
---
## Phase 3 — Correctness Sweep
**Libraries**
- RTK ecosystem: **@reduxjs/toolkit** (already present), **RTK Query** for data-fetching (adds caching, retries, invalidation).
- Schema/runtime validation: **Zod** (form/io validation) **and** `zod-to-json-schema` for docs **or** **AJV** if we align with JSON Schema like Q-Chess. (Pick one; see “Contracts” below.)
**Contracts / functions**
- **Fetch layer** with cancellation & retries:
- `fetchJson<T>(req: Request, opts): Promise<Result<T>>`
- `withTimeout(controller, ms): AbortSignal`
- **Async state convention**: `{ status: 'idle'|'loading'|'success'|'error', data?, error? }`.
- **Error object**: `{ code: string; message: string; recoverable: boolean }`.
- **Validation**:
- For Zod: `const PostDto = z.object({ id: z.string(), ... })``parse(response)`.
- For AJV: compile JSON Schemas once; validate per response.
**Work items**
- Remove legacy UI imports (v4 → v5), delete `ts-nocheck` sections by adding minimal types.
- Replace `any` in reducers/selectors and editor props with discriminated unions & precise payloads.
- Normalize empty/loading/error states for all remote views.
**Tests**
- Contract tests: MSW returns both **valid** and **invalid** payloads → validation errors surface as user-friendly messages.
- Reducer tests: transitions for each thunk/query state.
**Validation**
- No suppressed type regions; “known issues” list empty; core routes never hard-crash on bad data.
---
## Phase 4 — Accessibility Baseline
**Libraries**
- A11y testing: **jest-axe** or **axe-core** integration; **testing-library** queries by role/name.
- Focus management: rely on MUIs **Dialog/Popover** focus traps; add **focus-trap** only where needed.
**Contracts / behaviors**
- **Landmarks**: header/nav/main/footer; **Skip link** available and visible on focus.
- **Focus policy**: focus moves to first interactive element on open; returns to invoker on close.
- **Names/roles/states**: toggles expose `aria-pressed`; inputs have programmatic labels; errors are linked with `aria-describedby`.
- **Live regions**: polite updates for “saving…/uploaded/failed”.
- **Motion/contrast**: respect `prefers-reduced-motion`; enforce contrast tokens.
**Tests**
- Keyboard paths for protected journeys (Tab/Shift+Tab order, Escape close).
- axe checks for key pages (no critical violations).
- Live region announces long-running operations (assertions via `toHaveAccessibleDescription` or role queries).
**Validation**
- Keyboard-only success across protected journeys; automated checks pass with zero criticals.
---
## Phase 5 — UX & IA Touch-up
**Libraries**
- Router (current choice): ensure route guards & prefetch hooks supported.
- Toasts/banners: lightweight (e.g., notistack or MUI Snackbar pattern).
**Contracts / functions**
- **Design tokens**: spacing, radius, typography, color roles (text, surface, brand) centralized.
- **Common UI patterns**:
- Empty state contract: `{ icon?, title, body, primaryAction?, secondaryAction? }`
- Error surface: banner + inline guidance; all errors have recovery action.
- **Editor ergonomics**:
- Toolbar state machine: `computeToolbarState({ selection, schema }) → { boldEnabled, … }`
- Draft lifecycle: `autosaveDraft(postId, content) → { savedAt }` with debounce and “last saved” indicator.
**Tests**
- Token snapshots for theme roles; editor toolbar enable/disable based on selection.
- Error/empty states render consistently across pages (table-driven tests).
**Validation**
- Navigation clarity (first-click discoverability); zero dead ends; consistent affordances.
---
## Phase 6 — Multiple Blogs per Name
**Libraries**
- State & data: continue **RTK Query** (cache per blog key).
- Validation: same as Phase 3 (Zod/AJV).
**Domain model**
- **Name** (account) 1..N **Blog**; **Post** 1..1 **Blog** (immutable relationship post-creation).
- **Identifiers**:
- Blog handle: lowercase, slug rules; unique per **Name**; stored canonical form.
- URL composition: `/{name}/{blogHandle}/…` (conceptual, router-agnostic).
**Functions / contracts**
- `listBlogs(name): Promise<Blog[]>`
- `createBlog(input: { handle; title; visibility }): Promise<Blog>`
- `switchBlog(handle): void` (updates app-scoped “current blog”)
- `listPosts(blogId, filters)`, `createPost(blogId, input)`
- **Migration**: `backfillDefaultBlogs(): MigrationResult` (idempotent: skip if blog exists)
**Tests**
- Migration test with legacy dataset → posts appear under default blog.
- Router/selector scoping: when switching blogs, lists & counts update without leaking data.
- Handle validation: rejects collisions & invalid slugs; normalizes input.
**Validation**
- Users can create/switch blogs; all lists/summaries are scoped; legacy users retained seamlessly.
---
## Phase 7 — Shared Blogs (Collaboration)
**Libraries**
- Role/permission helpers: in-house constants; optional **casl** if we want DSL-like permissions (not required).
**Role model**
- **Owner**, **Editor**, **Author**, **Viewer** (implicit).
- Operation matrix:
- Owner: all
- Editor: edit any post, manage drafts
- Author: CRUD own posts
**Functions / contracts**
- Membership:
- `inviteMember(blogId, name, role): InviteToken`
- `acceptInvite(token): Membership`
- `removeMember(blogId, name): void`
- `listMembers(blogId): Membership[]`
- Authorization guard:
- `can(op: 'create'|'edit'|'publish'|'manageMembers', ctx: { user, blog, post? }): boolean`
- Attribution:
- Post metadata: `{ createdBy, updatedBy, updatedAt, revision }`
- Concurrency:
- `savePost(postId, input, { ifMatchRevision }): { revision }` → 409 on stale; client shows resolve UI.
**Tests**
- Permission matrix table tests (unit): each role × operation result.
- API guard tests via MSW: simulate forbidden responses, assert UI disables and surfaces denial.
- Concurrency tests: stale write attempt returns conflict → user offered resolve path.
**Validation**
- Owner can manage access; editors/authors can work within role; no unauthorized writes pass.
---
## Milestone Updates
- v0.1.x — Multiblog foundations and quality baselines delivered.
- v0.2.0 — Wiki Mode canonical selection shipped (owner/whitelist/blacklist; canonical dedupe across Names in feed, favorites, subscriptions, and post view). Settings cache added for efficient owner/settings resolution.
## Phase 8 — Performance & Resilience
**Libraries**
- **react-virtuoso** or equivalent (already used) for long lists.
- Data layer retries/backoff: **RTK Query**s retry plugin or custom wrapper.
- RUM metrics: **web-vitals** + lightweight sender.
**Functions / policies**
- **Optimistic updates** where safe (draft save, title edits) with rollback on failure.
- **Retry policy**: GETs exponential backoff; POST/PUT guarded by idempotency keys where applicable.
- **Abortable fetch**: timeouts and user-initiated cancel for uploads.
- **Prefetching**: on-hover/visible prefetch of likely next routes and data keys.
**Tests**
- Latency injection with MSW to verify spinners/skeletons and cancel/abort behaviors.
- Optimistic update rollback test: forced server failure restores previous UI state.
**Validation**
- Subjective feel: interactions seem instant; objective: Web Vitals within budget; error paths recover without data loss.
---
## Phase 9 — Internationalization & Theming Consistency
**Libraries**
- **react-i18next** (already used in sister projects).
- Intl APIs for date/number/plural; fallback polyfills if needed.
**Functions / contracts**
- `t('namespace:key', { vars })` usage with explicit, descriptive keys.
- Currency/number/date formatters as wrapper utilities to standardize formatting.
- Theme tokens as a single source of truth; contrast checks (scripted) against WCAG AA.
**Tests**
- Snapshot tests for key screens in both light/dark with token diffs.
- Formatting tests for pluralization and number/date locales.
**Validation**
- Strings externalized; theme passes automated contrast check; RTL-safety spot check on core screens.
---
## Phase 10 — Observability, Security & Release Readiness
**Libraries**
- Error tracking: **Sentry** (or equivalent) with source maps.
- Sanitization: **DOMPurify** for any HTML serialization/render of editor output.
- CSP guidance (server-side; document the policy).
**Functions / policies**
- **Error boundaries**: route-level and editor-level with friendly fallback and “copy details”.
- **Client logging**: `logClientError({ code, message, context })` throttle; opt-in breadcrumbs (sanitized, no PII).
- **Security checks**:
- Validate all inputs client-side with Zod/AJV before sending.
- Sanitize rich text on ingest and display; allowlist for marks/nodes.
- Permission checks every write path; never trust client-side hide/disable alone.
- **Migrations**:
- Versioned, idempotent; dry-run flag; record migration status.
- **Release discipline**:
- Version bump + changelog; migration notes; upgrade guide; go/no-go checklist (green gates + budgets met).
**Tests**
- Error boundary renders fallback on thrown child component; telemetry is called with redacted payload.
- XSS tests: paste malicious payload → sanitized output (no script execution) both save and render paths.
- Migration dry-run/effects tests with sample datasets.
**Validation**
- Crashes captured with actionable context; no stored XSS vectors; release checklist green.
---
## Cross-Cutting Standards
**Contracts-first**
- Pick **Zod** _(form/input + client response validation, type inference)_ or **JSON Schema + AJV** _(cross-project parity with Q-Chess)_.
- If Zod: generate JSON Schema for docs via `zod-to-json-schema`.
- If AJV: generate TypeScript types via `json-schema-to-ts`.
- Maintain a single **roles/status/constants** module imported across UI, data, and tests.
**Data access layer**
- Prefer **RTK Query** for cache, memoized selectors, retries, and invalidation; treat queries/mutations as the one path to remote state.
**State shaping**
- Store UI state distinct from server cache; selectors derive view models (small pure functions, unit-tested).
**Security posture**
- Default-deny in permission checks; treat editor input as untrusted; output sanitize on every render path.
**Testing pyramid (target distributions)**
- Unit (\~60%): pure utils, selectors, reducers, permission guards.
- Component (\~30%): screens/components with RTL (+ axe).
- Integration/E2E (\~10%): happy-path create/edit/publish; blog switch; invite/accept.
**CI**
- Node LTS matrix (current + previous).
- Caching for package manager.
- Artifacts: coverage report, axe report, Web Vitals synthetic (if applicable), build output for preview.
---
## Example Function Signatures (illustrative, no filenames)
```ts
// Networking & validation
type Result<T> =
| { ok: true; data: T }
| { ok: false; error: { code: string; message: string; recoverable: boolean } };
function fetchJson<T>(
input: RequestInfo,
init?: RequestInit & { timeoutMs?: number },
): Promise<Result<T>>;
// Permissions
type Role = 'owner' | 'editor' | 'author';
function can(
op: 'create' | 'edit' | 'publish' | 'manageMembers',
ctx: { role: Role; userId: string; postOwnerId?: string },
): boolean;
// Blog management
function createBlog(input: {
handle: string;
title: string;
visibility: 'public' | 'private';
}): Promise<Result<Blog>>;
function inviteMember(blogId: string, name: string, role: Role): Promise<Result<{ token: string }>>;
function savePost(
postId: string,
input: PostInput,
opts: { ifMatchRevision: number },
): Promise<Result<{ revision: number }>>;
```
---
## Completion Bars (what “done” looks like per category)
- **Correctness:** zero suppressed type regions; known issues = empty; core flows never hard-crash.
- **Testability:** green typecheck/lint/tests on clean checkout; coverage trend up and stable; fast local watch.
- **Accessibility:** keyboard-only paths pass; axe shows no criticals; live region & focus rules verified.
- **Usability:** consistent patterns for empty/error; editor ergonomics predictable; no dead ends.
- **Security:** sanitized rich text; permission matrix enforced server- and client-side; no privilege escalation paths.
- **Performance:** web vitals within budget; optimistic updates/abortable fetches; graceful failure/retry.
---
**Assumptions & Risks:**
We can evolve API contracts to support multi-blog and roles; legacy data maps cleanly to default blogs; Slate output can be sanitized without losing required formatting; e2e scope remains minimal to avoid CI flake—expand only if signals require it.
+158
View File
@@ -0,0 +1,158 @@
**TL;DR:** Heres a non-technical, stepwise roadmap from todays v0.0.0 to a polished 1.0.0. It front-loads docs + testing, cleans correctness/a11y/UX, then adds **multi-blog per name**, and finally **shared blogs**. Each phase has clear outcomes and “done” checks.
---
# Q-Blog → 1.0.0 Roadmap (non-technical)
## Phase 0 — Orientation & Quality Bar (now)
**Goal:** Align on what “top quality” means.
- Define success metrics: a11y baseline, crash/error rate, perf feel (snappy interactions), test coverage targets, doc set.
- Pick a short list of user journeys to protect (e.g., read posts, create post, edit post, manage blogs).
**Done when:**
- We have a one-pager of goals/metrics and the protected journeys list.
---
## Phase 1 — Project Docs & Working Agreements
**Goal:** Make the project legible and predictable.
- Author concise docs (what/why/how): Overview, Architecture sketch, Testing approach, Accessibility expectations, Contributing, Changelog.
- Record a few early decisions (editor, state, data shapes) as short decision notes.
**Done when:**
- New contributor can read docs and understand how to run, test, and contribute—without asking questions.
---
## Phase 2 — Test Harness & Linting Baseline (no feature work yet)
**Goal:** Establish guardrails to catch regressions early.
- Stand up unit/component test harness and a minimal test pyramid strategy (unit > component > a few integration).
- Add accessibility and code-quality lint passes (no rules list yet—just the intent).
**Done when:**
- “Run tests” and “lint” are routine and green on a clean checkout.
- We can write a simple render test for a page without friction (proof the harness works).
---
## Phase 3 — Correctness Sweep (stability before polish)
**Goal:** Remove known hazards and obvious defects.
- Replace legacy/broken imports, remove blanket type-ignores, fix crashing edge cases and flaky flows.
- Normalize basic data flows (loading/error/empty states) so the app never “mystery fails.”
**Done when:**
- No known red flags remain (e.g., invalid imports, suppressed type errors, missing alt text).
- Core pages load and operate consistently on a fresh profile.
---
## Phase 4 — Accessibility Baseline (foundational)
**Goal:** Make the app operable by keyboard and assistive tech.
- Landmarks & navigation: header/nav/main/footer + skip link.
- Focus management for dialogs/menus/editor; visible focus rings.
- Text alternatives, form labels, live region for async work; motion/contrast preferences respected.
**Done when:**
- Keyboard-only journeys succeed; basic automated checks are clean; a short manual checklist passes.
---
## Phase 5 — UX & Information Architecture Touch-up
**Goal:** Reduce friction and ambiguity before adding new power.
- Clear page hierarchy and navigation; consistent button labels and empty/error states.
- Editor ergonomics: predictable toolbar states, undo/redo clarity, draft/publish feedback.
**Done when:**
- The protected journeys feel obvious and forgiving (you cant get stuck; errors are actionable).
---
## Phase 6 — Data Model Extension: **Multiple Blogs per Name**
**Goal:** Allow one “name” to own several separate blogs.
- Concepts (non-technical): **Name** owns 0..N **Blogs**; **Post** belongs to exactly one **Blog**.
- Identity: pick a human-friendly blog identifier (e.g., “name + blog handle”) that works well in URLs.
- UX: a simple “Blog switcher” and “Create new blog” flow; list scoped to current blog.
- Migration: existing content becomes Blog #1 for each name.
**Done when:**
- A user can create/switch blogs and see posts scoped appropriately.
- Existing users posts are intact under their first blog without surprises.
---
## Phase 7 — Permissions Model: **Shared Blogs**
**Goal:** Enable collaboration on a blog.
- Roles (simple first): **Owner**, **Editor**, **Author** (can post), **Viewer** (implicit, public).
- Invitations & membership management: add/remove collaborators; show who has access.
- Edit safeguards: clear attribution on posts, activity notes on updates (lightweight).
**Done when:**
- Owner can invite/remove collaborators.
- Collaborators can create/edit posts according to role, and non-members cannot.
---
## Phase 8 — Performance & Resilience
**Goal:** Keep interactions fast and reliable as features grow.
- Perceived speed: snappy navigation, optimistic updates where safe, progress indicators for async operations.
- Robustness: graceful offline/slow-network handling for key actions; safe retries on saves/uploads.
**Done when:**
- Core actions “feel instant” with clear feedback, and temporary failures recover without data loss.
---
## Phase 9 — Internationalization & Theming Consistency (light)
**Goal:** Make UI text and visuals consistent and adaptable.
- Externalize UI strings; ensure headings/labels follow a consistent style.
- Validate light/dark themes for contrast and motion preferences.
**Done when:**
- Strings are centralized for future i18n; themes meet contrast/motion guardrails.
---
## Phase 10 — Observability, Security & Release Readiness
**Goal:** Ship 1.0 with confidence.
- Error boundaries and user-friendly error pages; minimal analytics/telemetry (opt-in) to catch crashes.
- Basic security posture review (inputs, uploads, permission checks).
- Backups/migration notes; upgrade guide; crisp release notes.
**Done when:**
- We can trace and act on production errors; permissions are enforced in all write paths; the “1.0 checklist” is fully checked.
---
# Why this order?
- **Docs + tests first** reduce rework (catch issues as we fix).
- **Correctness → a11y → UX** ensures polish builds on stable, inclusive foundations.
- **Multi-blog** precedes **Shared blogs** so collaboration lands on the right data model.
- **Perf/Resilience** and **Release** bookend features to keep the app trustworthy.
---
## Acceptance bars (numbers keep us honest)
- Accessibility: keyboard-only passes for protected journeys; automated checks free of critical issues.
- Reliability: zero known crashers in core flows; error states always show recovery actions.
- Testability: green test/lint on clean checkout; steadily rising coverage (well agree targets when we stand up the harness).
- Usability: no dead ends; consistent labels; empty/error states always informative.
---
## On your two feature asks
- **Multiple blogs per name:** 👍 Fits naturally with Q-Blogs goals and unlocks better organization. Well keep identifiers human-readable and migrate existing content seamlessly.
- **Shared blogs:** 👍 A strong, later-phase addition. Well start with a lean role model and keep invites/management simple to avoid UX sprawl.
+215
View File
@@ -0,0 +1,215 @@
# Q-Blog — Project Instructions (1.0 Track)
_Generated 2025-08-16 23:37Z_
> Default instructions for all Q-Blog chats. Optimize for **correctness, accessibility, collaboration, and a focused writing UX**. Keep answers decisive and artifact-oriented.
---
## 1) Intent
Q-Blog is a modern blogging workspace for **individuals and small teams**. Writers can own **multiple blogs under one Name**, and enable **Shared Blogs** with roles (Owner/Editor/Author). North star: **inclusive, predictable publishing** with a resilient, testable UI.
---
## 2) Core flows & stable components (props)
**Flows:** Read → Draft → Edit → Publish → Manage Blogs → Collaborate.
**BlogSwitcher** — select active blog; scopes all lists/forms.
```ts
activeBlog: BlogRef | null
blogs: BlogSummary[]
onCreateBlog: () => void
onSelectBlog: (blog: BlogRef) => void
disabled?: boolean
```
**PostEditor** — Slate-based editor with autosave and preview.
```ts
mode: 'create' | 'edit'
blog: BlogRef
value: EditorState
onChange: (s: EditorState) => void
onSaveDraft: (s: EditorState) => Promise<void>
onPublish: (input: PublishInput) => Promise<void>
canPublish: boolean
status: 'idle'|'saving'|'publishing'|'error'|'success'
```
**PostList** — virtualized list scoped to active blog.
```ts
blog: BlogRef
filters: PostFilters
onOpenPost: (id: Id) => void
```
**MembersPanel** (shared blogs).
```ts
blog: BlogRef
members: Membership[]
onInvite: (name: NameRef, role: Role) => Promise<void>
onRemove: (name: NameRef) => Promise<void>
currentUserRole: Role
```
**Header/Nav** — global actions.
```ts
onOpenMembers?: () => void
onCreatePost: () => void
onSwitchBlog: () => void
unreadCount?: number
```
**Stable props** change only via coordinated refactor with tests & docs.
---
## 3) Tech & repo posture
React 18 + TypeScript (strict) + Vite; **MUI v5**; Slate editor; **Redux Toolkit + RTK Query**; i18next (strings).
Testing: **Vitest + RTL + user-event + MSW + jest-axe**.
Config parity across dev/test/build (aliases, JSX, TS). Alias: **@ → src**. Layout: `src/`, `tests/`, `docs/`, `.gitea/`.
---
## 4) Behavior contracts
**Blog scoping & routing**
- Every view is scoped to **active blog**. Route params encode scope (conceptually `/{name}/{blog}/...`). Changing blog updates lists/forms without leaks.
**Editor & autosave**
- Debounced autosave writes drafts with **polite live region** announcements. Clear “Last saved” time; explicit **Publish** pathway. Toolbar enable/disable is deterministic from selection.
**Post lifecycle**
- States: `draft | scheduled | published | archived`. Idempotent publish; show attribution (`createdBy`, `updatedBy`, `updatedAt`).
**Media**
- Client validates type/size; shows upload progress; failed uploads are retryable; all `<img>` have alt (or `alt=""` if decorative).
---
## 5) Accessibility (A11y)
- Landmarks: header/nav/main/footer; **Skip link** focuses main. One H1 per route.
- Keyboard: all controls reachable; **Esc closes** dialogs/popovers and **restores focus**.
- Names/roles/states: explicit labels; toggles expose `aria-pressed`.
- Live regions: succinct progress for save/publish/upload; no spam.
- Preferences: honor `prefers-reduced-motion`; enforce contrast tokens.
- Tests: axe checks for key pages; keyboard journey tests for protected flows.
---
## 6) Data, permissions & contracts
- **Entities**: Name 1..N Blog; Post 1..1 Blog (immutable link). (`nameId`,`blogHandle`) unique.
- **Roles**: Owner (all), Editor (edit any, manage drafts), Author (own posts). Deny by default.
- **RTK Query** is the only remote path; queries/mutations define cache keys by blog.
- **Validation**: Prefer **Zod** for forms & responses (or JSON Schema + AJV if aligning w/ Q-Chess); failed validation → friendly error with retry path.
- **Concurrency**: optimistic where safe; use revision/ETag to detect conflicts; show resolve UI on 409.
---
## 7) Testing standards
- Register matchers once in `tests/setup.ts`. Use **accessible queries** (`getByRole`, `getByLabelText`).
- Unit: selectors, reducers, guards (`can()`), formatters, sanitizers.
- Component: editor toolbar & autosave, blog switch scoping, lists (virtualized), dialogs focus.
- A11y: axe smoke on read/draft/manage; keyboard-only journeys.
- MSW: success + error + invalid payload paths. Deterministic tests; fake timers for debounce.
- Coverage gate per package; exclude fixtures/build artifacts.
---
## 8) Performance & resilience
- Virtualize long lists; memoize high-churn components.
- Abortable fetch with timeouts; retry/backoff for GETs; idempotency keys for create/update.
- Perceived speed: skeletons & optimistic UI (draft saves); defer non-critical work to idle.
- Offline-friendly drafts: queued writes with clear status and conflict handling.
---
## 9) TypeScript posture
Strict mode, zero `any` in public props; no `@ts-nocheck`. Prefer discriminated unions for async/status shapes. Treat red squiggles as **must fix** before PR.
---
## 10) Security & content safety
- Sanitize editor output **on save and render** (DOMPurify allowlist). Never render untrusted HTML directly.
- Validate inputs at boundaries; never trust client-visible state for authorization (server checks required).
- Error boundaries: route-level and editor-level with friendly fallback + “copy details” action.
- Minimal telemetry for quality (crashes, vitals); no PII.
---
## 11) Internationalization & theming
Centralize strings; use Intl for dates/numbers; prep RTL-safe layouts. Theme tokens (color/spacing/typography) audited for WCAG AA; respect `prefers-color-scheme`.
---
## 12) Docs & decision hygiene
Docs live in `docs/`. Keep: **ARCHITECTURE**, **TESTING**, **ACCESSIBILITY**, **SECURITY**, **USER_JOURNEYS**, **GLOSSARY**, **RISKS**, **ROADMAP_DEPENDENCIES**, **DECISIONS/** (short ADRs). Update docs when behavior or contracts change.
---
## 13) Delivery & workflow (assistants)
- Prepare files locally; share **download links** (send full files once edits exceed a few lines). Keep a resendable copy.
- Prefer thin **vertical slices** (schema→API→state→UI→tests→docs). Include acceptance notes + quick verify.
- If assumptions are needed, state them briefly and proceed with a concrete artifact.
---
## 14) Single source of truth (constants)
Centralize shared constants and import everywhere: roles, statuses, limits (upload sizes), blog handle rules, debounce durations, routes, a11y labels. **UI, data, and tests** must reference the same values.
---
## 15) Versioning & releases
SemVer with human changelogs; each release includes notes, migration steps, and quick verify. CI gates: typecheck, lint (jsx-a11y), unit/component tests, axe smoke, coverage, build.
---
## 16) Common error → action
- **MUI v4 import fails** → switch to `@mui/material` / `@mui/icons-material`.
- **Unexpected HTML execution** → sanitize on save+render; add unit tests with known XSS vectors.
- **Data shape mismatch** → validation failed: show friendly error; log redacted details; fix schema or endpoint.
- **Keyboard trap** → ensure focus trap + restore; add test.
- **Leaked posts across blogs** → check scoping (selector/query keys) and route params; add failing test then fix.
---
## 17) Checklists
**Before sending anything**
- [ ] No red underlines; strict TS passes.
- [ ] Vite build + **Vitest green** (unit & component); axe smoke passes (no criticals).
- [ ] Dev/test/build configs match (aliases, JSX, TS options).
- [ ] Public props typed; callers match; no `any` in props.
- [ ] Landmarks + labels; toggles use `aria-pressed`; live regions only when meaningful.
- [ ] Lists scoped to active blog; routes stable and shareable.
- [ ] Editor: autosave status visible; publish path explicit; sanitized output.
**Thin vertical delivery**
- [ ] Code + tests + docs move together.
- [ ] Include acceptance criteria and quick verification steps.
- [ ] If any contract changes, add/adjust a short Decision Record.
+53
View File
@@ -0,0 +1,53 @@
# Q-Blog — Quality Charter (Patch 0)
_Generated 2025-08-16 23:43Z_
## Purpose
Define the quality bar for Q-Blog 1.0 and the gates that every change must pass. This charter governs scope, acceptance, and sign-off.
## North Star
**Inclusive, predictable publishing** with a resilient, testable UI.
## Protected User Journeys
(See `docs/USER_JOURNEYS.md`; mirrored here for convenience.)
1. Read posts 2) Create post 3) Edit post 4) Manage blogs 5) Collaborate
## Service Level Objectives (SLOs)
- **Reliability:** Draft save success ≥ **99.9%** P7D; crash-free sessions ≥ **99%**.
- **Accessibility:** Keyboard-only paths pass for protected journeys; **axe** critical violations = **0**.
- **Performance:** P75 **LCP < 2.5 s**, **INP < 200 ms**, **CLS < 0.1** on modern desktop; mobile budgets P75 LCP < 3.2 s, INP < 250 ms.
- **Usability:** No “dead-end” screens; each error has a visible recovery action.
- **Security:** Sanitized rich text on save and render; deny-by-default for writes.
- **Testability:** Coverage gates start **80/70/70** (lines/branches/functions), trend to **90/80/80** by RC.
## Acceptance Gates (per PR)
- Typecheck strict **green**; ESLint (incl. **jsx-a11y**) **green**; Vitest **green**.
- No new `any` in public props; no `@ts-nocheck` or blanket disables.
- For UI changes: keyboard path verified; aria names/states present; screenshots or notes for key states.
- For data/wire changes: request/response validated (Zod or AJV); error handling user-friendly.
- Docs updated if behavior/contract changes (ADR or section update).
## Measurement & Observability
- **Local/CI:** Web Vitals synthetic where feasible; axe in tests; coverage artifacts exported in CI.
- **Runtime (opt-in minimal):** Crash/error codes, action breadcrumbs (no PII), vitals sample. Redaction enforced.
## Roles & Sign-off
- **Quality Owner** (temporary): Project lead (or delegate) confirms gates pass.
- **A11y Owner:** Reviews keyboard + axe results for protected journeys.
- **Security Owner:** Signs off on sanitization and permission-sensitive changes.
## Out of Scope (1.0)
- Fine-grained per-post ACLs; end-to-end encryption; advanced analytics.
## Change Control
Any change to this charter or to protected journeys requires an ADR (see template) and project sign-off.
+65
View File
@@ -0,0 +1,65 @@
# Release Flow (Gitea + local scripts)
This repo uses local scripts to cut releases and create uploads in Gitea. CI stays marketplace-free and works with your self-hosted runner.
## One-time
- Ensure env vars are exported in your shell (or place them in `.gitea.env` and `source` it):
```bash
export GITEA_BASE_URL=https://gitea.qortal.link
export GITEA_TOKEN=... # personal access token with repo scope
export OWNER=greenflame089
export REPO=q-blog
```
## Phase wrap example (Phase 0 → v0.0.1)
1. Bump & tag:
```bash
bash scripts/release/bump-and-tag.sh 0.0.1 "Phase 0 — Orientation & Quality Bar"
```
2. Create archives (source only by default; pass `--with-build` later in Phase 1+):
```bash
bash scripts/release/build-archive.sh
# or with a production build attempt:
# bash scripts/release/build-archive.sh --with-build
```
3. Before pushing: format and checks
```bash
npm run format:fix
npm run typecheck
npm run lint:full
npm run test:run
```
4. Create/Update Gitea release and upload zips for the current version only:
```bash
bash scripts/release/create-gitea-release.sh 0.0.1 \
--title "Phase 0 — v0.0.1" \
--notes docs/RELEASE_NOTES_v0.0.1.md \
--branch update
```
The script will:
- create the release if missing (or patch it if it exists),
- upload only `release/*-v0.0.1-*.zip` artifacts by default (current version),
- print the Release URL on success.
### Notes
- We intentionally **skip a TypeScript build by default** in Phase 0 to avoid coupling to app code. Use `--with-build` after the Phase 1 correctness sweeps.
- Artifacts are placed in `release/`:
- `*-src.zip` is always created.
- `*-dist.zip` is created if `dist/` exists (when `--with-build` is used).
### Tips
- To override which files are uploaded (e.g., re-upload a specific dist), pass `--assets 'release/*-v0.2.0-dist.zip'`.
- If you have older zips in `release/`, the default upload pattern avoids attaching them.
+34
View File
@@ -0,0 +1,34 @@
# Q-Blog — Multi-Blog Feature Notes (Patch)
Date: 2025-08-21
This patch implements ADR-0005 (Allow Multiple Blogs per Name) using the Plan A approach described in docs/features.
Highlights
- Added new route `/:user/blogs` listing all blogs for a Name.
- Added smart redirect for `/:user`:
- 1 blog → `/:user/:blog` (history replace)
- 0 or >1 → `/:user/blogs`
- Converted header action from a single “My Blog” link to a “My Blogs” dropdown with per-blog entries and a “Create new blog” action. For 0 blogs, the header shows a primary “Create Blog” button.
- Kept all existing blog-scoped routes intact.
Code Map
- `src/utils/blogs.ts`: list + fetch blog details helpers.
- `src/pages/UserBlogs/UserBlogs.tsx`: new list page with owner CTAs.
- `src/pages/UserBlogs/NameRootRedirect.tsx`: redirect component for `/:user`.
- `src/App.tsx`: routes added for `/:user/blogs` and `/:user`.
- `src/wrappers/GlobalWrapper.tsx`: fetches current user blogs; wires Navbar selection to set active `currentBlog` and navigate.
- `src/components/layout/Navbar/Navbar.tsx`: “My Blogs” dropdown, accessible popover menu; primary Create button when 0 blogs.
Tests
- `tests/routes/NameRootRedirect.test.tsx`: redirects for single vs multiple blogs.
- `tests/pages/UserBlogs.test.tsx`: renders list.
- `tests/components/Navbar.multiblog.test.tsx`: Create vs My Blogs visibility.
Notes
- Redirect target within Q-Blog is `/:user/:blog` (this apps profile/posts page), not `…/posts`.
- No data migrations; all changes are UI/routing.
+14
View File
@@ -0,0 +1,14 @@
# Q-Blog v0.0.1 — Phase 0 wrap
**Highlights**
- Self-hosted CI workflow (no marketplace actions); verified green.
- Test harness online (Vitest + RTL + jest-axe) with a11y smoke test.
- ESLint flat config with Phase-scoped lint; IDE noise reduced.
- Tracker primed: labels, milestone, seed issues, maintenance scripts.
- Contributor docs and PR templates in place.
**Notes**
- App code linting is intentionally limited in Phase 0; sweeps start Phase 1.
- No functional app changes intended; safe to merge to upstream at any time.
+25
View File
@@ -0,0 +1,25 @@
# Q-Blog v0.1.0 — Multiple Blogs per Name
Release date: 2025-08-21
This release introduces multi-blog support per Name with a minimal, backwards-compatible routing update and improved navigation.
Highlights
- User blogs page: `/:user/blogs` lists all blogs for a Name, with owner actions to Edit/Create.
- Smart redirect for `/:user`:
- Exactly 1 blog → `/:user/:blog` (history replace)
- 0 or >1 blogs → `/:user/blogs`
- Header: “My Blogs” dropdown (lists all your blogs + “Create new blog”); when 0 blogs, shows “Create Blog” button.
- Name clicks: Clicking an author name/avatar now navigates to `/:user` (which resolves to the blogs list unless a single blog exists).
- Editor flow: You can create and edit posts per blog; switching blogs via the header updates the active blog context.
Developer notes
- New files: `src/utils/blogs.ts`, `src/pages/UserBlogs/UserBlogs.tsx`, `src/pages/UserBlogs/NameRootRedirect.tsx`.
- Updated routes in `src/App.tsx`.
- `GlobalWrapper` now fetches current user blogs and wires selection to set `currentBlog`.
- `Navbar` updated to a dropdown menu with accessible patterns (popover + list + focus return).
- Tests cover redirect behavior, page rendering, and header menu states.
No data migrations. Deep links to `/:user/:blog/...` continue to work.
+36
View File
@@ -0,0 +1,36 @@
# Q-Blog v0.1.1 — Wiki Mode Integration, Canonicalization, and Perf
Date: 2025-08-22
## Highlights
- Wiki Mode settings in Create + Edit Blog modals (checkbox + whitelist/blacklist).
- Canonical resolver wired into Post, Blog page, Global feed, Subscriptions, and Favorites.
- Attribution on Post page: “Latest by <Name> · <relative time>”.
- Header “My Blogs” switcher seeds blog context for immediate list refresh.
- Performance: settings read from BLOG metadata when available; per-page parallel prefetch for duplicate identifiers; singletons skip canonicalization.
## Details
- Data model: BLOG JSON and metadata now include `wikiEnabled`, `editorWhitelist`, `editorBlacklist`.
- Resolver logic (`src/utils/wiki.ts`): Owner always allowed; blacklist > whitelist > global allow; newest wins with owner/id tiebreakers.
- Caching (`src/utils/wikiSettingsCache.ts`): Reads from metadata; fetches BLOG JSON only when fields are missing.
- Lists (`src/hooks/useFetchPosts.tsx`):
- Global feed and Subscriptions canonicalize per identifier across Names.
- Favorites canonicalizes per identifier across Names.
- Prefetches settings for blogs with duplicates in parallel; singles are fast-pathed.
## Fixes
- Edit modal now correctly pre-fills the wiki checkbox and lists.
- Blog page refreshes the posts list reliably when switching blogs in the header.
## Testing
- New tests for resolver, cache, blog/post pages, and Favorites canonicalization.
- MSW defaults prevent unhandled network calls in tests.
## Notes
- Backwards compatible: legacy blogs behave unchanged until wiki is enabled.
- Further perf options: optimistic list render and concurrency caps are available if desired.
+38
View File
@@ -0,0 +1,38 @@
Q-Blog v0.2.0 — Wiki Mode Canonical Selection
Date: 2025-08-22
Highlights
- Wiki Mode canonical selection: When multiple authors publish the same `BLOG_POST` identifier, the app now selects a single canonical revision to display based on blog settings.
- Per-blog settings cache: Efficiently resolves each blogs owner and wiki settings (`wikiEnabled`, `editorWhitelist`, `editorBlacklist`).
- Favorites and Subscriptions respect wiki mode: Lists deduplicate by identifier across names and pick the canonical author.
- Individual post view resolves canonical author when wiki is enabled before fetching content JSON.
- UI: Navbar search/filter and persistent Tile/List view toggle. Notifications consolidated in one place.
- Quality: Accessibility tests and stronger utility/unit coverage; minor code cleanup and worker URL fix.
Canonical selection rules
- Owner is always allowed (cannot be blocked by blacklist).
- Blacklist disallows others even if whitelisted; whitelist gates non-owners if non-empty.
- Among authorized authors: pick latest by `updatedAt`/`qdnUpdated`; owner wins ties; otherwise lowest id as last tiebreaker.
Implementation notes
- `utils/wiki.ts` implements `isAuthorized`, `canEdit`, and `selectCanonical`.
- `utils/wikiSettingsCache.ts` caches per-blog owner/settings, preferring metadata when available.
- `hooks/useFetchPosts.tsx` canonicalizes search results for feed, favorites, and subscriptions.
- `pages/BlogIndividualPost/BlogIndividualPost.tsx` selects the canonical author for the requested post.
Breaking changes
- None. Existing identifiers and endpoints are unchanged.
Fixes & cleanup
- Removed an unused web worker and fixed a BLOG_POST worker URL.
- Removed redundant local helper where a shared util exists.
Upgrade notes
- No migration steps required. Deploy as usual; v0.2.0 is compatible with prior data.
+18
View File
@@ -0,0 +1,18 @@
Q-Blog v0.2.1 — Wiki Editing UX Fixes
Date: 2025-08-22
Fixes
- Edit pencil in feed: Shows for authorized wiki editors (not only the original author). Uses blog wiki settings and `canEdit()` to compute visibility.
- Editor loading under wiki mode: The Edit page now builds the full BLOG_POST identifier (`q-blog-<blog>-post-<postId>`), resolves the canonical author if wiki is enabled, and loads their content. Publishing uses the full identifier so editors without their own blog can contribute when allowed.
- Button contrast: “Create Blog” and blog list action buttons (View, Edit) now use themed variants with better contrast in dark/light modes.
Quality
- Tests added for edit-route canonical loading and feed pencil visibility under wiki permissions.
- Prettier formatting step documented before pushes to keep CI green.
Upgrade notes
- No migration steps. This is a behavioral and UX fix release on top of v0.2.0.
+41
View File
@@ -0,0 +1,41 @@
# Q-Blog — Release Notes (v0.2.2)
## Highlights
- **Wiki edits without owning a blog** — editors can publish revisions to wiki-enabled blogs even if they dont have their own blog.
- **Authenticate UX** — auto-auth on first load; Authenticate button shows spinner and prevents double-clicks.
- **Names popover** — clear states for Loading / Error / Empty; prevents “empty menu” confusion.
- **Stability** — tests added for wiki edits, auth button, and names popover.
## Changes
### Code
- `CreatePostMinimal.tsx` — remove `currentBlog` guard in **edit** publish path; prefill title from canonical content.
- `CreatePostBuilder.tsx` — same guard removal in edit path.
- `CreatePost.tsx` — wiki edit gating; canonical selection; passes content to editors.
- `GlobalWrapper.tsx` — thin, idempotent auth; auto-auth on mount; parallel names fetch (`namesStatus`).
- `Navbar.tsx` — Authenticate spinner/disabled; names popover states; “My Blogs” popover.
### Docs
- `FEATURE_WIKI_MODE_OVERVIEW.md` — editors dont need their own blog for edits.
- `TECH_IMPL_WIKI_MODE.md` — edit flow clarified; BLOG vs BLOG_POST responsibilities.
- `FEATURE_MULTIBLOG_OVERVIEW.md` — header Blog Switcher out-of-scope for v0.2.2.
### Tests
- `CreatePost.wiki.editNoBlog.test.tsx` — publishes edit without `currentBlog`.
- `AuthenticateButton.test.tsx` — loading state disables click.
- `SwitchNameMenu.test.tsx` — Loading state visible in popover.
## Upgrade notes
- No migrations. Backward-compatible: missing wiki fields → wiki disabled.
- If you vendor custom Navbar, integrate the names popover states to match tests and UX.
## Verify
- Open a wiki-enabled blog (empty whitelist). Authenticate → edit a post as a different Name with no blog → Publish succeeds.
- Authenticate button shows spinner; double-clicking is ignored.
- Switch-name popover shows Loading/Error/Empty correctly.
+43
View File
@@ -0,0 +1,43 @@
QBlog v0.3.0 — Enhancements and UX updates
Summary
- Image Lightbox: Click images inside posts to view them in a fullwindow overlay. Close the overlay by clicking anywhere or pressing Escape.
- ScrollToTop Button: A floating button appears when scrolled down; smoothly returns to the top.
- Bookmarks: Favorites is becoming Bookmarks. Your existing Favorites are automatically migrated to Bookmarks on first launch and cleared from legacy storage. Bookmarks support folders, sorting, and a dedicated management page at /bookmarks.
- Header/menu: The header now organizes features as: QBlog logo, search, notifications, a unified Main Menu (Create Blog/Post, My Blogs, view toggle, Subscriptions, Bookmarks, Blocked Names, QMail), and the Name selector (for switching names only). The Authenticate button is positioned next to the Menu for stable layout.
Developer Details
- New Components
- ImageLightbox overlay: src/components/common/ImageLightbox.tsx
- ScrollToTop FAB: src/components/common/ScrollToTop.tsx
- Bookmarks
- Redux slice: src/state/features/bookmarksSlice.ts (load, add, remove, set folder)
- Bookmarks page: src/pages/Bookmarks/Bookmarks.tsx with table, sorting, actions, and confirmation dialog
- Migration: src/utils/migrateFavoritesToBookmarks.ts (runs at startup in GlobalWrapper to import Favorites → Bookmarks)
- Wiring: store registration in src/state/store.ts, route added in src/App.tsx, Navbar menu entry
- Toggles: Post list cards, post page, and blog page share a canonical href key so bookmark state is consistent across views
- UI and A11Y
- Navbar: Single “Menu” entry consolidates actions; Name menu focuses on switching only
- Authenticate button placed to the right of Menu to avoid layout jumps when authenticating
- ResponsiveImage passes alt to the underlying image for a11y
- Tests Added/Updated
- tests/features/ImageOverlay.test.tsx: Lightbox opens on image click
- tests/components/ScrollToTop.test.tsx: Button appears after scrolling
- tests/pages/Bookmarks.test.tsx: Bookmark save and list presence
- tests/components/Navbar.multiblog.test.tsx: Updated to open the Main Menu and assert menu items
- Total tests: 33 test files exercising routes, components, features (feed, wiki canonicalization, subscriptions, posting flows), a11y basics, and the new features above
Upgrade Notes
- Favorites is becoming Bookmarks: On first launch, the app migrates Favorites to Bookmarks automatically and removes the old entries from LocalForage. No manual steps required.
- No breaking changes to APIs.
QA Checklist
- Lightbox renders and closes cleanly on click
- Scroll to top button shows after scrolling and hides at top
- Bookmark icons are in sync across list, post page, and blog page views
- Bookmarks table loads metadata (Updated) per item and shows a spinner until available
- My Blogs section expands/collapses on click in the Main Menu
+15
View File
@@ -0,0 +1,15 @@
QBlog v0.3.1 — Image Preview Fix
Summary
- Builder posts: Clicking an image now opens the fullwindow lightbox, matching minimal posts.
- UX: Press Escape or click the dark backdrop to close the preview.
Technical Notes
- Added robust click handling for images inside builder/grid layouts and hardened modal open/close to prevent accidental immediate dismissal.
- No breaking changes; no API changes.
Upgrade Notes
- No action required. Existing posts automatically benefit from the fix.
+63
View File
@@ -0,0 +1,63 @@
# Releasing — Q-Blog
This summarizes the practical steps we use today to publish a release via Gitea and attach versioned artifacts.
## Pre-flight
- Ensure CI is green on the branch to release.
- Verify locally:
- Format first: `npm run format:fix`
- `npm run typecheck && npm run lint:full && npm run test:run`
- Optional production build: `npm run build`
## Create archives
Use our helper to produce versioned source and dist archives under `release/`:
```
bash scripts/release/build-archive.sh --with-build --outdir release --name q-blog
```
This creates:
- `release/q-blog-vX.Y.Z-src.zip` (always)
- `release/q-blog-vX.Y.Z-dist.zip` (if `dist/` exists)
## Tag
If the `package.json` version is already correct, tag it:
```
git tag -a vX.Y.Z -m "Q-Blog vX.Y.Z"
git push origin --tags
```
Or bump + tag via script:
```
bash scripts/release/bump-and-tag.sh X.Y.Z "Q-Blog vX.Y.Z"
```
## Create/Update Gitea Release
With `.gitea.env` configured, run:
```
bash scripts/tracker/with_env.sh .gitea.env \
bash scripts/release/create-gitea-release.sh X.Y.Z \
--title "Q-Blog vX.Y.Z" \
--notes docs/RELEASE_NOTES_vX.Y.Z.md \
--branch update
```
By default, only the current versions zips are uploaded (`release/*-vX.Y.Z-*.zip`). Override with `--assets` if needed.
## Publish to Qortal
- Upload the `*-dist.zip` to Qortal once the Gitea release is live.
- Announce using `docs/USER_ANNOUNCEMENT_vX.Y.Z.md` as a base.
## Notes
- The release scripts avoid attaching older zip files by default.
- For re-runs where artifacts already exist, the API calls are idempotent: release is patched and assets re-uploaded as needed.
+13
View File
@@ -0,0 +1,13 @@
# Q-Blog — Risks, Assumptions & Mitigations
_Generated 2025-08-16 23:27Z_
| ID | Area | Risk/Assumption | Phase | Impact | Mitigation |
| --: | ------- | ----------------------------------------------------------------------- | :---: | ------ | ----------------------------------------------------- |
| R1 | Data | Legacy content migration to default blogs may fail on malformed records | 6 | High | Idempotent migrator, dry run, backup + rollback notes |
| R2 | Editor | Rich-text sanitization strips needed formatting | 10 | Medium | Allowlist tuned with tests; sample content goldens |
| R3 | A11y | Keyboard traps in complex modals/popovers | 45 | Medium | Component audits; focus tests; Esc/restore policies |
| R4 | Collab | Permission gaps lead to privilege escalation | 7 | High | Server-side checks; matrix tests; deny-by-default |
| R5 | Perf | Large lists regress INP/LCP | 8 | Medium | Virtualization, prefetch, memoization; vitals budgets |
| A1 | API | We can evolve/extend server contracts | 0 | — | If not, draft shims and versioned adapters |
| A2 | Tooling | CI runners can execute headless browsers for axe/e2e | 2 | — | If flaky, move some checks to nightly |
+28
View File
@@ -0,0 +1,28 @@
# Q-Blog — Roadmap Dependencies & Milestones
_Generated 2025-08-16 23:27Z_
## Dependency Graph (high level)
- Phase 2 depends on Phase 1 docs (testing/a11y standards referenced).
- Phase 3 depends on Phase 2 harness (to verify fixes).
- Phase 4 (a11y) builds on Phase 3 stability.
- Phase 6 (multi-blog) depends on Phase 5 UX patterns (switcher placement, routing clarity).
- Phase 7 (shared blogs) depends on Phase 6 data model and routing.
- Phase 10 (release) depends on all prior phases plus observability wiring.
## Milestone Gates
- **M1 — Baseline Ready (P0P2):** Docs present; typecheck/lint/tests green; coverage report generated.
- **M2 — Stable & Accessible (P3P4):** No crashers; keyboard-only journeys pass; axe critical=0.
- **M3 — UX Solid (P5):** Consistent patterns, no dead ends; editor ergonomics audited.
- **M4 — Multi-Blog (P6):** Create/switch blogs with migrated legacy content.
- **M5 — Shared Blogs (P7):** Role matrix enforced; invites work.
- **M6 — Resilient & Performant (P8P9):** Vitals within budget; themes/i18n consistent.
- **M7 — Release Ready (P10):** Telemetry, security checks, notes/guides done.
---
## Audit Notes (v0.0.1)
- ⚠️ Note: Release automation script had issues with curl args; roadmap should note fixing release tooling.
+23
View File
@@ -0,0 +1,23 @@
# Q-Blog — Security & Privacy Posture (1.0)
_Generated 2025-08-16 23:27Z_
## Principles
- **Least privilege** — Roles restrict actions; tokens scoped; client never authoritative.
- **Sanitize everywhere** — Rich text sanitized on save and render (allowlist).
- **Fail safe** — On doubt, deny writes; surface clear errors with next actions.
- **Minimal telemetry** — Only crash/quality signals; no PII; user-visible policy.
- **Defense in depth** — CSP, input validation, dependency hygiene, error boundaries.
## Non-Goals (1.0)
- End-to-end encryption for content.
- Fine-grained per-post ACLs (roles are per blog).
## Checklist (Dev)
- Inputs validated client-side; re-validated server-side.
- All writes include role checks and revision/ETag for concurrency.
- Sanitization unit tests cover common XSS vectors.
- Dependencies audited; pinned versions for determinism.
+59
View File
@@ -0,0 +1,59 @@
# Testing — Q-Blog
## Local
```bash
# ensure Node 20.x is active (use nvm if needed)
npm run typecheck
npm run lint:phase0
npm run test:run
npm run build
```
MSW is enabled in tests; unhandled requests are bypassed.
## CI (Gitea)
- Workflow: `.gitea/workflows/ci.yml`
- Runner: self-hosted
- Steps: checkout → Node setup → deps → typecheck → lint (phase0) → format check → tests (Vitest + RTL + MSW) → build (Vite)
## a11y smoke
`tests/axe-smoke.test.tsx` runs jest-axe.
Additional tests added:
- `tests/utils/blogIdformats.test.ts`: blog id helpers.
- `tests/utils/extractTextFromSlate.test.ts`: Slate plain-text extraction.
- `tests/utils/time.test.ts`: relative time helpers.
- `tests/utils/toBase64.test.ts`: binary encode/decode helpers.
- `tests/state/blogSlice.test.ts`: reducer behaviors and upserts.
- `tests/hooks/useFetchPosts.test.tsx`: minimal sanity for hook utilities.
- `tests/utils/checkStructure.test.ts`: blog and mail structure validation.
- `tests/utils/fetchPosts.test.ts`: maps BLOG_POST fetch via MSW.
- `tests/utils/qortalRequestFunctions.test.ts`: qortalRequest wrappers behavior.
- `tests/utils/fetchMail.test.ts`: mail decrypt/mapping flow with mocked qortalRequest.
- `tests/utils/wiki.test.ts`: wiki authorization and canonical resolver.
- `tests/pages/BlogList.test.tsx`: renders posts and new-post banner (MUI ThemeProvider).
- `tests/pages/BlogIndividualProfile.test.tsx`: loads blog title from API (MSW + ThemeProvider).
- `tests/pages/BlogIndividualPost.wiki.test.tsx`: canonical across Names + edit visibility.
- `tests/pages/BlogIndividualProfile.wiki.test.tsx`: canonical grouping on blog page.
- `tests/utils/wikiSettingsCache.test.ts`: cache fetch and settings mapping.
- `tests/hooks/useFetchPosts.favorites.wiki.test.tsx`: Favorites canonicalization across Names.
---
## Notes
- MSW is configured in `tests/setup.ts`; unhandled requests are bypassed by default.
- Coverage reporter is v8 (`text`, `lcov`). Consider adding thresholds as the suite expands.
- Test env adds a minimal `IntersectionObserver` polyfill in `tests/setup.ts` for components using `react-intersection-observer`.
- UI tests wrap components in MUI `ThemeProvider` with a default theme to satisfy styled theme usage.
## Test Scenarios — Multi-Blog (future)
- Header: 0 vs 1 vs N blogs; menu opens/closes; selection navigates and sets active blog.
- Name root redirect: single blog → posts; else → `/{{name}}/blogs`; polite loading text in `aria-live`.
- UserBlogs page: list/empty states; self vs public controls.
- A11y smoke (axe) on header + UserBlogs (when available).
+11
View File
@@ -0,0 +1,11 @@
# Q-Blog v0.1.0 — Whats New
Q-Blog now supports multiple blogs per Name.
- New: a “My Blogs” menu to switch between your blogs or create a new one
- New: a user blogs page at `/:user/blogs` listing all blogs for that name
- Smarter name links: clicking a user name takes you to their blogs list (or straight to their single blog if they have only one)
Try it here: qortal://app/qblog
If you have feedback or run into any issues, please let us know. Enjoy blogging!
+21
View File
@@ -0,0 +1,21 @@
Q-Blog v0.2.0 — Wiki Mode is here
Weve shipped a smarter, collaborative blogging experience. When multiple people post updates to the same article, Q-Blog now shows a single, “canonical” version based on the blog owners wiki settings.
Whats new
- Canonical posts: If several authors publish a revision of the same post, Q-Blog picks one to display. The owners settings define who can contribute. The most up-to-date allowed revision wins; the owners revision wins ties.
- Works everywhere: The main feed, Favorites, Subscriptions, and the post page all use canonical selection when wiki mode is enabled.
- Quality of life: Search/filter from the navbar and switch between Tile/List views (your choice is remembered).
- (v0.2.1) Better readability: “Create Blog” and blog list action buttons are clearer in both dark and light themes.
- (v0.2.2) Wiki editing bugfix: Editing a post without having a blog no longer crashes the editor.
No action needed
Just update to the current release. Your posts and favorites continue to work; collaborative updates will show consistently.
Tips
- Owners can enable wiki mode and manage who can edit via blog settings (whitelist/blacklist).
- Use the List view to scan more details at a glance; switch back to Tiles any time.
Thanks for using Q-Blog!
+14
View File
@@ -0,0 +1,14 @@
Q-Blog v0.2.1 — Smoother Wiki Editing
Weve polished the wiki experience based on feedback from v0.2.0.
Whats improved
- Edit access in the feed: If a blog enables wiki mode and youre allowed to edit, the edit pencil now appears for you (even if you arent the original author).
- Reliable editing: The Edit page correctly loads the latest canonical content and publishes updates under the proper identifier so contributions “just work”.
- Better readability: “Create Blog” and blog list action buttons are clearer in both dark and light themes.
No action needed
Update to v0.2.1 — your workflows and posts remain the same, just smoother.
Thanks for the quick feedback and keep it coming!
+15
View File
@@ -0,0 +1,15 @@
QBlog v0.3.0 — Whats New
- Image lightbox: Click any image in a post to view it fullwindow; click anywhere to dismiss.
- Scrolltotop: A floating button appears as you scroll; click to return smoothly to the top.
- Bookmarks: Favorites is becoming Bookmarks. Your saved items migrate automatically on first launch. Manage at /bookmarks — organize with folders, sort by Updated, open/copy links, and remove with confirmation.
- Streamlined header: Logo, search, notifications, a unified Main Menu (Create Blog/Post, My Blogs, view toggle, Subscriptions, Bookmarks, Blocked Names, QMail), and a simple Name switcher. The Authenticate button sits next to the Menu for a stable layout.
- QA: 3 new test sets added (33 total) covering the lightbox, scrolltotop, and bookmarks flows, alongside existing route, component, wiki canonicalization, subscriptions, posting, and a11y coverage.
Where to find it
- Main Menu (next to the bell): Create Blog, Create Post, My Blogs (expandable), switch Tile/List view, Subscriptions, Bookmarks, Blocked Names, QMail.
- Bookmark toggles: On post cards, post pages, and blog pages — changes sync everywhere.
- Bookmarks page: /bookmarks (also in the Main Menu).
Questions or feedback? Open an issue or reach out via QMail.
+2
View File
@@ -0,0 +1,2 @@
QBlog v0.3.1: Image previews now work for builder posts — click any post image to view fullscreen.
qortal://app/qblog
+49
View File
@@ -0,0 +1,49 @@
# Q-Blog 1.0 — Personas & Key Journeys
_Generated 2025-08-16 23:27Z_
## Personas
- **Solo Writer** — Publishes articles and drafts privately before release.
- **Team Owner** — Creates a shared blog and invites collaborators.
- **Invited Author** — Writes posts on a shared blog; cannot manage members.
- **Reader** — Browses and discovers posts; expects fast loads and clear navigation.
## Protected Journeys (1.0)
1. **Read posts** — Navigate lists → open an article; keyboard and screen reader friendly.
2. **Create post** — Draft, autosave, preview, publish; clear feedback on errors.
3. **Edit post** — Update content; attribution and revision increase predictability.
4. **Manage blogs** — Create/switch blogs; lists scope to the selected blog.
5. **Collaborate** — Invite member; invited author creates/edits within role.
## Acceptance Hints per Journey
- **Read:** Landmarks in place, heading hierarchy valid, link text descriptive, images have alt.
- **Create/Edit:** Toolbar states deterministic; autosave status surfaced via live region; error can be retried.
- **Manage:** Blog switch affects all scoped views; URLs stable and shareable.
- **Collaborate:** Role gating enforced in UI and API; forbidden paths never succeed.
---
## Audit Notes (v0.0.1)
- ⚠️ Note: PostEditor autosave + live region announcements need verification in code; docs may overstate completeness.
## Journeys — Multi-Blog
### 0 blogs (self)
- Header shows **Create Blog**. `/{{name}}``/{{name}}/blogs` with empty state + CTA.
### 1 blog
- `/{{name}}` redirects to `/{{name}}/{{blog}}/posts`. Header shows **My Blogs** (1 item + Create).
### N blogs
- Header dropdown lists all blogs (active ✓). `/{{name}}/blogs` lists them with Edit/Create when self.
### Public view
- Viewing someone elses `/{{name}}/blogs` shows a read-only list; no edit/create controls.
+14
View File
@@ -0,0 +1,14 @@
# Versioning & Phases
We map **project phases** to **SemVer**:
- **Phase 0** → `0.0.1` (orientation & quality bar; no feature changes)
- **Phase 1** → `0.1.0` (correctness & hygiene foundations)
- **Phase 2** → `0.2.0` (docs/testing expansion, early UX fixes)
- ...
Rules:
- Use **SemVer**; phases bump the **minor** (except Phase 0 uses `0.0.1`).
- Patch bumps (`0.X.Y`) are reserved for small fixes within a phase.
- Tag releases and include short notes in `docs/RELEASE_NOTES_vX.Y.Z.md`.
+41
View File
@@ -0,0 +1,41 @@
# Q-Blog 1.0 — PRFAQ
_Generated 2025-08-16 23:27Z_
## Press Release (Narrative)
Today we announce **Q-Blog 1.0**, a modern, accessible writing workspace that makes it effortless to publish under multiple blogs with a single name, and to collaborate safely through **Shared Blogs**. Q-Blog pairs a focused editor with strong correctness, accessibility, and testability so writers can publish confidently, teams can coordinate, and readers can trust what they see.
Key highlights:
- **Multiple blogs per name** — Organize topics cleanly without creating new accounts.
- **Shared blogs** — Invite editors and authors with role-based permissions.
- **Inclusive by design** — Keyboard-ready, screen-reader friendly, respectful of motion/contrast preferences.
- **Resilient** — Deterministic saves, offline tolerance for drafts, clear recoveries on failure.
- **Transparent** — Human-readable release notes and an upgrade guide.
## FAQ
**What is Q-Blog? Who is it for?**
A content creation app for individuals and teams who want a fast, accessible, and reliable blogging workflow.
**How is it different?**
Strong quality bar (tests, a11y), multi-blog under one name, and safe collaboration via shared blogs—all while keeping the UI simple.
**Why multiple blogs per name?**
Writers often publish in distinct domains (e.g., dev notes vs. essays). Separate blogs keep audiences clear without fragmenting identity.
**How do Shared Blogs work?**
Blog owners can invite collaborators and assign roles (**Owner**, **Editor**, **Author**). Permissions are enforced server-side; the UI mirrors capabilities but never trusts the client alone.
**What about accessibility?**
We commit to keyboard-only journeys, semantic landmarks, focus management, descriptive names/labels, and honoring user preferences for motion and contrast.
**How do you keep content safe?**
Rich text is sanitized on save and render (allowlist). Permissions gate every write. Observability captures issues without leaking personal data.
**How do you measure success?**
Reliability (save success rate), accessibility (keyboard checks + automated audits), performance (Web Vitals), and usability (zero “dead-end” flows).
**Whats in 1.0?**
Multi-blog per name, Shared Blogs, resilient editor workflows, accessibility baseline, docs, and a clear release process.
@@ -0,0 +1,37 @@
# Multiple Blogs per Name — Overview (Plan A: Smart Redirect)
_Generated 2025-08-21_
## Goal
Allow a single **Name** to own **multiple Blogs**, while preserving existing blog-scoped routes and minimizing churn. Replace the single “My Blog” button with a **“My Blogs” dropdown** and introduce a new **User → Blogs** list page. When a Name has exactly one blog, visiting `/{name}` **auto-redirects** to that blog.
## Scope
- **In**: Header/Nav blog access, Name landing behavior, simple blog list page, light routing changes, a11y behavior.
- **Out**: Post editor behavior, post list virtualization, roles/permissions model changes, server protocol changes (unless list-by-name is missing).
## High-level UX
- Header/Nav:
- 0 blogs → Button shows **Create Blog** (primary action). Dropdown disabled/hidden.
- ≥1 blog → Button shows **My Blogs**. Dropdown lists each blog (title + id). Bottom row = **Create new blog**.
- Name route:
- `/{name}`:
- If blog count === 1 → redirect to `/{name}/{blog}/posts` (existing default route).
- If blog count !== 1 → show **User Blogs** page (`/{name}/blogs`).
- User Blogs page:
- Public, lists all blogs for the Name. If viewer is same user, show **Edit** per blog and **Create Blog** CTA.
- A11y:
- Dropdown uses proper menu roles, keyboard navigation, and focus return.
- User Blogs page has a single **H1**, semantic list, clear button labels.
## Benefits
- Aligns with entity contract (**Name 1..N Blog**).
- Minimizes refactors; relies on existing blog-scoped routing and components.
- Backward-compatible for deep links.
## Notes
- The **Header Blog Switcher** dropdown is **out of scope for v0.2.2** (list + redirect are implemented).
@@ -0,0 +1,39 @@
# Wiki Mode / Multi-Editor — Product Overview
_Generated 2025-08-22_
## Summary
Q-Blog gains a new **Wiki mode**, allowing multiple **Names** to publish **visible revisions** of posts under a blog.
- **Names** are the identity key (not account addresses).
- Authorization is computed **client-side**; Qortal has no server scripting.
- Precedence: **blacklist > whitelist > global allow**.
- Backward-compatible: missing fields mean wiki is off.
## UX
> Editors **do not need their own blog** to publish a revision on a wiki-enabled blog.
- **Blog Settings:** toggle Wiki Mode; configure whitelist/blacklist of Names.
- **Post page:** shows the canonical (latest visible) revision with author/date attribution.
- Implemented: canonical chosen across Names by exact BLOG_POST identifier; attribution shown when author differs from owner.
- **Edit button:** shown only to Owner and authorized editors.
- **Blog list:** groups posts by lineage and shows canonical revision per group.
- Implemented for Blog page (`/{name}/{blog}`) using identifier-grouping.
- Global feed, Subscriptions, and Favorites canonicalize duplicates via a cached lookup of per-blog wiki settings.
- Header “My Blogs” switcher updates both title and posts by seeding blog context immediately.
## Canonical Selection (v0.2.0)
The app selects one canonical revision when multiple Names publish the same `BLOG_POST` identifier:
- Authorization first: filter out unauthorized Names using blog settings. The Owner is always allowed (cannot be blocked).
- Freshest wins: compare `updatedAt` or `qdnUpdated`; pick the most recent.
- Tie-breakers: if timestamps tie, prefer the Owner; otherwise, pick the lowest id for stability.
This logic is applied consistently in:
- Main feed pages and “Load new posts”.
- Favorites and Subscriptions lists.
- Individual post view before fetching content.
+36
View File
@@ -0,0 +1,36 @@
# Wiki Mode — Resolver & Authorization Spec
_Generated 2025-08-22_
## Identity
- **Name** (string) is the identity key.
- One account may own multiple Names. Names may be transferred; rules apply to current Name string.
## Authorization Precedence
1. Owner always allowed.
2. If Name ∈ blacklist → blocked.
3. If whitelist non-empty → allowed iff Name ∈ whitelist (and not blacklisted).
4. If whitelist empty → allowed (unless blacklisted).
If Name in both lists → blacklisted.
## Canonical Selection
- Group posts by originPostId (or id if missing).
- Candidates: published posts in lineage blog id.
- Filter: authorized authors only.
- Choose newest by updatedAt. Tie: Owner wins; then lowest id.
## Edit Visibility
Shown if: (viewer == Owner) or (wikiEnabled && viewer not blacklisted && (whitelist empty or viewer ∈ whitelist)).
## Backward Compatibility
- Missing wikiEnabled → false
- Missing lists → empty
- Missing originPostId → id
- Missing lineageBlogId → current blogId
- Missing updatedAt → QDN timestamp
+215
View File
@@ -0,0 +1,215 @@
# Implementation Guide — Multiple Blogs per Name (Plan A)
_Generated 2025-08-21_
> **AAA mode:** Actionable, artifact-oriented, and accessible. This guide defines exact files to create or modify, contracts, a11y rules, redirects, and tests.
---
## Contracts & Routing
### Entities (unchanged)
- **Name**: can own multiple **Blog** records.
- **Blog**: immutable link to its **Name**; addressed by `blogHandle` (aka blog id).
- **Post**: 1..1 Blog (unchanged).
### Routes (final form)
- `/{name}`
- If **hasExactlyOneBlog(name)** → redirect (replace history) to `/{name}/{blog}/posts` (or current posts index).
- Else → route to **User Blogs** page (`/{name}/blogs`).
- `/{name}/blogs`**User Blogs** page (new).
- `/{name}/{blog}/*`**existing blog-scoped routes** (unchanged).
**Assumption:** Existing default blog landing is `/{name}/{blog}/posts`. Adjust if your index differs.
---
## UI Changes
### 1) Header/Nav — “My Blogs” dropdown
**File:** `src/components/HeaderNav.tsx` (or equivalent container for auth controls)
- Replace single “My Blog”/“Create Blog” button with a **dropdown** (same pattern as the Authenticate/Names dropdown).
- **State inputs:**
- `currentUser?: NameRef`
- `userBlogs: BlogSummary[]` — list of blogs for current user (title + `blogHandle`).
- `onCreateBlog: () => void`
- `onSelectBlog: (blog: BlogRef) => void` → navigates to `/{name}/{blog}/posts` and updates active blog in store.
- **Label logic:**
- `userBlogs.length === 0` → show **Create Blog** (primary button), hide dropdown caret.
- `userBlogs.length >= 1` → show **My Blogs** with caret; open menu on click.
- **Menu content (for ≥1):**
- One item per blog: `Title · blogHandle` (truncate cautiously). Current active shows a ✓.
- Divider
- **Create new blog** (button-like menu item).
- **A11y:**
- Use proper `role="menu"`/`menuitem"`, `aria-haspopup="menu"`, `aria-expanded`, `aria-controls`.
- Focus returns to the trigger on close. ESC closes.
- Ensure high-contrast tokens; respect `prefers-reduced-motion` (no aggressive animation).
> **Note:** If a reusable `BlogSwitcher` exists, consider refactoring the above as a `UserBlogSwitcher` variant that consumes the current users blogs.
### 2) User Blogs page (new)
**File:** `src/pages/UserBlogs.tsx` (or `src/routes/UserBlogs.tsx`)
- **H1:** “{name}s Blogs”
- **List:** Card per blog with title, `blogHandle`, optional description/meta.
- **Actions:**
- View → pushes route `/{name}/{blog}/posts`.
- If `currentUser.nameId === params.nameId` → buttons: **Edit**, **Create Blog** (page-level CTA).
- **Empty states:**
- Self (0 blogs): “You dont have any blogs yet.” + prominent **Create Blog**.
- Other user (0 blogs): “No public blogs yet.”
**A11y:** One H1, list semantics, buttons with explicit labels (include blog title in `aria-label`).
---
## Store & Data
### Selectors
- `selectCurrentUser()` → NameRef / undefined.
- `selectUserBlogs(nameId)` → BlogSummary[] (RTK Query / slice selector).
- `selectActiveBlog()` → BlogRef | null (unchanged).
### RTK Query (if missing)
- **Endpoint:** `listBlogsByName(nameId)`
- Cache key: `['blogs', nameId]`.
- Normalized response: `BlogSummary[]` (include `blogHandle`, `title`).
- Consumers:
- Header dropdown fetches `listBlogsByName(currentUser.nameId)` when authenticated.
- `UserBlogs` page fetches `listBlogsByName(params.nameId)`.
> If an endpoint already exists, reuse it. Otherwise add it in `src/store/api.ts` with blog-scoped cache keys documented in Q-Blog.
---
## Routing Changes
**File:** `src/router.tsx` (or where routes are defined)
1. Add route for `/{name}/blogs``UserBlogs` component.
2. Update `/{name}` route element to perform **smart redirect**:
- On mount:
- Fetch `blogs = listBlogsByName(params.nameId)`.
- If `blogs.length === 1``navigate('/{name}/' + blogs[0].blogHandle + '/posts', { replace: true })`.
- Else → `navigate('/{name}/blogs', { replace: true })`.
- **Loading state:** show a lightweight spinner/skeleton with polite live region (“Loading blogs…”).
> Keep deep links to `/{name}/{blog}/…` working as today.
---
## Permissions
- Editing controls on `UserBlogs` are visible only when viewing your own Name (`currentUser.nameId === params.nameId`).
- All blog/post routes retain existing guards (no change).
---
## Edge Cases & Behavior
- **0 blogs (self):** Header shows **Create Blog**. `/{name}` goes to `/blogs` with the “create” empty state.
- **1 blog:** Header shows **My Blogs**; menu has one item + “Create new”. Name root redirects to that blog.
- **N blogs (large):** Menu scrolls; consider collapsed ids or optional search later (not part of this change).
- **Anonymous viewer:** Can see another users `/{name}/blogs`, no edit/create controls.
- **Broken blogHandle:** If a listed blog 404s, show error boundary as today; menu item should not break the app.
---
## Accessibility Requirements
- Dropdown follows WAI-ARIA menu pattern; keyboard: ArrowUp/Down, Enter, ESC.
- Focus management: opening menu moves focus to first item; closing returns to trigger.
- Single H1 on `UserBlogs`. Buttons have informative `aria-label`s (“View blog Alpha (alpha)”).
- Live region only for the brief loading text on Name root redirect; no chatty updates.
---
## Testing Plan (Vitest + RTL + user-event + MSW + jest-axe)
> If MSW/jest-axe arent present yet, include unit tests now and add MSW/a11y smoke later. Aim to cover behavior, not visuals.
1. **Header/Menu**
- 0 blogs → shows “Create Blog” button, no dropdown.
- 1+ blogs → shows “My Blogs”; opens menu; selecting a blog navigates to proper route and sets active blog.
- Keyboard navigation: items reachable via arrows; ESC closes; focus returns to trigger.
2. **Name Root Redirect**
- 1 blog → navigates to blog posts route (replace history).
- 0 or >1 → navigates to `/{name}/blogs`.
- Loading state renders politely with an `aria-live="polite"` region text.
3. **UserBlogs Page**
- Renders list matching `listBlogsByName` response.
- Self-view shows Edit + Create; other-view hides them.
- Empty states render correct CTAs/messages.
4. **A11y/axe Smoke**
- Header and `UserBlogs` produce no critical axe violations.
---
## Step-by-step Changes (files)
1. **Create:** `src/pages/UserBlogs.tsx`
- Functional component rendering list of blogs by `params.nameId`.
- Uses RTK Query selector/hook to fetch `listBlogsByName`.
- Buttons: View, (conditional) Edit; page-level Create when self.
2. **Modify:** `src/router.tsx`
- Add `/{name}/blogs` route.
- Update `/{name}` route element to perform smart redirect based on fetched blog count.
3. **Modify:** `src/components/HeaderNav.tsx`
- Replace single button with dropdown:
- When 0 blogs → primary **Create Blog** button.
- When ≥1 blogs → **My Blogs** dropdown listing blog items + “Create new blog”.
- Wire `onSelectBlog` to navigate and set active blog; wire `onCreateBlog` to current flow (dialog/route).
4. **Add (if missing):** `src/store/api.ts`
- Endpoint: `listBlogsByName(nameId)` with `providesTags: (nameId) => ['blogs', nameId]` or equivalent.
- Types: `BlogSummary` (title, blogHandle).
5. **Tests:**
- `tests/routes/UserBlogs.test.tsx`
- `tests/components/HeaderNav.multiblog.test.tsx`
- `tests/routes/NameRootRedirect.test.tsx`
6. **Docs:**
- Link this guide from `docs/USER_JOURNEYS.md` and `docs/ARCHITECTURE.md` after merge.
---
## Acceptance Criteria
- Header shows **Create Blog** when user has 0 blogs; shows **My Blogs** dropdown when ≥1.
- Selecting a blog from the dropdown navigates to `/{name}/{blog}/posts` and marks it active.
- Visiting `/{name}`:
- If exactly one blog exists → silently lands on that blogs posts list.
- Else → shows `/{name}/blogs` page listing all blogs for that Name.
- `UserBlogs` page renders with a single H1, accessible list, and correct empty states.
- Unit/component tests pass; a11y smoke has no critical issues.
- No regressions to existing blog-scoped routes; deep links remain valid.
---
## Rollout Notes
- No data migrations; purely UI/routing.
- If telemetry exists, consider logging menu usage and redirect outcomes to validate adoption.
- Update release notes after merge.
+72
View File
@@ -0,0 +1,72 @@
# Wiki Mode / Multi-Editor — Technical Implementation
_Generated 2025-08-22_
## Data Model
**BlogSettings**
- `wikiEnabled: boolean` (optional; missing = false)
- `editorWhitelist: Name[]` (optional; empty = global allow)
- `editorBlacklist: Name[]` (optional)
**Post (revision)**
- `originPostId: Id` (first ancestor; fallback to id if missing)
- `parentPostId?: Id`
- `lineageBlogId: BlogId` (owners blog id; fallback to route blog id)
- `authorName: Name` (registered name)
- `updatedAt: ISO timestamp` (fallback to QDN timestamp if missing)
- `published: boolean`
## Authorization (by Name)
1. Owner always allowed.
2. Blacklist blocks regardless.
3. Whitelist non-empty → only listed Names (minus blacklist).
4. Whitelist empty → all Names allowed (minus blacklist).
## canEdit(viewerName, settings, ownerName)
- Returns true if viewerName is Owner, or if wiki enabled and viewerName passes the whitelist/blacklist rules.
## Canonical Resolver (client)
1. Resolve origin id.
2. Collect revisions with same origin + blog lineage, published = true.
3. Filter by authorization (authorName vs blog settings).
4. Pick newest by updatedAt; tiebreak: Owner wins; then lowest id.
Reference implementation in code:
- `src/utils/wiki.ts` provides `isAuthorized`, `canEdit`, and `selectCanonical` used across UI.
- `src/utils/wikiSettingsCache.ts` caches per-blog (ownerName, settings) using `/arbitrary/resources?service=BLOG&identifier=...` and `/arbitrary/BLOG/<owner>/<id>`.
- `src/hooks/useFetchPosts.tsx` groups search results by identifier and applies canonical selection in feed, favorites, and subscriptions.
- `src/pages/BlogIndividualPost/BlogIndividualPost.tsx` resolves canonical author before fetching BLOG_POST JSON when wiki mode is enabled.
## UI
- Blog Settings: toggle, Name pickers for whitelist/blacklist.
- Implemented in `Edit Blog` modal (checkbox + comma-separated Name inputs).
- Also available in `Create Blog` modal so new blogs can enable wiki from the start.
- Post page: “Latest by Name on Date” subheader if revision not by Owner.
- Edit: shown only if canEdit true.
- Blog list: use resolver to show canonical per lineage.
- Global feed, Subscriptions, and Favorites use a lightweight cache of per-blog settings to canonicalize duplicates by identifier.
- Header blog switcher seeds blog context to ensure the posts list refreshes immediately on change.
## Backward Compatibility
- Missing fields → defaults (wikiEnabled=false, lists empty, origin=id, lineage=blogId, updatedAt=QDN ts).
## Performance Notes
- Settings Resolution: reads wiki settings from BLOG resource metadata when available; otherwise fetches BLOG JSON as fallback.
- Prefetch: for each page of results, settings for blogs with duplicate identifiers are prefetched in parallel (singletons skip resolution).
- Canonicalization happens only when necessary; otherwise owner or newest item is used.
## Edit Flow (Wiki)
- **Editors do not need their own blog** to publish a revision of an existing post when the target blog has Wiki Mode enabled.
- Edits publish a new `BLOG_POST` under the editor's **Name** using the **original BLOG_POST identifier**; the canonical resolver selects the visible revision.
- Blog (`service: BLOG`) metadata is **not** republished during edits.
+53
View File
@@ -0,0 +1,53 @@
// eslint.config.mjs — Phase 0 env-scoped ignores
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import reactHooks from 'eslint-plugin-react-hooks';
import jsxA11y from 'eslint-plugin-jsx-a11y';
// Toggle with LINT_SCOPE=phase0|full (default phase0 during Phase 0)
const SCOPE = process.env.LINT_SCOPE || 'phase0';
const baseIgnores = [
'node_modules/**',
'dist/**',
'coverage/**',
'.vite/**',
'.gitea/**',
'eslint.config.mjs',
'vite.config.*',
'vitest.config.*',
];
const phase0Extra = ['src/**']; // exclude app during Phase 0 sweeps
const always = ['**/*.d.ts']; // we dont lint type decl files
export default [
{ ignores: [...baseIgnores, ...(SCOPE === 'phase0' ? phase0Extra : []), ...always] },
js.configs.recommended,
...tseslint.configs.recommended, // untyped baseline
{
files: ['**/*.{ts,tsx,js,jsx}'],
languageOptions: {
parser: tseslint.parser,
ecmaVersion: 2022,
sourceType: 'module',
parserOptions: { tsconfigRootDir: import.meta.dirname },
},
plugins: { 'react-hooks': reactHooks, 'jsx-a11y': jsxA11y },
rules: {
'no-console': ['warn', { allow: ['warn', 'error'] }],
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'jsx-a11y/alt-text': 'error',
'jsx-a11y/no-autofocus': 'warn',
'jsx-a11y/label-has-associated-control': 'error',
},
},
{
files: ['tests/**/*.{ts,tsx,js,jsx}'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'no-unused-expressions': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
},
},
];
@@ -0,0 +1,24 @@
# Adopt Quality Charter (sign-off)
## Goal
Ratify the Quality Charter and set acceptance gates for Q-Blog 1.0.
## Tasks
- Review reliability, a11y, perf, security, and testability targets
- Adjust thresholds if needed
- Record sign-off and owners (Quality, A11y, Security)
## Acceptance
- Charter updated (if needed) and marked signed
- Owners assigned and documented
- See docs/QUALITY_CHARTER.md.
## Labels
Type: docs · Area: docs · Priority: P0 · Size: S
**Labels:** docs, P0, S, docs-area
**Milestone:** Patch 0 — Orientation & Quality Bar
@@ -0,0 +1,26 @@
# Finalize Foundation Docs (Architecture, Testing, Accessibility, Security)
## Goal
Publish concise, source-of-truth docs to guide development.
## Includes
- ARCHITECTURE (UI ↔ state ↔ data, Name/Blog/Post model)
- TESTING (pyramid, coverage, fixtures/mocks, MSW policy)
- ACCESSIBILITY (landmarks, focus, live regions, motion/contrast)
- SECURITY & PRIVACY (sanitization, permission checks, CSP posture)
- USER_JOURNEYS & GLOSSARY (personas, invariants)
## Acceptance
- Docs present in `docs/`, linked from README
- Each doc has a short "How to verify" section
- See docs/QUALITY_CHARTER.md.
## Labels
Type: docs · Area: docs · Priority: P1 · Size: M
**Labels:** docs, P1, M, docs-area
**Milestone:** Patch 0 — Orientation & Quality Bar
@@ -0,0 +1,25 @@
# Stand up Test Harness (Vitest + RTL + MSW + jest-axe)
## Goal
Enable fast, realistic tests with accessibility checks.
## Tasks
- Configure Vitest (jsdom), alias @→src, coverage thresholds
- Global test setup with RTL, user-event, jest-dom, jest-axe
- MSW server for success/error/invalid payloads
- Seed tests: app smoke, editor minimal, a11y smoke
## Acceptance
- `pnpm test` runs green; coverage report produced
- At least 3 smoke tests and 1 a11y test in place
- See docs/QUALITY_CHARTER.md.
## Labels
Type: test · Area: tests · Priority: P0 · Size: M
**Labels:** test, P0, M, tests
**Milestone:** Patch 0 — Orientation & Quality Bar
@@ -0,0 +1,24 @@
# ESLint Flat Config + jsx-a11y Baseline
## Goal
Catch accessibility and code-quality issues early.
## Tasks
- Add flat ESLint config with TypeScript/React/jsx-a11y
- Prettier integration; import/order; testing-library plugin
- Fix initial lint errors (no blanket disables)
## Acceptance
- `pnpm lint` green on clean checkout
- No `@ts-nocheck`; no new `any` in public props
- See docs/QUALITY_CHARTER.md.
## Labels
Type: chore · Area: build · Priority: P0 · Size: S
**Labels:** chore, P0, S, build
**Milestone:** Patch 0 — Orientation & Quality Bar
@@ -0,0 +1,24 @@
# Correctness Sweep I — MUI v4 imports & ts-nocheck removal
## Goal
Remove known correctness hazards blocking development.
## Targets
- Replace `@material-ui/*` imports with `@mui/*`
- Remove `@ts-nocheck` in editor and app; add minimal types
- Ensure app still builds/boots after changes
## Acceptance
- No remaining v4 imports; no ts-nocheck in core paths
- Smoke tests green; manual run shows editor screen
- See docs/QUALITY_CHARTER.md.
## Labels
Type: fix · Area: editor · Priority: P0 · Size: M
**Labels:** fix, P0, M, editor
**Milestone:** Patch 0 — Orientation & Quality Bar
@@ -0,0 +1,24 @@
# Correctness Sweep II — Type hygiene in reducers/selectors
## Goal
Reduce `any` usage and adopt consistent async state shapes.
## Tasks
- Replace common `any` with discriminated unions/interfaces
- Standardize async lifecycles (idle/loading/success/error)
- Normalize error object shape (code/message/recoverable)
## Acceptance
- Reducer/selector tests added
- No new `any` in reducers/selectors; stricter types compile
- See docs/QUALITY_CHARTER.md.
## Labels
Type: fix · Area: state · Priority: P1 · Size: M
**Labels:** fix, P1, M, state
**Milestone:** Patch 0 — Orientation & Quality Bar
@@ -0,0 +1,24 @@
# Correctness Sweep III — Empty/Loading/Error states standardization
## Goal
Users never experience a “mystery fail.”
## Tasks
- Implement consistent empty/loading/error components
- Wire to data flows across list/detail/editor
- Ensure retry paths for transient failures
## Acceptance
- Error surfaces include a recovery action
- Smoke tests cover all three states on a list screen
- See docs/QUALITY_CHARTER.md.
## Labels
Type: fix · Area: lists · Priority: P1 · Size: M
**Labels:** fix, P1, M, lists
**Milestone:** Patch 0 — Orientation & Quality Bar
@@ -0,0 +1,24 @@
# Adopt Quality Charter (sign-off)
## Goal
Ratify the Quality Charter and set acceptance gates for Q-Blog 1.0.
## Tasks
- Review reliability, a11y, perf, security, and testability targets
- Adjust thresholds if needed
- Record sign-off and owners (Quality, A11y, Security)
## Acceptance
- Charter updated (if needed) and marked signed
- Owners assigned and documented
- See docs/QUALITY_CHARTER.md.
## Labels
Type: docs · Area: docs · Priority: P0 · Size: S
**Labels:** docs, P0, S, docs-area
**Milestone:** Patch 0 — Orientation & Quality Bar
@@ -0,0 +1,24 @@
# Correctness Sweep I — MUI v4 imports & ts-nocheck removal
## Goal
Remove known correctness hazards blocking development.
## Targets
- Replace `@material-ui/*` imports with `@mui/*`
- Remove `@ts-nocheck` in editor and app; add minimal types
- Ensure app still builds/boots after changes
## Acceptance
- No remaining v4 imports; no ts-nocheck in core paths
- Smoke tests green; manual run shows editor screen
- See docs/QUALITY_CHARTER.md.
## Labels
Type: fix · Area: editor · Priority: P0 · Size: M
**Labels:** fix, P0, M, editor
**Milestone:** Patch 0 — Orientation & Quality Bar
@@ -0,0 +1,24 @@
# Correctness Sweep II — Type hygiene in reducers/selectors
## Goal
Reduce `any` usage and adopt consistent async state shapes.
## Tasks
- Replace common `any` with discriminated unions/interfaces
- Standardize async lifecycles (idle/loading/success/error)
- Normalize error object shape (code/message/recoverable)
## Acceptance
- Reducer/selector tests added
- No new `any` in reducers/selectors; stricter types compile
- See docs/QUALITY_CHARTER.md.
## Labels
Type: fix · Area: state · Priority: P1 · Size: M
**Labels:** fix, P1, M, state
**Milestone:** Patch 0 — Orientation & Quality Bar
@@ -0,0 +1,24 @@
# Correctness Sweep III — Empty/Loading/Error states standardization
## Goal
Users never experience a “mystery fail.”
## Tasks
- Implement consistent empty/loading/error components
- Wire to data flows across list/detail/editor
- Ensure retry paths for transient failures
## Acceptance
- Error surfaces include a recovery action
- Smoke tests cover all three states on a list screen
- See docs/QUALITY_CHARTER.md.
## Labels
Type: fix · Area: lists · Priority: P1 · Size: M
**Labels:** fix, P1, M, lists
**Milestone:** Patch 0 — Orientation & Quality Bar
@@ -0,0 +1,24 @@
# ESLint Flat Config + jsx-a11y Baseline
## Goal
Catch accessibility and code-quality issues early.
## Tasks
- Add flat ESLint config with TypeScript/React/jsx-a11y
- Prettier integration; import/order; testing-library plugin
- Fix initial lint errors (no blanket disables)
## Acceptance
- `pnpm lint` green on clean checkout
- No `@ts-nocheck`; no new `any` in public props
- See docs/QUALITY_CHARTER.md.
## Labels
Type: chore · Area: build · Priority: P0 · Size: S
**Labels:** chore, P0, S, build
**Milestone:** Patch 0 — Orientation & Quality Bar
@@ -0,0 +1,26 @@
# Finalize Foundation Docs (Architecture, Testing, Accessibility, Security)
## Goal
Publish concise, source-of-truth docs to guide development.
## Includes
- ARCHITECTURE (UI ↔ state ↔ data, Name/Blog/Post model)
- TESTING (pyramid, coverage, fixtures/mocks, MSW policy)
- ACCESSIBILITY (landmarks, focus, live regions, motion/contrast)
- SECURITY & PRIVACY (sanitization, permission checks, CSP posture)
- USER_JOURNEYS & GLOSSARY (personas, invariants)
## Acceptance
- Docs present in `docs/`, linked from README
- Each doc has a short "How to verify" section
- See docs/QUALITY_CHARTER.md.
## Labels
Type: docs · Area: docs · Priority: P1 · Size: M
**Labels:** docs, P1, M, docs-area
**Milestone:** Patch 0 — Orientation & Quality Bar
+39
View File
@@ -0,0 +1,39 @@
{
"issues": [
{
"title": "Adopt Quality Charter (sign-off)",
"body": "## Goal\nRatify the Quality Charter and set acceptance gates for Q-Blog 1.0.\n\n## Tasks\n- Review reliability, a11y, perf, security, and testability targets\n- Adjust thresholds if needed\n- Record sign-off and owners (Quality, A11y, Security)\n\n## Acceptance\n- Charter updated (if needed) and marked signed\n- Owners assigned and documented\n- See docs/QUALITY_CHARTER.md.\n\n## Labels\nType: docs \u00b7 Area: docs \u00b7 Priority: P0 \u00b7 Size: S",
"labels": ["docs", "P0", "S", "docs-area"]
},
{
"title": "Finalize Foundation Docs (Architecture, Testing, Accessibility, Security)",
"body": "## Goal\nPublish concise, source-of-truth docs to guide development.\n\n## Includes\n- ARCHITECTURE (UI \u2194 state \u2194 data, Name/Blog/Post model)\n- TESTING (pyramid, coverage, fixtures/mocks, MSW policy)\n- ACCESSIBILITY (landmarks, focus, live regions, motion/contrast)\n- SECURITY & PRIVACY (sanitization, permission checks, CSP posture)\n- USER_JOURNEYS & GLOSSARY (personas, invariants)\n\n## Acceptance\n- Docs present in `docs/`, linked from README\n- Each doc has a short \"How to verify\" section\n- See docs/QUALITY_CHARTER.md.\n\n## Labels\nType: docs \u00b7 Area: docs \u00b7 Priority: P1 \u00b7 Size: M",
"labels": ["docs", "P1", "M", "docs-area"]
},
{
"title": "Stand up Test Harness (Vitest + RTL + MSW + jest-axe)",
"body": "## Goal\nEnable fast, realistic tests with accessibility checks.\n\n## Tasks\n- Configure Vitest (jsdom), alias @\u2192src, coverage thresholds\n- Global test setup with RTL, user-event, jest-dom, jest-axe\n- MSW server for success/error/invalid payloads\n- Seed tests: app smoke, editor minimal, a11y smoke\n\n## Acceptance\n- `pnpm test` runs green; coverage report produced\n- At least 3 smoke tests and 1 a11y test in place\n- See docs/QUALITY_CHARTER.md.\n\n## Labels\nType: test \u00b7 Area: tests \u00b7 Priority: P0 \u00b7 Size: M",
"labels": ["test", "P0", "M", "tests"]
},
{
"title": "ESLint Flat Config + jsx-a11y Baseline",
"body": "## Goal\nCatch accessibility and code-quality issues early.\n\n## Tasks\n- Add flat ESLint config with TypeScript/React/jsx-a11y\n- Prettier integration; import/order; testing-library plugin\n- Fix initial lint errors (no blanket disables)\n\n## Acceptance\n- `pnpm lint` green on clean checkout\n- No `@ts-nocheck`; no new `any` in public props\n- See docs/QUALITY_CHARTER.md.\n\n## Labels\nType: chore \u00b7 Area: build \u00b7 Priority: P0 \u00b7 Size: S",
"labels": ["chore", "P0", "S", "build"]
},
{
"title": "Correctness Sweep I \u2014 MUI v4 imports & ts-nocheck removal",
"body": "## Goal\nRemove known correctness hazards blocking development.\n\n## Targets\n- Replace `@material-ui/*` imports with `@mui/*`\n- Remove `@ts-nocheck` in editor and app; add minimal types\n- Ensure app still builds/boots after changes\n\n## Acceptance\n- No remaining v4 imports; no ts-nocheck in core paths\n- Smoke tests green; manual run shows editor screen\n- See docs/QUALITY_CHARTER.md.\n\n## Labels\nType: fix \u00b7 Area: editor \u00b7 Priority: P0 \u00b7 Size: M",
"labels": ["fix", "P0", "M", "editor"]
},
{
"title": "Correctness Sweep II \u2014 Type hygiene in reducers/selectors",
"body": "## Goal\nReduce `any` usage and adopt consistent async state shapes.\n\n## Tasks\n- Replace common `any` with discriminated unions/interfaces\n- Standardize async lifecycles (idle/loading/success/error)\n- Normalize error object shape (code/message/recoverable)\n\n## Acceptance\n- Reducer/selector tests added\n- No new `any` in reducers/selectors; stricter types compile\n- See docs/QUALITY_CHARTER.md.\n\n## Labels\nType: fix \u00b7 Area: state \u00b7 Priority: P1 \u00b7 Size: M",
"labels": ["fix", "P1", "M", "state"]
},
{
"title": "Correctness Sweep III \u2014 Empty/Loading/Error states standardization",
"body": "## Goal\nUsers never experience a \u201cmystery fail.\u201d\n\n## Tasks\n- Implement consistent empty/loading/error components\n- Wire to data flows across list/detail/editor\n- Ensure retry paths for transient failures\n\n## Acceptance\n- Error surfaces include a recovery action\n- Smoke tests cover all three states on a list screen\n- See docs/QUALITY_CHARTER.md.\n\n## Labels\nType: fix \u00b7 Area: lists \u00b7 Priority: P1 \u00b7 Size: M",
"labels": ["fix", "P1", "M", "lists"]
}
]
}
@@ -0,0 +1,25 @@
# Stand up Test Harness (Vitest + RTL + MSW + jest-axe)
## Goal
Enable fast, realistic tests with accessibility checks.
## Tasks
- Configure Vitest (jsdom), alias @→src, coverage thresholds
- Global test setup with RTL, user-event, jest-dom, jest-axe
- MSW server for success/error/invalid payloads
- Seed tests: app smoke, editor minimal, a11y smoke
## Acceptance
- `pnpm test` runs green; coverage report produced
- At least 3 smoke tests and 1 a11y test in place
- See docs/QUALITY_CHARTER.md.
## Labels
Type: test · Area: tests · Priority: P0 · Size: M
**Labels:** test, P0, M, tests
**Milestone:** Patch 0 — Orientation & Quality Bar
+5
View File
@@ -0,0 +1,5 @@
{
"title": "Patch 0 \u2014 Orientation & Quality Bar",
"description": "# Patch 0 \u2014 Orientation & Quality Bar\n_Generated 2025-08-16 23:43Z_\n\n## Goal\nRatify the Quality Charter and spin up the basic governance scaffolding before touching code.\n\n## Tasks\n1. **Adopt Quality Charter** \u2014 review, adjust targets if needed, and sign.\n2. **Label set** \u2014 create canonical labels in the tracker (Area, Type, Priority, Size).\n3. **Templates** \u2014 add Issue + PR templates with acceptance sections and a11y/security checks.\n4. **Milestone** \u2014 create `Patch 0` milestone with this checklist as description.\n5. **Backlog triage** \u2014 seed initial issues for Phase 1\u20133 planning (docs, harness, correctness).\n\n## Label Set (proposed)\n- **Type:** feat, fix, chore, docs, test, a11y, perf, security\n- **Area:** editor, lists, blogs, members, routing, state, build, tests, docs\n- **Priority:** P0, P1, P2\n- **Size:** XS, S, M, L, XL\n\n## Acceptance\n- Charter signed; templates merged; labels exist; milestone created; 5\u20138 seeded issues for Phase 1\u20133.\n",
"state": "open"
}
+9055 -3920
View File
File diff suppressed because it is too large Load Diff
+35 -6
View File
@@ -1,12 +1,23 @@
{
"name": "q-blog",
"private": true,
"version": "0.0.0",
"version": "0.3.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
"preview": "vite preview",
"lint": "eslint .",
"format": "prettier --check .",
"format:fix": "prettier --write .",
"test": "vitest run",
"test:run": "vitest run",
"typecheck": "tsc -p tsconfig.json -noEmit",
"lint:phase0": "LINT_SCOPE=phase0 eslint .",
"lint:full": "LINT_SCOPE=full eslint .",
"check": "npm run typecheck && npm run lint:phase0 && npm run test:run",
"scan:phase1": "bash scripts/dev/phase1-scan.sh",
"report:phase1": "bash scripts/dev/phase1-report.sh"
},
"dependencies": {
"@emotion/react": "^11.10.6",
@@ -41,14 +52,32 @@
"ts-key-enum": "^2.0.12"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@mui/types": "^7.2.3",
"@types/react": "^18.0.28",
"@testing-library/jest-dom": "^6.7.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/estree": "^1.0.8",
"@types/json-schema": "^7.0.15",
"@types/node": "^24.3.0",
"@types/react": "^18.3.23",
"@types/react-copy-to-clipboard": "^5.0.4",
"@types/react-dom": "^18.0.11",
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react-swc": "^3.2.0",
"prettier": "^2.8.6",
"typescript": "^4.9.3",
"@vitest/coverage-v8": "^3.2.4",
"axe-core": "^4.10.3",
"eslint": "^9.33.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react-hooks": "^5.2.0",
"jest-axe": "^10.0.0",
"jsdom": "^26.1.0",
"msw": "^2.10.5",
"prettier": "^2.8.8",
"typescript": "^4.9.5",
"typescript-eslint": "^8.39.1",
"vite": "^4.2.0",
"vitest": "^3.2.4",
"worker-loader": "^3.0.8"
}
}
+10
View File
@@ -0,0 +1,10 @@
# CI Enablement PR — Q-Blog
- Add `.gitea/workflows/ci.yml` (Node 20, lint, vitest).
- Add `ci-mirror.yml` fallback targeting `workflow_dispatch` and `ci/*` branches.
- Docs: `docs/CI_GITEA.md` for quick troubleshooting.
**Notes**
- `runs-on: self-hosted`. Ensure your runner exposes the `self-hosted` label.
- If your runner only matches `ubuntu-latest`, change `runs-on` accordingly.
+23
View File
@@ -0,0 +1,23 @@
**Title:** Phase 0 — Closeout (v0.0.1)
**Summary**
- Repo hygiene, docs, test harness, ESLint flat config, CI workflow
- No behavior changes
**Whats included**
- Docs: Charter, Instructions, Testing, Releasing
- Harness: Vitest + RTL + MSW + jest-axe (a11y smoke)
- CI: Gitea workflow (test + lint:phase0)
- Version: bump to `0.0.1`
**Verification**
- `npm run test` → green
- `npm run lint:phase0` → clean
- CI green on PR
**Notes**
- App code (`src/**`) linting deferred to Phase 13 sweeps.
+27
View File
@@ -0,0 +1,27 @@
# Phase 0 — Orientation & Quality Bar
**Summary**
- Add CI workflow (no marketplace actions)
- Establish lint/format/test baseline
- Seed a11y smoke + MSW harness
- Tracker hygiene scripts
**Changes**
- ESLint flat + jsx-a11y, Prettier, EditorConfig
- Vitest config (JSDOM, coverage, @ alias)
- tests: setup + a11y smoke + MSW
- tracker scripts: verify / labels dedupe / milestone rename
- CI: `.gitea/workflows/ci-no-marketplace.yml`
- Repo hygiene: `.nvmrc`, `.npmrc`, CODEOWNERS, CONTRIBUTING
**How to verify**
```bash
nvm use
pnpm install
pnpm typecheck && pnpm lint && pnpm test:run
```
Confirm CI runs on PR and shows typecheck/lint/test/build steps.
+20
View File
@@ -0,0 +1,20 @@
**Title:** Phase 1 — Step 1A: Remove `@ts-nocheck` headers
**Summary**
- Remove blanket `@ts-nocheck` headers to restore type safety.
- No behavior changes.
**Details**
- Files touched: see diff (e.g., `src/App.tsx`, `src/components/editor/BlogEditor.tsx`).
- Leave per-line todo comments in follow-ups instead of blanket disables.
**Verification**
- `scripts/dev/check.sh` (Vitest run + lint:phase0) → green
- App still builds locally (if applicable): `npm run dev` quick smoke
**Follow-ups (tracked in Phase 1 issues)**
- Type hygiene & rule violations addressed file-by-file.
+239
View File
@@ -0,0 +1,239 @@
# Q-Blog Phase-by-Phase File Guide
_Generated: 2025-08-16T23:24:16.656998Z_
This guide lists files to **edit** and **create** for each roadmap phase, with a short rationale per item. It aligns Q-Blog with conventions from Q-Place and Q-Chess.
## Phase 0
### Edit
- _No edits in this phase._
### Create
- **docs/QUALITY_CHARTER.md** — Quality goals, SLOs, protected journeys; living doc.
## Phase 1
### Edit
- **README.md** — If present: align quickstart/run/test steps with new tooling; link core docs.
### Create
- **docs/ARCHITECTURE.md** — High-level UI/state/data map; domain model for Name/Blog/Post.
- **docs/TESTING.md** — Pyramid, coverage thresholds, fixtures/mocks guidance.
- **docs/ACCESSIBILITY.md** — Landmarks, focus/focus return, live regions, motion/contrast, keyboard checklist.
- **docs/CONTRIBUTING.md** — Branch/commit, PR, code review; conventions adopted from Q-Place/Q-Chess.
- **docs/DECISIONS/ADR-0001-editor-stack.md** — Why Slate; sanitation constraints; alternatives considered.
- **docs/DECISIONS/ADR-0002-data-validation.md** — Choose Zod or JSON Schema/AJV; consequences for types/docs.
- **CHANGELOG.md** — Human-readable changes; keepers for releases.
## Phase 2
### Edit
- **package.json** — Add scripts (typecheck/lint/test/coverage), devDeps (Vitest/RTL/MSW/axe), align TS/Vite versions.
- **tsconfig.json** — Enable strict, noImplicitAny; add path alias '@/_' to 'src/_'.
- **vite.config.ts** — Add @ alias; ensure React plugin; keep SWC or standard plugin as chosen.
### Create
- **eslint.config.js** — Flat ESLint (TS, React, jsx-a11y); import/order; testing-library plugin.
- **vitest.config.ts** — jsdom env, alias '@'→ 'src', coverage thresholds, setup file.
- **tests/setup.ts** — RTL matchers, MSW server, jest-axe/axe; test-time polyfills.
- **tests/smoke/app.smoke.test.tsx** — Top-level route rendering smoke test.
- **tests/a11y/home.a11y.test.tsx** — axe checks for home and one form page.
## Phase 3
### Edit
- **src/components/editor/BlogEditor.tsx** — Replace @material-ui/_ with @mui/_ equivalents; fix icon imports.
- **src/App.tsx** — Remove '@ts-nocheck'; add minimal types or local @ts-expect-error with rationale.
- **src/components/editor/BlogEditor.tsx** — Remove '@ts-nocheck'; add minimal types or local @ts-expect-error with rationale.
- **src/components/AudioElement.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/DynamicHeightItem.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/DynamicHeightItemMinimal.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/FileElement.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/common/AudioPlayer.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/common/AudioPublishModal.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/common/Comments/Comment.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/common/Comments/CommentEditor.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/common/Comments/CommentSection.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/common/ContextMenu/ContextMenuResource.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/common/DownloadTaskManager.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/common/GenericPublishModal.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/common/ImagePanel.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/common/PollPanel.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/common/PollWidget.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/common/PostPublishModal.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/common/PublishAudio.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/common/PublishGeneric.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/common/PublishVideo.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/common/Tipping/Tipping.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/common/UserNavbar/UserNavbar.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/common/VideoPlayer.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/common/VideoPublishModal.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/editor/BlogEditor.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/editor/ReadOnlySlate.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/layout/Navbar/Navbar.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/modals/EditBlogModal.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/modals/PublishBlogModal.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/components/modals/ReusableModal.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/global.d.ts** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/hooks/useFetchPosts.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/interfaces/interfaces.ts** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/main.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/pages/BlogIndividualPost/BlogIndividualPost.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/pages/BlogIndividualProfile/BlogIndividualProfile.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/pages/BlogList/PostPreview.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/pages/CreatePost/CreatePostBuilder.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/pages/CreatePost/CreatePostMinimal.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/pages/CreatePost/components/Navbar/NavbarBuilder.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/pages/CreatePost/components/Toolbar/EditorToolbar.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/pages/EditPost/EditPost.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/state/features/blogSlice.ts** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/state/features/globalSlice.ts** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/state/features/mailSlice.ts** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/utils/checkStructure.ts** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/utils/extractTextFromSlate.ts** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/utils/fetchMail.ts** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/utils/fetchPosts.ts** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/utils/qortalRequestFunctions.ts** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/utils/toBase64.ts** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/wrappers/DownloadWrapper.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/wrappers/GlobalWrapper.tsx** — Tighten ': any' types—start with reducers/selectors/editor props.
- **src/pages/BlogIndividualProfile/BlogIndividualProfile.tsx** — Remove or guard console.log behind debug util.
- **src/wrappers/GlobalWrapper.tsx** — Remove or guard console.log behind debug util.
- **src/components/FileElement.tsx** — Remove or guard console.log behind debug util.
- **src/utils/fetchMail.ts** — Remove or guard console.log behind debug util.
- **src/hooks/useIframe.tsx** — Remove or guard console.log behind debug util.
- **src/components/common/PollWidget.tsx** — Remove eslint-disable; fix rule (deps/exhaustive-deps, etc.).
- **src/wrappers/GlobalWrapper.tsx** — Wrap app with ErrorBoundary; verify provider order; remove logs.
- **index.html** — Verify lang, meta, and root element a11y (no functional change).
### Create
- **src/components/system/ErrorBoundary.tsx** — Top-level error boundary with fallback UI and error reporting hook.
## Phase 4
### Edit
- **src/pages/BlogIndividualPost/BlogIndividualPost.tsx** — Add meaningful alt text or alt='' for decorative images.
- **src/pages/CreatePost/CreatePostMinimal.tsx** — Add meaningful alt text or alt='' for decorative images.
- **src/pages/CreatePost/CreatePostBuilder.tsx** — Add meaningful alt text or alt='' for decorative images.
- **src/pages/EditPost/EditPost.tsx** — Add meaningful alt text or alt='' for decorative images.
- **src/components/common/ImagePanel.tsx** — Add meaningful alt text or alt='' for decorative images.
- **src/components/common/ResponsiveImage.tsx** — Add meaningful alt text or alt='' for decorative images.
- **src/components/common/Tipping/Tipping.tsx** — Add meaningful alt text or alt='' for decorative images.
- **src/App.tsx** — Ensure page landmarks and skip link mount; set document titles per route.
- **src/components/common/\*** — Normalize labels, aria-\* states; ensure focus-visible rings remain visible.
- **src/components/modals/\*** — Trap focus, restore on close; Esc to dismiss.
### Create
- **src/components/a11y/SkipToContentLink.tsx** — Visible on focus; anchors main content.
- **src/components/a11y/LiveRegion.tsx** — Polite status updates (saving, uploaded, failed).
- **tests/a11y/keyboard.journeys.test.tsx** — Tab order, focus trap/restore across protected journeys.
## Phase 5
### Edit
- **src/components/layout/Header.tsx** — Clarify nav, add Blog switch entry placeholder, consistent naming.
- **src/components/layout/Nav.tsx** — Primary/secondary nav separation; active route indication.
- **src/components/editor/Toolbar.tsx** — Deterministic enable/disable states; accessible names.
- **src/components/editor/EditorSurface.tsx** — Autosave status UI, debounced save feedback.
- **src/pages/\*** — Consistent empty/error state components with clear actions.
### Create
- **src/components/feedback/Toast.tsx** — Unified success/error toasts (MUI Snackbar pattern).
- **src/components/states/EmptyState.tsx** — Shared empty-state component with title/body/actions.
- **src/components/states/ErrorBanner.tsx** — Shared error banner with retry hook.
- **tests/ui/patterns.test.tsx** — Ensure consistent empty/error/toast patterns across pages.
## Phase 6
### Edit
- **src/pages/PostList.tsx** — Scope list to current blog; update queries/selectors.
- **src/pages/CreatePost.tsx** — Bind creation to current blog; default blog auto-select.
- **src/wrappers/GlobalWrapper.tsx** — Provide current blog context from state for children.
- **src/routes/\*** — Reflect blog handle in routes (router params).
- **docs/ARCHITECTURE.md** — Update domain model and routing implications.
### Create
- **src/features/blogs/domain.ts** — Blog entity types; handle normalization/validation.
- **src/features/blogs/api.ts** — listBlogs/createBlog endpoints (RTK Query).
- **src/features/blogs/state.ts** — Current blog state, selectors, actions.
- **src/components/blogs/BlogSwitcher.tsx** — Switcher in header; shows current blog; keyboard-friendly.
- **src/components/blogs/CreateBlogDialog.tsx** — Create flow with handle validation preview.
- **tests/blogs/blog-switcher.test.tsx** — Switcher interaction and scoping of lists.
- **tests/blogs/migration.test.ts** — Legacy data migration to default blog per name.
## Phase 7
### Edit
- **src/components/posts/PostEditor.tsx** — Gate edit/publish controls by role; show attribution.
- **src/components/posts/PostListItem.tsx** — Show author/updatedBy; role-sensitive actions.
- **docs/DECISIONS/ADR-0002-permissions-model.md** — Fill in finalized operation matrix and tradeoffs.
### Create
- **src/features/permissions/roles.ts** — Role constants and operation matrix.
- **src/features/permissions/can.ts** — `can(op, ctx)` guard utilities.
- **src/features/memberships/api.ts** — invite/accept/remove/list endpoints.
- **src/pages/BlogMembers.tsx** — Membership management UI, Owner-only actions.
- **tests/permissions/matrix.test.ts** — Table-driven role × operation tests.
- **tests/permissions/ui-guards.test.tsx** — Controls hidden/disabled for insufficient roles.
## Phase 8
### Edit
- **src/components/lists/VirtualizedList.tsx** — Ensure virtualization for long lists (react-virtuoso).
- **src/features/\*/api.ts** — Adopt retry/backoff for idempotent GET; idempotency keys for mutations where applicable.
- **src/components/uploads/\*** — Make uploads abortable; show progress via LiveRegion.
### Create
- **src/lib/net/fetchJson.ts** — Abortable fetch with timeout and structured errors.
- **src/lib/net/retry.ts** — Backoff strategy; idempotency helpers.
- **src/components/states/SkeletonList.tsx** — Skeletons for perceived speed on lists.
- **tests/perf/optimistic-updates.test.ts** — Rollback on failure; consistent UI after retry.
## Phase 9
### Edit
- **src/theme/tokens.ts** — Centralize color/spacing/typography tokens; enforce contrast.
- **src/components/layout/\*** — Replace literals with t('…'); ensure logical properties for RTL prep.
### Create
- **src/i18n/index.ts** — i18next init; namespace scaffolding.
- **src/i18n/en/common.json** — Default strings.
- **tests/i18n/formatting.test.ts** — Plural/date/number formatting sanity.
## Phase 10
### Edit
- **src/components/editor/Renderer.tsx** — Sanitize HTML on render.
- **src/components/editor/Serializer.ts** — Sanitize on save/serialize pipeline.
- **docs/QUALITY_CHARTER.md** — Mark budgets and acceptance as met; note metrics.
### Create
- **src/components/system/RouteErrorBoundary.tsx** — Route-level error boundaries with friendly fallback.
- **src/lib/logging/clientLog.ts** — Client error/event logger with throttling.
- **src/lib/security/sanitizeHtml.ts** — DOMPurify wrapper with allowlist for editor output.
- **docs/RELEASE_NOTES.md** — Narrative changes per release.
- **docs/UPGRADE_GUIDE.md** — Breaking changes and migration steps.
- **.gitea/workflows/ci.yml** — Install/cache, typecheck, lint, tests, coverage artifacts.
+231
View File
@@ -0,0 +1,231 @@
# Q-Blog Structure & Edit Plan
_Generated: 2025-08-16T23:19:33.635026Z_
## Conventions reference (from sister apps)
- Q-Place/Q-Chess use **eslint.config.js**, **vitest.config.ts**, **tests/setup.ts**, and a **docs/** folder with architecture/decisions.
- CI lives under **.gitea/**; tests run with Vitest + React Testing Library; a11y linting via jsx-a11y and axe checks.
## Full file list (current repository)
```text
.gitignore
.prettierrc.json
index.html
package-lock.json
package.json
public/favicon.ico
src/App.tsx
src/assets/img/arrr.png
src/assets/img/btc.png
src/assets/img/dgb.png
src/assets/img/doge.png
src/assets/img/ltc.png
src/assets/img/qBlogLogo.png
src/assets/img/qort.png
src/assets/img/rvn.png
src/assets/react.svg
src/assets/svgs/AccountCircleSVG.tsx
src/assets/svgs/AlignCenterSVG.tsx
src/assets/svgs/AlignLeftSVG.tsx
src/assets/svgs/AlignRightSVG.tsx
src/assets/svgs/BoldSVG.tsx
src/assets/svgs/CodeBlockSVG.tsx
src/assets/svgs/H2SVG.tsx
src/assets/svgs/H3SVG.tsx
src/assets/svgs/ItalicSVG.tsx
src/assets/svgs/LinkSVG.tsx
src/assets/svgs/NewWindowSVG.tsx
src/assets/svgs/UnderlineSVG.tsx
src/assets/svgs/accountCircle.svg
src/assets/svgs/interfaces.ts
src/components/AudioElement.tsx
src/components/DynamicHeightItem.tsx
src/components/DynamicHeightItemMinimal.tsx
src/components/FileElement.tsx
src/components/common/AudioPanel.tsx
src/components/common/AudioPlayer.tsx
src/components/common/AudioPublishModal.tsx
src/components/common/BlockedNamesModal/BlockedNamesModal-styles.ts
src/components/common/BlockedNamesModal/BlockedNamesModal.tsx
src/components/common/Comments/Comment.tsx
src/components/common/Comments/CommentEditor.tsx
src/components/common/Comments/CommentSection.tsx
src/components/common/ContextMenu/ContextMenuResource.tsx
src/components/common/CustomIcon.tsx
src/components/common/DownloadTaskManager.tsx
src/components/common/DraggableResizableGrid.tsx
src/components/common/ErrorBoundary.tsx
src/components/common/FilePanel.tsx
src/components/common/GenericPublishModal.tsx
src/components/common/ImagePanel.tsx
src/components/common/ImageUploader.tsx
src/components/common/LazyLoad.tsx
src/components/common/Notification/Notification.tsx
src/components/common/PageLoader.tsx
src/components/common/PollPanel.tsx
src/components/common/PollWidget.tsx
src/components/common/Portal.tsx
src/components/common/PostPublishModal.tsx
src/components/common/PublishAudio.tsx
src/components/common/PublishGeneric.tsx
src/components/common/PublishVideo.tsx
src/components/common/ResponsiveImage.tsx
src/components/common/Tipping/Tipping.tsx
src/components/common/UserNavbar/UserNavbar-styles.ts
src/components/common/UserNavbar/UserNavbar.tsx
src/components/common/VideoContent.tsx
src/components/common/VideoPanel.tsx
src/components/common/VideoPlayer.tsx
src/components/common/VideoPublishModal.tsx
src/components/editor/BlogEditor.css
src/components/editor/BlogEditor.tsx
src/components/editor/ReadOnlySlate.tsx
src/components/editor/customTypes.ts
src/components/layout/Navbar/Navbar-styles.ts
src/components/layout/Navbar/Navbar.tsx
src/components/modals/ConsentModal.tsx
src/components/modals/EditBlogModal.tsx
src/components/modals/PublishBlogModal.tsx
src/components/modals/ReusableModal.tsx
src/constants/mail.ts
src/global.d.ts
src/hooks/useFetchPosts.tsx
src/hooks/useIframe.tsx
src/index.css
src/index.d.ts
src/interfaces/interfaces.ts
src/main.tsx
src/pages/BlogIndividualPost/BlogIndividualPost.tsx
src/pages/BlogIndividualProfile/BlogIndividualProfile.tsx
src/pages/BlogList/BlogList.tsx
src/pages/BlogList/PostPreview-styles.ts
src/pages/BlogList/PostPreview.tsx
src/pages/CreateEditProfile/CreatEditProfile.tsx
src/pages/CreatePost/CreatePost-styles.ts
src/pages/CreatePost/CreatePost.tsx
src/pages/CreatePost/CreatePostBuilder.tsx
src/pages/CreatePost/CreatePostMinimal.tsx
src/pages/CreatePost/components/Navbar/NavbarBuilder.tsx
src/pages/CreatePost/components/Toolbar/EditorToolbar.tsx
src/pages/EditPost/EditPost.tsx
src/pages/Home/Home.tsx
src/state/features/authSlice.ts
src/state/features/blogSlice.ts
src/state/features/globalSlice.ts
src/state/features/mailSlice.ts
src/state/features/notificationsSlice.ts
src/state/store.ts
src/styles/fonts/Cairo.ttf
src/styles/fonts/Cambon-Light.ttf
src/styles/fonts/Catamaran.ttf
src/styles/fonts/Oxygen.ttf
src/styles/fonts/Raleway.ttf
src/styles/theme.ts
src/utils/blogIdformats.ts
src/utils/checkAndUpdatePost.tsx
src/utils/checkStructure.ts
src/utils/extractTextFromSlate.ts
src/utils/fetchMail.ts
src/utils/fetchPosts.ts
src/utils/qortalRequestFunctions.ts
src/utils/time.ts
src/utils/toBase64.ts
src/vite-env.d.ts
src/webworkers/decodeBase64.js
src/webworkers/getBlogWorker.js
src/wrappers/DownloadWrapper.tsx
src/wrappers/GlobalWrapper.tsx
tsconfig.json
vite.config.ts
```
## Files to EDIT (with reasons)
- **index.html** — Verify a11y root attributes and mount point; no functional change expected.
- **package.json** — Add test/lint scripts; add Vitest/RTL/axe; upgrade TypeScript/Vite to match sister apps; add eslint/jsx-a11y.
- **src/App.tsx** — Remove '@ts-nocheck'; add minimal types/props.
- **src/components/AudioElement.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/DynamicHeightItem.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/DynamicHeightItemMinimal.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/FileElement.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.; Remove stray 'console.log' or guard behind debug flag.
- **src/components/common/AudioPlayer.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/common/AudioPublishModal.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/common/Comments/Comment.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/common/Comments/CommentEditor.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/common/Comments/CommentSection.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/common/ContextMenu/ContextMenuResource.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/common/DownloadTaskManager.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/common/GenericPublishModal.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/common/ImagePanel.tsx** — Ensure <img> has alt (or alt='') and semantic context.; Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/common/PollPanel.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/common/PollWidget.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.; Remove 'eslint-disable' and fix underlying lint issues.
- **src/components/common/PostPublishModal.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/common/PublishAudio.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/common/PublishGeneric.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/common/PublishVideo.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/common/ResponsiveImage.tsx** — Ensure <img> has alt (or alt='') and semantic context.
- **src/components/common/Tipping/Tipping.tsx** — Ensure <img> has alt (or alt='') and semantic context.; Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/common/UserNavbar/UserNavbar.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/common/VideoPlayer.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/common/VideoPublishModal.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/editor/BlogEditor.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.; Remove '@ts-nocheck'; add minimal types/props.; Replace MUI v4 import with '@mui/material' (or icons).
- **src/components/editor/ReadOnlySlate.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/layout/Navbar/Navbar.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/modals/EditBlogModal.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/modals/PublishBlogModal.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/components/modals/ReusableModal.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/global.d.ts** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/hooks/useFetchPosts.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/hooks/useIframe.tsx** — Remove stray 'console.log' or guard behind debug flag.
- **src/interfaces/interfaces.ts** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/main.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/pages/BlogIndividualPost/BlogIndividualPost.tsx** — Ensure <img> has alt (or alt='') and semantic context.; Reduce ': any' usage; add precise types for props/state/selectors.
- **src/pages/BlogIndividualProfile/BlogIndividualProfile.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.; Remove stray 'console.log' or guard behind debug flag.
- **src/pages/BlogList/PostPreview.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/pages/CreatePost/CreatePostBuilder.tsx** — Ensure <img> has alt (or alt='') and semantic context.; Reduce ': any' usage; add precise types for props/state/selectors.
- **src/pages/CreatePost/CreatePostMinimal.tsx** — Ensure <img> has alt (or alt='') and semantic context.; Reduce ': any' usage; add precise types for props/state/selectors.
- **src/pages/CreatePost/components/Navbar/NavbarBuilder.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/pages/CreatePost/components/Toolbar/EditorToolbar.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/pages/EditPost/EditPost.tsx** — Ensure <img> has alt (or alt='') and semantic context.; Reduce ': any' usage; add precise types for props/state/selectors.
- **src/state/features/blogSlice.ts** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/state/features/globalSlice.ts** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/state/features/mailSlice.ts** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/utils/checkStructure.ts** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/utils/extractTextFromSlate.ts** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/utils/fetchMail.ts** — Reduce ': any' usage; add precise types for props/state/selectors.; Remove stray 'console.log' or guard behind debug flag.
- **src/utils/fetchPosts.ts** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/utils/qortalRequestFunctions.ts** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/utils/toBase64.ts** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/wrappers/DownloadWrapper.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.
- **src/wrappers/GlobalWrapper.tsx** — Reduce ': any' usage; add precise types for props/state/selectors.; Remove stray 'console.log' or guard behind debug flag.; Replace console logs; verify provider order; add error boundary if missing.
- **tsconfig.json** — Enable strict mode; add path aliases; align compiler options with sister apps.
- **vite.config.ts** — Add path alias '@' → 'src'; keep React SWC; verify base path.
## Files to RENAME
- **src/pages/CreateEditProfile/CreatEditProfile.tsx → src/pages/CreateEditProfile/CreateEditProfile.tsx** — Fix spelling; align filename with folder and imports.
## New files to ADD (to align with Q-Place/Q-Chess)
- **eslint.config.js** — Flat ESLint config with TypeScript, React, jsx-a11y; shared rules.
- **vitest.config.ts** — Vitest setup (jsdom), coverage thresholds, alias '@' to 'src'.
- **tests/setup.ts** — Global test setup: RTL matchers, MSW, jest-axe/axe-core.
- **tests/smoke/app.smoke.test.tsx** — Smoke render of top-level routes (sanity).
- **tests/a11y/home.a11y.test.tsx** — axe checks for key pages; keyboard path checks.
- **docs/ARCHITECTURE.md** — UI ↔ state ↔ data flows; blog/post domain model.
- **docs/ACCESSIBILITY.md** — Landmarks, focus policy, live regions, motion/contrast.
- **docs/TESTING.md** — Pyramid, coverage policy, fixtures/mocks guidance.
- **docs/DECISIONS/ADR-0001-editor-stack.md** — Why Slate; sanitation rules.
- **docs/DECISIONS/ADR-0002-permissions-model.md** — Roles and operation matrix (later phase).
- **.gitea/workflows/ci.yml** — CI: install/cache, typecheck, lint, tests, coverage artifact.
- **scripts/check-a11y.mjs** — Optional: batch axe checks for critical routes.
- **docs/RELEASE_NOTES.md** — Human-readable changes per release.
## Notes
- Many files flagged for `: any` should be addressed opportunistically: start with reducers/selectors and editor props, then components used across pages.
- For `<img>` elements, provide descriptive `alt` where meaningful; use `alt=""` (decorative) when the image conveys no unique information.
- Replace the single `eslint-disable` by fixing the underlying pattern (likely a hook dependency or exhaustive-deps issue).
- Consider adding an **ErrorBoundary** at the top-level wrapper if not present.
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail
APPLY="${APPLY:-0}"
CANDIDATES=(
"$HOME/.cache/act/actions"
"$HOME/.cache/actions"
"$HOME/_actions"
"/var/lib/act_runner/data/actions"
"/var/lib/act_runner/_actions"
)
echo "== Candidate action cache directories =="
for d in "${CANDIDATES[@]}"; do
if [ -d "$d" ]; then
echo "FOUND: $d"
else
echo "MISS: $d"
fi
done
if [ "$APPLY" != "1" ]; then
echo "DRY RUN. Set APPLY=1 to delete any FOUND directories."
exit 0
fi
for d in "${CANDIDATES[@]}"; do
if [ -d "$d" ]; then
echo "Deleting $d"
rm -rf "$d"
fi
done
echo "Done. Restart your runner service."
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -euo pipefail
MODE="${1:-phase0}"
PHASE0_CONTENT="$(cat <<'EOF'
# Phase 0: limit lint to harness/docs/scripts; app code will be addressed in Phase 13 sweeps.
node_modules/
dist/
coverage/
.vite/
.gitea/
# Ignore full app sources during Phase 0
src/**
# Type definitions are not linted
**/*.d.ts
# Build artifacts & caches
*.log
EOF
)"
FULL_CONTENT="$(cat <<'EOF'
node_modules/
dist/
coverage/
.vite/
.gitea/
**/*.d.ts
*.log
EOF
)"
case "$MODE" in
phase0)
echo "$PHASE0_CONTENT" > .eslintignore
echo "Applied Phase 0 lint scope (src/** ignored)."
;;
full)
echo "$FULL_CONTENT" > .eslintignore
echo "Applied FULL lint scope (src/** included)."
;;
*)
echo "Usage: $0 [phase0|full]" >&2
exit 1
;;
esac
# Show effective ignore
echo "== .eslintignore =="
cat .eslintignore
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
new_version="${1-}"
if [ -z "${new_version}" ]; then
echo "Usage: scripts/dev/bump-version.sh <new-version>"
echo "Examples: 0.0.1 (Phase 0), 0.1.0 (Phase 1)"
exit 1
fi
tmp=$(mktemp)
jq --arg v "$new_version" '.version=$v' package.json > "$tmp"
mv "$tmp" package.json
git add package.json
echo "Version set to ${new_version} in package.json (staged)."
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
echo "== Tests (vitest run) =="
CI=1 npx vitest run
echo "== Lint (phase0 scope) =="
npm run lint:phase0

Some files were not shown because too many files have changed in this diff Show More