forked from Qortal/q-blog
15 KiB
15 KiB
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
{
"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)
{}
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
- sample
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)
#!/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)
#!/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
- Content sanitization (DOMPurify) not detected in source scan. Verify sanitize-on-save and sanitize-on-render are implemented.
- Zod validation not detected; confirm validation at form and API boundaries or update docs.
- Testing stack may be incomplete (MSW/jest-axe). Ensure test coverage for autosave, a11y smoke, and error paths.
- RTK Query not detected; if used, ensure endpoints are defined with blog-scoped cache keys.
Next Steps (thin vertical slice)
- Run
pnpm i(or npm/yarn) andpnpm testto confirm toolchain and a11y smoke tests. - Verify PostEditor autosave path, live region announcements, and sanitize pipeline.
- Resolve release script guard/arg issues; add CI job to create release on tag.
- Pick a small feature change and map the affected slice (state → API → UI → tests).