diff --git a/.github/workflows/cms.yml b/.github/workflows/cms.yml index 8c8e8bf..53eb20b 100644 --- a/.github/workflows/cms.yml +++ b/.github/workflows/cms.yml @@ -47,6 +47,7 @@ jobs: exit 1 fi psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f src/db/migrations/0003_cms/up.sql + psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f src/db/migrations/0004_public_core/up.sql - run: npm run typecheck - run: npm run build - name: Start synthetic CMS web process diff --git a/.github/workflows/identity.yml b/.github/workflows/identity.yml index b74f202..00dbd97 100644 --- a/.github/workflows/identity.yml +++ b/.github/workflows/identity.yml @@ -50,6 +50,8 @@ jobs: exit 1 fi psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f src/db/migrations/0002_identity/up.sql + psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f src/db/migrations/0003_cms/up.sql + psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f src/db/migrations/0004_public_core/up.sql - run: npm run typecheck - run: npm run build - name: Start synthetic web process diff --git a/.github/workflows/pages-preview.yml b/.github/workflows/pages-preview.yml new file mode 100644 index 0000000..03fb1e9 --- /dev/null +++ b/.github/workflows/pages-preview.yml @@ -0,0 +1,43 @@ +name: Phase 5 GitHub Pages preview +on: + push: + branches: [phase-05/public-core] + workflow_dispatch: +permissions: + contents: read + pages: write + id-token: write +concurrency: + group: phase-05-pages-preview + cancel-in-progress: false +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - name: Validate owner-confirmed editorial copy + run: node scripts/check_public_copy.mjs + - name: Build bilingual noindex preview + run: python3 scripts/build_pages_preview.py + - name: Check generated pages and publication boundaries + run: | + python3 - <<'PY' + from pathlib import Path + root = Path('pages-preview-output') + pages = list(root.glob('*/**/index.html')) + assert len(pages) == 20, len(pages) + for page in pages: + text = page.read_text(encoding='utf-8') + assert 'name="robots" content="noindex, nofollow"' in text, page + assert '- + --health-cmd pg_isready + --health-interval 5s + --health-timeout 5s + --health-retries 10 + env: + DATABASE_URL: postgres://onskillit:onskillit@localhost:5432/onskillit + PUBLIC_SITE_URL: https://example.test + PUBLIC_INDEXING_ENABLED: 'true' + INQUIRY_RATE_SECRET: synthetic-ci-only-secret-with-thirty-two-characters + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: '24' + cache: npm + - run: npm ci + - name: Validate owner-confirmed editorial drafts + run: node scripts/check_public_copy.mjs + - run: npm run check:api + - name: Apply reviewed migrations + run: | + psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f src/db/migrations/0001_foundation_probe/up.sql + psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f src/db/migrations/0002_identity/up.sql + psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f src/db/migrations/0003_cms/up.sql + psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f src/db/migrations/0004_public_core/up.sql + - name: Verify Phase 5 migration reversal and reapply + run: | + psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f src/db/migrations/0004_public_core/down.sql + if psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -c 'select 1 from public_inquiry limit 0' > /dev/null 2>&1; then + echo 'public_inquiry survived down migration' >&2 + exit 1 + fi + psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f src/db/migrations/0004_public_core/up.sql + - run: npm run typecheck + - run: npm run build + - name: Start synthetic public web process + run: | + npm start > /tmp/onskillit-public.log 2>&1 & + for attempt in $(seq 1 30); do + if curl --fail --silent http://localhost:3000/api/v1/health > /dev/null; then break; fi + sleep 1 + done + - name: Exercise publication and inquiry boundaries + run: node scripts/public_ci.mjs + - name: Install Playwright Chromium + run: npx playwright install --with-deps chromium + - name: Exercise bilingual public pages in browser + run: npx playwright test playwright/public.spec.ts + - name: Upload synthetic public page previews + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: public-page-previews + path: test-results/**/*.png + if-no-files-found: ignore + retention-days: 14 diff --git a/.gitignore b/.gitignore index bcab2ec..a046a99 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,5 @@ __pycache__/ *.tsbuildinfo playwright-report/ test-results/ +pages-preview-output/ .DS_Store diff --git a/.repo/phases.yaml b/.repo/phases.yaml index ee5dda2..999ac84 100644 --- a/.repo/phases.yaml +++ b/.repo/phases.yaml @@ -2,7 +2,7 @@ # Kept in lockstep with docs/PHASES.md (enforced by scripts/check_docs_architecture.py). phases: - name: PHASE-00 - title: Foundational Discovery & Specification + title: Discovery and documentation gate: DOCUMENTATION READY status: completed completed_at: '2026-09-25' @@ -10,7 +10,7 @@ phases: run_id: '36045402016' evidence_path: docs/READINESS.md - name: PHASE-01 - title: Repository, Architecture, and Delivery Foundation + title: GitHub foundation gate: FOUNDATION VERIFIED status: completed completed_at: '2026-09-28' @@ -18,15 +18,15 @@ phases: run_id: '36446181731' evidence_path: docs/PHASE-01-WORKLOG.md - name: PHASE-02 - title: Design System, Tokens, Theme, and App Shell - gate: SHELL VERIFIED + title: Design and localization shell + gate: EXPERIENCE FOUNDATION VERIFIED status: completed completed_at: '2026-09-28' commit: 627ce274011ff7f62c915058fd2fc8c9ac965f8d run_id: '36461586655' evidence_path: docs/PHASE-02-WORKLOG.md - name: PHASE-03 - title: Authentication, Authorization, and User Core + title: Identity, RBAC and audit gate: IDENTITY VERIFIED status: completed completed_at: '2026-09-29' @@ -34,7 +34,7 @@ phases: run_id: '36562919856' evidence_path: docs/PHASE-03-WORKLOG.md - name: PHASE-04 - title: Public Site, CMS, Editorial, Media, and SEO Core + title: CMS and publishing core gate: PUBLISHING CORE VERIFIED status: completed completed_at: '2026-09-29' @@ -42,46 +42,46 @@ phases: run_id: '36571645321' evidence_path: docs/PHASE-04-WORKLOG.md - name: PHASE-05 - title: Core Domain Workflows — Public Core + title: Public business site gate: PUBLIC CORE VERIFIED - status: not_started + status: in_progress - name: PHASE-06 - title: Core Domain Workflows — Agency & Commercial - gate: AGENCY CORE VERIFIED + title: LMS and course catalog + gate: LEARNING VERIFIED status: not_started - name: PHASE-07 - title: Payments, Orders, Invoicing, and Entitlements - gate: COMMERCE VERIFIED + title: TMS and training catalog + gate: TRAINING VERIFIED status: not_started - name: PHASE-08 - title: Learning Delivery, Enrollments, Progress, Certificates, and Verification - gate: LEARNING DELIVERY VERIFIED + title: Enrollment and finance + gate: COMMERCE VERIFIED status: not_started - name: PHASE-09 - title: Dashboards, Admin Console, Operations, and Support - gate: OPERATIONS VERIFIED + title: CRM and client operations + gate: CLIENT OPERATIONS VERIFIED status: not_started - name: PHASE-10 - title: Notifications, Search, Exports, and Background Jobs - gate: ASYNC & DISCOVERY VERIFIED + title: Student and client portals + gate: PORTALS VERIFIED status: not_started - name: PHASE-11 - title: Security Hardening, Abuse Controls, Privacy, and Audit - gate: SECURITY VERIFIED + title: Content and business operations + gate: OPERATIONS VERIFIED status: not_started - name: PHASE-12 - title: Accessibility, Responsive UX, Localization, and Content Polish - gate: UX & ACCESSIBILITY VERIFIED + title: Cross-system hardening + gate: HARDENING VERIFIED status: not_started - name: PHASE-13 - title: Performance, Observability, Backup/Restore, and Release Readiness - gate: RELEASE CANDIDATE VERIFIED + title: Integrated QA and staging + gate: RELEASE CANDIDATE APPROVED status: not_started - name: PHASE-14 - title: Staging Verification, Production Cutover, and Post-Launch Runbooks + title: Production launch and stabilization gate: PRODUCTION ACCEPTED status: not_started - name: PHASE-15 - title: Post-V1 Controlled Evolution - gate: V1.x CHANGE GOVERNED + title: Governed evolution + gate: CHANGE-SPECIFIC GATE status: not_started diff --git a/AI_ASSISTANT.md b/AI_ASSISTANT.md index 230406d..6895b7e 100644 --- a/AI_ASSISTANT.md +++ b/AI_ASSISTANT.md @@ -1,5 +1,7 @@ # OnSkillIT agent working agreement +Current work: PHASE-05 public core is IN PROGRESS; PUBLIC CORE VERIFIED is open. See `docs/PHASE-05-WORKLOG.md` and the owner's classified content brief. The GitHub Pages editorial preview is live; server and production publication remain pending. + Status: PHASE-00 through PHASE-04 gates passed; PHASE-04 CMS and publishing core is CI-verified with PUBLISHING CORE VERIFIED passed on synthetic GitHub evidence. The founding partner approved the PHASE-03 plan on 2026-09-29 with mobile-only recovery deferred and disabled, reviewed/audited first-owner bootstrap, and verified-email-only reset. No production deployment, migration or change to `onskillit.com` occurred. The public GitHub repository is the project source of truth; agent chat history is not. At session start: read this file, `README.md`, `docs/PHASES.md`, `docs/PAGES.md`, `docs/TRACEABILITY.md`, `docs/READINESS.md`, `docs/README.md`, and only the task-relevant linked documents. Then discover canonical `soobujmiah/skb` at `ASSISTANT_CONTEXT.md` and `profile/assistant-guidance.md`, followed by task-relevant standards. Verify the SKB Git remote when using a local checkout. Compare dated claims with live repository and source evidence. Follow `standards/policy-resolution.md` for authorization. diff --git a/README.md b/README.md index 0491410..bf8621c 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,10 @@ **Machine-generated operational state:** see [`.repo/STATUS.md`](.repo/STATUS.md) (generated by `tools/repo_knowledge`). For cross-project SKB registry state, see [`onskillit-platform.json`](https://github.com/soobujmiah/skb/blob/main/projects/registry/onskillit-platform.json) in the parent SKB repository (`soobujmiah/skb`). # OnSkillIT Digital Platform — documentation workspace +Current work: PHASE-05 public core is IN PROGRESS with PUBLIC CORE VERIFIED open; see `docs/PHASE-05-WORKLOG.md`. + +The [GitHub Pages Phase 5 preview](https://soobujmiah.github.io/onskillit-platform/) presents confirmed English/Bangla business copy for review. It is static and noindex; the server application and external-domain deployment remain separate. + Status: PHASE-00 through PHASE-04 gates passed. PHASE-04 CMS and publishing core passed PUBLISHING CORE VERIFIED on synthetic GitHub evidence. See `docs/PHASE-04-WORKLOG.md` and `docs/CMS-PUBLISHING.md`. No production deployment or data migration has occurred. The public GitHub repository is `soobujmiah/onskillit-platform`. ![OnSkillIT Digital Platform Phase 0 architecture blueprint](docs/assets/onskillit-platform-architecture.svg) diff --git a/docs/API-CONTRACT.md b/docs/API-CONTRACT.md index 01ab574..95177fa 100644 --- a/docs/API-CONTRACT.md +++ b/docs/API-CONTRACT.md @@ -1,5 +1,7 @@ # API architecture and endpoint contract +PHASE-05 source contract (CI review in progress): anonymous `POST /api/v1/public/inquiries` is defined in [public OpenAPI](openapi/public.json) and implemented in `src/app/api/v1/public/inquiries/route.ts`. It accepts bounded JSON with name, email, optional phone, message, locale, current published privacy-revision consent and source path; it requires an HMAC rate secret and a configured database. A published contact and privacy page are required before storage. It returns 202 without a record ID, 400 for invalid input, 403 for origin mismatch, 429 for rate limiting and 503 when intake is unavailable. `public_inquiry` stores the minimal intake, while Phase 9 owns conversion to Lead. Server-rendered public pages query the published CMS revision directly through `src/lib/public-content.ts`; draft and staff preview data are not anonymous endpoints. + PHASE-01 implementation note: `GET /api/v1/health` is an unauthenticated liveness endpoint returning `{"status":"ok"}` with `Cache-Control: no-store`. When `DATABASE_URL` is configured it also attempts `select 1` and reports the result as `checks.database` (`"ok"` with HTTP 200, or `"unreachable"` with HTTP 503); when `DATABASE_URL` is absent — the case for the `web` CI job and any environment without a database — it returns the liveness-only body unchanged, so existing consumers are unaffected. It still does not check worker readiness; there is no cross-process readiness signal yet. Source: `src/app/api/v1/health/route.ts`, `src/db/client.ts`; GitHub smoke evidence recorded in [PHASE-01-WORKLOG](PHASE-01-WORKLOG.md). Status: **accepted V1 API architecture**, not implemented. Exact OpenAPI schemas are authored with each owning phase under ADRs 0006/0007/0009. API-first means each domain command/query has an explicit contract; server-rendered pages may call application services directly without making an HTTP loopback request. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e599cff..59e29fd 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,5 +1,7 @@ # Accepted platform architecture +PHASE-05 source implementation (CI review in progress): `src/lib/public-content.ts` is the public published-revision read boundary; `catalog_service` extends the CMS page aggregate for service category; `public_inquiry` is a separate append-only intake pending explicit Phase 9 Lead conversion. Public SEO/indexing requires both protected environment configuration and CMS approval. This is a Phase 5 module extension of the accepted modular monolith, without a production deployment change. + Implementation note (PHASE-01 COMPLETE, PHASE-02 in progress): `src/app/` contains a minimal Next.js shell and system boundaries; `next.config.ts` selects standalone output. `src/db/` holds a single infrastructure-only migration-pipeline table (not domain schema — see [DATA-MODEL](DATA-MODEL.md)) and `src/worker/` holds a minimal separately-runnable worker process, both proved by the `Database` and `Container` GitHub workflows. This foundation slice still does not establish a live deployed topology — CI builds and smoke-tests a Docker image in an isolated runner; it does not deploy anywhere. PHASE-02 adds `src/middleware.ts` (locale detection/redirect), `src/app/[locale]/` (locale-aware nested layout/page), `src/styles/tokens.css` (DESIGN-SYSTEM.md's token table as CSS custom properties) and `src/i18n/` (dictionary loader, no new i18n library) — see [PHASE-02-WORKLOG](PHASE-02-WORKLOG.md). See also [PHASE-01-WORKLOG](PHASE-01-WORKLOG.md). Status: **accepted Phase 0 blueprint, 2026-09-25**. ADRs [0006](decisions/0006-runtime-and-hosting-candidate.md), [0007](decisions/0007-browser-auth-and-rbac.md), [0008](decisions/0008-payment-adapter-contract.md) and [0009](decisions/0009-platform-services-and-operations.md) own the major decisions. This is documentation, not deployed software. diff --git a/docs/DATA-MODEL.md b/docs/DATA-MODEL.md index d0f85b6..c21d090 100644 --- a/docs/DATA-MODEL.md +++ b/docs/DATA-MODEL.md @@ -1,5 +1,7 @@ # Conceptual database architecture +PHASE-05 source contract (CI review in progress): reviewed SQL pair `src/db/migrations/0004_public_core/` permits the `service` CMS subtype and adds `catalog_service` with a required category, plus `public_inquiry`. The inquiry stores name, email, optional phone, message, locale, current published privacy revision and source path; it does not write a Phase 9 Lead. The record is immutable except for a single reviewed redaction that clears all four personal fields and leaves a minimal receipt with operator and request/review references. A database trigger records an append-only audit event for that transition. `src/db/schema.ts` mirrors these tables. Public rendering joins only a `cms_page` in `published` state to its `published_revision_id`, and service details resolve the locale-specific published slug. No production migration has occurred. + PHASE-01 implementation note: `src/db/schema.ts` and the reviewed SQL pair in `src/db/migrations/0001_foundation_probe/` add exactly one table, `foundation_probe`. It is infrastructure-only — it exists to prove the migration up/down pipeline and back the health endpoint's database check, and carries no business meaning. It is **not** the Identity/CMS/etc. physical schema; those tables remain owned by their respective phases (PHASE-03 Identity, PHASE-04 CMS, and so on) and are not created here. The `Database` GitHub workflow applies `up.sql`, verifies the table, applies `down.sql`, verifies removal, then reapplies `up.sql`. See [PHASE-01-WORKLOG](PHASE-01-WORKLOG.md). PHASE-03 branch implementation: `src/db/migrations/0002_identity/` and `src/db/schema.ts` add the identity tables listed in [PHASE-03-WORKLOG](PHASE-03-WORKLOG.md), including a pending/approved privileged grant request for independent approval. PostgreSQL uniqueness covers normalized contact values; sessions/tokens store hashes only; assignments are scope-bound and an audit trigger denies UPDATE/DELETE. No mobile recovery table exists. This is source and synthetic CI state, not a production migration. diff --git a/docs/DESIGN-SYSTEM.md b/docs/DESIGN-SYSTEM.md index 80015b6..5ec3722 100644 --- a/docs/DESIGN-SYSTEM.md +++ b/docs/DESIGN-SYSTEM.md @@ -12,7 +12,7 @@ Portfolio source (`soobujmiah.github.io/DESIGN_SYSTEM.md`) uses near-black `#050 Separate foundation tokens (color scales, type, spacing, radii, elevation, motion) from semantic tokens (`surface.canvas`, `surface.card`, `text.primary`, `text.secondary`, `border.default`, `action.primary`, `status.success/warning/error/info`, `focus.ring`). Semantic tokens have explicit dark and light values. Dark canvas/primary ink/green anchor derive from portfolio grading. Light theme is warm neutral with green accents, not white-on-green inversion. Accent overuse and low-opacity text must be rejected by contrast review. Create tokens for hover, pressed, disabled, selected, focus, loading and invalid states. Document actual contrast ratios and approvals before code. -Typography: Bangla needs Noto Sans Bengali or a reviewed equivalent with dedicated metrics; Latin UI can evaluate Inter or a distinct face. Use a limited display style for headings, a comfortable text face for learning and a restrained mono treatment for technical labels. Bangla line-height/letter-spacing must be tested separately; never apply wide Latin tracking to Bangla. Digit localization follows language. Use a type scale with body ≥16 CSS px in learning/forms and fluid headings; sizes below are the implementation baseline and must pass device review. +Typography: Bangla uses Hind Siliguri per my 2026-09-29 direction, with dedicated metrics; Latin UI uses Inter for the current shell. Use a limited display style for headings, a comfortable text face for learning and a restrained mono treatment for technical labels. Bangla line-height/letter-spacing must be tested separately; never apply wide Latin tracking to Bangla. Digit localization follows language. Use a type scale with body ≥16 CSS px in learning/forms and fluid headings; sizes below are the implementation baseline and must pass device review. Spacing and layout: 4px base rhythm with semantic spacing steps; constrained prose width; responsive grid for catalog cards; content-first single column on narrow screens. Admin tables may transform to labeled cards or bounded scrollers that preserve row identity/actions. Keep global navigation, search, active course and account context predictable. Sticky UI must not obscure form controls or accessibility focus. @@ -71,7 +71,7 @@ Tokens are semantic and theme switched; components must not embed brand hex valu **Foundation scale:** spacing `0, 4, 8, 12, 16, 24, 32, 48, 64, 96` CSS px; semantic gaps `xs=4`, `sm=8`, `md=16`, `lg=24`, `xl=32`, `section=64` (48 narrow). Grid: 12 columns wide, 8 tablet, 4 mobile; max content width 1200px, prose width 70ch; gutters 16px mobile, 24px tablet, 32px desktop. Breakpoints are content triggers at 640, 768, 1024 and 1440 CSS px, not device labels. Radius `field=8`, `card=12`, `overlay=16`, `pill=9999`; borders 1px normal, 2px focus/selected. Elevation uses border and subtle shadow: level 0 none, level 1 `0 2px 8px rgba(0,0,0,.08)`, level 2 `0 8px 24px rgba(0,0,0,.16)`; dark mode reduces shadow reliance. -**Type scale:** body 16px/1.6 English and 17px/1.7 Bangla, small 14px/1.5, lead 20px/1.5, headings 24/30/40/56px with fluid interpolation for the top two sizes. English UI: self-hosted Inter if license verified; Bangla: self-hosted Noto Sans Bengali if license verified. Both have system sans fallbacks. Bangla receives no forced uppercase or letter spacing; localized digits follow locale except immutable identifiers. Limit long learning prose to 70ch-equivalent and use comfortable media captions. +**Type scale:** body 16px/1.6 English and 17px/1.7 Bangla, small 14px/1.5, lead 20px/1.5, headings 24/30/40/56px with fluid interpolation for the top two sizes. English UI: Inter via `next/font/google`; Bangla: Hind Siliguri via `next/font/google` per my direction. Both have system sans fallbacks. Bangla receives no forced uppercase or letter spacing; localized digits follow locale except immutable identifiers. Limit long learning prose to 70ch-equivalent and use comfortable media captions. **Components:** button variants primary/secondary/quiet/destructive; inputs with persistent label, help and error text; cards with clear title/metadata/action; data tables with sortable labeled headers and mobile row identity; navigation with active and breadcrumb states; dialogs and drawers with focus containment/return; tabs/accordions with keyboard operation; badges never color-only; alerts persistent for consequential errors; toasts only for noncritical confirmations; pagination with current/total status. All have idle, hover, focus-visible, pressed/selected, disabled, busy and relevant invalid/success states. Empty/loading/error/success patterns use the UX contract in UX-CONCEPT-REVIEW. diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index e89dccb..73f3215 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -1,21 +1,21 @@ # OnSkillIT canonical agent handoff -Updated: 2026-09-29 (Asia/Dhaka). **This file is the canonical cross-agent handoff in this repository.** Recheck Git, GitHub CI and dated external facts before acting; source and current CI evidence outrank this snapshot. +Updated: 2026-09-30 (Asia/Dhaka). **This file is the canonical cross-agent handoff in this repository.** Recheck Git, GitHub CI and dated external facts before acting; source and current CI evidence outrank this snapshot. ## State and source of truth - Public repository: `soobujmiah/onskillit-platform`, default branch `main`; local checkout `/home/sbj/onskillit-platform-docs` has the verified `origin` remote. Use a fresh branch from current `main` for new work. - PHASE-00 through PHASE-03 are **COMPLETE**. PHASE-03 IDENTITY VERIFIED passed after [gate-closure Identity CI](https://github.com/soobujmiah/onskillit-platform/actions/runs/36562919856), all nine PR checks at `5f21544`, synthetic browser screenshots and the founding partner's 2026-09-29 instruction to complete PHASE-03 before PHASE-04. [PR #14](https://github.com/soobujmiah/onskillit-platform/pull/14) merged as `0428cfb`; [post-merge Identity CI](https://github.com/soobujmiah/onskillit-platform/actions/runs/36565770895) and the other `main` checks passed. [PHASE-03-WORKLOG](PHASE-03-WORKLOG.md) has the detailed evidence and boundaries. -- PHASE-04 PUBLISHING CORE VERIFIED **PASSED** on synthetic GitHub evidence. [PR #15](https://github.com/soobujmiah/onskillit-platform/pull/15) merged as `aeb3d696223f4f7870458b14913e357afc41386b` after all eleven final PR checks passed on `65cb2d9`; [post-merge CMS run 36571645321](https://github.com/soobujmiah/onskillit-platform/actions/runs/36571645321) and the other eight `main` workflows passed. The six Phase 4 staff routes are CI-verified. PHASE-05 through PHASE-15 are NOT STARTED. Public business pages belong to PHASE-05. See [PHASES](PHASES.md), [PAGES](PAGES.md), [API-CONTRACT](API-CONTRACT.md), [DATA-MODEL](DATA-MODEL.md) and [TRACEABILITY](TRACEABILITY.md). +- PHASE-04 PUBLISHING CORE VERIFIED **PASSED** on synthetic GitHub evidence. [PR #15](https://github.com/soobujmiah/onskillit-platform/pull/15) merged as `aeb3d696223f4f7870458b14913e357afc41386b` after all eleven final PR checks passed on `65cb2d9`; [post-merge CMS run 36571645321](https://github.com/soobujmiah/onskillit-platform/actions/runs/36571645321) and the other eight `main` workflows passed. The six Phase 4 staff routes are CI-verified. PHASE-05 source and the [GitHub Pages editorial preview](https://soobujmiah.github.io/onskillit-platform/) are in [PR #17](https://github.com/soobujmiah/onskillit-platform/pull/17); PUBLIC CORE VERIFIED remains open. PHASE-06 through PHASE-15 are NOT STARTED. Public business pages belong to PHASE-05. See [PHASES](PHASES.md), [PAGES](PAGES.md), [API-CONTRACT](API-CONTRACT.md), [DATA-MODEL](DATA-MODEL.md), [PHASE-05-WORKLOG](PHASE-05-WORKLOG.md) and [TRACEABILITY](TRACEABILITY.md). - The owner declined `main` branch protection during PHASE-01; do not enable it without a new instruction. ## Delivered and remaining boundaries - PHASE-01 established the Next.js runtime, database-aware health endpoint, reviewed SQL migration pipeline, worker, OCI smoke and GitHub CI. PHASE-02 established bilingual routing, theme, design tokens and browser/locale checks. PHASE-03 delivered identity migration/API, bilingual account and staff pages, scoped RBAC, append-only audit, reviewed operator bootstraps, OpenAPI, and synthetic PostgreSQL/mail/browser CI. - No real owner grant, external SMTP delivery, production deployment, production data migration or change to `onskillit.com` occurred. Mobile-only recovery remains deferred and disabled. The protected operator context must authenticate the operator and retain independent approval before any live owner bootstrap; those scripts are never HTTP routes or migration seeds. -- A live public preview/staging host still needs owner selection and environment configuration. GitHub browser CI currently supplies synthetic screenshots, not a shareable site. A full manual accessibility pass remains a later cross-system gate. +- A live public preview/staging host still needs owner selection and environment configuration. GitHub browser CI currently supplies synthetic screenshots, not a shareable site. A full manual accessibility pass remains a later cross-system gate. The confirmed unmarked business brief is mapped to ten bilingual [editorial CMS drafts](PHASE-05-CMS-COPY.json); they are not imported, reviewed or published. Explicit `[CONFIRM]` terms and all legal policies remain open; bilingual [policy proposals](PHASE-05-POLICY-REVIEW.md) are outside the CMS for review. The inquiry table now supports audited, operator-reviewed personal-field redaction for privacy requests; retention and policy approval are still required before enabling the form. ## Exact continuation order -1. Begin PHASE-05 public core on a fresh branch from current `main`, using owner-verified content and the existing CMS contracts. Arrange a shareable preview only after selecting a host and protected environment configuration. -2. Keep real content claims, external mail, preview hosting and production/domain changes at their respective gates. Never place credentials or private records in this public repository. +1. Continue PHASE-05 after PR #17 source merge. Review the live bilingual static preview and the editorial drafts; the owner confirmed exact contact details and the unmarked business/service brief. Resolve the operational questions and approve complete English/Bangla Privacy, Terms and Accessibility text before CMS publication or enabling inquiries. Keep the Phase 5 gate open until the review and evidence are recorded. +2. GitHub Pages serves the static editorial preview only. A compatible server, PostgreSQL, worker and protected configuration are still needed to run CMS, sign-in and inquiry endpoints. Keep external mail, production/domain changes and real content publication at their respective gates. Never place credentials or private records in this public repository. diff --git a/docs/OFFERINGS.md b/docs/OFFERINGS.md index 716dd15..9a2f549 100644 --- a/docs/OFFERINGS.md +++ b/docs/OFFERINGS.md @@ -1,5 +1,7 @@ # Offering evidence and launch catalog gate +The founding partner supplied Phase 5 business and service descriptions on 2026-09-29 and clarified on 2026-09-30 that unmarked content was confirmed; see [PHASE-05-CONTENT-DRAFT](PHASE-05-CONTENT-DRAFT.md). Explicit `[CONFIRM]` items remain open. The table below is the historical Phase 0 proposal inventory, not the owner-supplied Phase 5 service list. Operational evidence and CMS review are still needed before publishing claims about live module availability or payment integration. + Reviewed 2026-09-24. The founding partner explicitly asked the agent to suggest and add a Bangladesh-suitable, world-class catalog. That authorizes a **proposed V1 catalog**, not an assertion that any service/course is already delivered. **VERIFIED actual offerings: none yet.** “OnSkillIT” and founding-partner involvement are owner-confirmed, but no service/course catalog, syllabus, invoice, instructor assignment or delivery evidence has been supplied. Candidate records below are planning hypotheses or observations, never public claims. No candidate may be seeded as published CMS content. Proposed scope is informed by [NSDA competency/accreditation materials](https://nsda.gov.bd/pages/static-pages/69687e401fa8288efab67a26), [NSDA training-institution guidance](https://nsda.gov.bd/pages/static-pages/6922dd02933eb65569e13246) and [BTEB course materials](https://bteb.gov.bd/sites/default/files/files/bteb.portal.gov.bd/notices/591b0f18_db87_44c9_b423_e51dae4c44dd/4608.pdf); these sources establish local training context, not OnSkillIT accreditation or demand forecasts. Source dates refer to this review. [SPECIFICATION.md](SPECIFICATION.md) gives the capability hypothesis; [SITE-AUDIT.md](SITE-AUDIT.md) records current-site evidence. Status meanings: **VERIFIED** = partner-approved actual current/launch offer with supporting evidence; **LIKELY** = plausible from the name/site but unconfirmed; **UNVERIFIED** = proposed for owner review; **TEMPLATE/DEMO** = theme material, not an offer; **OBSOLETE/UNKNOWN** = historical status cannot be determined. `V1?` means candidate scope only, not an implementation commitment. `OWNER DECISION REQUIRED` applies to every non-demo candidate. diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index ec7ae41..5d796bf 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -2,6 +2,8 @@ PHASE-01 implementation note: `foundation.yml` checks lint, types, build and HTTP liveness with read-only repository permission. `database.yml` adds a synthetic `postgres:17-alpine` service container, applies the reviewed migration up/down, and smoke-tests the health endpoint's database check and the worker's DB connectivity. `container.yml` builds the versioned OCI image and, in an isolated runner network with no external host, smoke-tests both the web process and the worker process (`PROCESS_ROLE=worker`) started from that same image — the closest PHASE-01 can get to a "deploy smoke" without an external preview-hosting account/credentials. There is still **no external preview provider, no staging deployment and no production promotion** in this slice; the container smoke test proves the artifact runs, not that it is reachable anywhere. See [PHASE-01-WORKLOG](PHASE-01-WORKLOG.md) and the secret review below. +PHASE-05 adds a GitHub Pages **static editorial preview** workflow on `phase-05/public-core`. It builds from the classified owner copy in GitHub Actions and publishes only public business/contact content. GitHub Pages does not run the Next.js server, CMS, sign-in, worker, inquiry endpoint or database. The workflow needs `pages: write` and `id-token: write` only for its deployment job; it uses no repository secrets, synthetic database or customer data. The preview is `noindex`, and external-domain deployment remains a later release action. See [PHASE-05-WORKLOG](PHASE-05-WORKLOG.md). + Status: **accepted portable operating architecture under ADRs 0006/0009**. No hosting or production action is authorized by this document. PHASE-03 branch configuration: identity routes require `DATABASE_URL` and `PUBLIC_BASE_URL` (the exact trusted public origin for browser mutations). Email verification and verified-email reset additionally require protected `SMTP_HOST`, `SMTP_PORT`, `SMTP_SECURE`, `SMTP_FROM` and, when the relay requires authentication, `SMTP_USER`/`SMTP_PASSWORD`. Without mail configuration, the public request endpoint returns its generic response but issues no usable email link. The GitHub `Identity` workflow uses a synthetic local SMTP sink; it does not prove external delivery. Mobile-only recovery is disabled. No production secret is configured or stored in this public repository. @@ -14,6 +16,12 @@ Backup scope: database, private media, configuration inventory and key reference Observability: structured application logs with request/trace ID and release SHA; error monitoring; queue depth/failures; DB health/slow queries; uptime probes; web vitals; security alerts for admin/login/publication anomalies; immutable audit events. Redact passwords, reset links, tokens, personal documents and payment payloads. Assign an incident owner and escalation route before production. Define retention per data category and legal review. +## Phase 5 inquiry privacy requests + +The public inquiry form remains disabled until the privacy notice is approved and published. A deletion request sent to `onskillitbd@gmail.com` requires an authenticated operator to verify the requester against the private inquiry record, record a request case reference, and obtain a separate review reference. Do not place the name, email, message or other personal data in those references or in GitHub issues, commits, logs or CI artifacts. In a protected operator environment with the target database configured, run `scripts/redact_public_inquiry.ts` through `tsx` with `INQUIRY_ID`, `OPERATOR_IDENTITY`, `PRIVACY_REQUEST_REFERENCE`, `PRIVACY_REVIEW_REFERENCE` and `PRIVACY_REDACTION_APPROVED=yes`. Never expose this script through an HTTP route or run it from public PR CI against real data. + +The procedure clears name, email, phone and message in one transaction. A database trigger permits this transition once, preserves a minimal receipt and writes an append-only audit event with the case references. It denies ordinary updates and physical deletion. The operator must verify the redacted state and record completion in the private case system. Retention of the minimal receipt, backups, application logs, and any third-party copies still needs an approved policy and operational review before the form is enabled. GitHub CI exercises the procedure only with synthetic data. + ## Current-host evidence boundary (2026-09-24) [HOSTING-CAPABILITY.md](HOSTING-CAPABILITY.md) records the Namecheap-registered public IP, LiteSpeed/PHP response and Namecheap product-family documentation. The actual OnSkillIT plan, quotas, supported database, persistent worker, cron, secret management, deploy method and backup restore capability remain unknown. Namecheap's plan-dependent AutoBackup and shared-host process limits must be verified against the account before the production topology is approved. The application must remain portable to a compatible upgraded or separate host under the same domain if the current plan cannot meet transaction, queue and recovery requirements. No production or DNS action has occurred. diff --git a/docs/PAGES.md b/docs/PAGES.md index e5e059e..9871021 100644 --- a/docs/PAGES.md +++ b/docs/PAGES.md @@ -10,15 +10,15 @@ A page is a route-level responsibility with its own loading, access, error and n | Page ID | Page name | Route | Type/audience | Purpose | Parent | Module | Auth | Permission | SEO | Static/dynamic | Template | Priority | Implementation phase | Status | Dependencies | |---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| -| PAGE-HOME | Home | / | Public | Introduce verified offers | root | CMS | none | public | index | static | PAGE-HOME | P1 | PHASE-05 | NOT STARTED | CMS, Service | -| PAGE-ABOUT | About | /about | Public | Explain verified organization | root | CMS | none | public | index | static | PAGE-ABOUT | P1 | PHASE-05 | NOT STARTED | Page | -| PAGE-SERVICES | Services | /services | Public | Browse services | root | Catalog | none | public | index | listing | PAGE-SERVICES | P0 | PHASE-05 | NOT STARTED | Service, Category | -| PAGE-SERVICE-DETAIL | Service detail | /services/{slug} | Public | Explain one service | PAGE-SERVICES | Catalog | none | public | index | dynamic | PAGE-SERVICE-DETAIL | P0 | PHASE-05 | NOT STARTED | Service, Media, SEO | -| PAGE-CONTACT | Contact | /contact | Public | Submit inquiry | root | CRM | none | public | index | static | PAGE-CONTACT | P1 | PHASE-05 | NOT STARTED | Inquiry, rate limit | -| PAGE-FAQ | FAQ | /faq | Public | Answer approved questions | root | CMS | none | public | index | static | PAGE-FAQ | P1 | PHASE-05 | NOT STARTED | FAQ, SEO | -| PAGE-PRIVACY | Privacy | /privacy | Public | Publish approved notice | footer | CMS | none | public | index | static | PAGE-PRIVACY | P1 | PHASE-05 | NOT STARTED | Approved legal text | -| PAGE-TERMS | Terms | /terms | Public | Publish approved terms | footer | CMS | none | public | index | static | PAGE-TERMS | P1 | PHASE-05 | NOT STARTED | Approved legal text | -| PAGE-ACCESSIBILITY | Accessibility | /accessibility | Public | Explain access support | footer | CMS | none | public | index | static | PAGE-ACCESSIBILITY | P1 | PHASE-05 | NOT STARTED | Approved support policy | +| PAGE-HOME | Home | / | Public | Introduce verified offers | root | CMS | none | public | index | static | PAGE-HOME | P1 | PHASE-05 | CI-VERIFIED | CMS, Service | +| PAGE-ABOUT | About | /about | Public | Explain verified organization | root | CMS | none | public | index | static | PAGE-ABOUT | P1 | PHASE-05 | CI-VERIFIED | Page | +| PAGE-SERVICES | Services | /services | Public | Browse services | root | Catalog | none | public | index | listing | PAGE-SERVICES | P0 | PHASE-05 | CI-VERIFIED | Service, Category | +| PAGE-SERVICE-DETAIL | Service detail | /services/{slug} | Public | Explain one service | PAGE-SERVICES | Catalog | none | public | index | dynamic | PAGE-SERVICE-DETAIL | P0 | PHASE-05 | CI-VERIFIED | Service, Media, SEO | +| PAGE-CONTACT | Contact | /contact | Public | Submit inquiry | root | CRM | none | public | index | static | PAGE-CONTACT | P1 | PHASE-05 | CI-VERIFIED | Inquiry, rate limit | +| PAGE-FAQ | FAQ | /faq | Public | Answer approved questions | root | CMS | none | public | index | static | PAGE-FAQ | P1 | PHASE-05 | CI-VERIFIED | FAQ, SEO | +| PAGE-PRIVACY | Privacy | /privacy | Public | Publish approved notice | footer | CMS | none | public | index | static | PAGE-PRIVACY | P1 | PHASE-05 | CI-VERIFIED | Approved legal text | +| PAGE-TERMS | Terms | /terms | Public | Publish approved terms | footer | CMS | none | public | index | static | PAGE-TERMS | P1 | PHASE-05 | CI-VERIFIED | Approved legal text | +| PAGE-ACCESSIBILITY | Accessibility | /accessibility | Public | Explain access support | footer | CMS | none | public | index | static | PAGE-ACCESSIBILITY | P1 | PHASE-05 | CI-VERIFIED | Approved support policy | | PAGE-COURSES | Courses | /courses | Public | Browse real courses | root | LMS | none | public | index | listing | PAGE-COURSES | P0 | PHASE-06 | NOT STARTED | Course, Instructor | | PAGE-COURSE-DETAIL | Course detail | /courses/{slug} | Public | Choose a course | PAGE-COURSES | LMS | none | public | index | dynamic | PAGE-COURSE-DETAIL | P0 | PHASE-06 | NOT STARTED | Course, Edition, Media, SEO | | PAGE-PROGRAMS | Programs | /programs | Public | Browse training programs | root | TMS | none | public | index | listing | PAGE-PROGRAMS | P0 | PHASE-07 | NOT STARTED | Program | diff --git a/docs/PHASE-05-CMS-COPY.json b/docs/PHASE-05-CMS-COPY.json new file mode 100644 index 0000000..d4d75cf --- /dev/null +++ b/docs/PHASE-05-CMS-COPY.json @@ -0,0 +1,462 @@ +{ + "status": "editorial_draft", + "source": "Owner content brief, 2026-09-29; unmarked descriptions confirmed 2026-09-30", + "publication": "Import through the staff CMS as drafts, review Bangla copy, then use the independent review and publish workflow. This file is never a production seed.", + "pages": [ + { + "page_key": "home", + "kind": "landing", + "en": { + "title": "Software and skills for growing Bangladeshi businesses", + "slug": "home", + "description": "OnSkillIT builds cloud software for businesses in Bangladesh and offers practical IT training in Bangla.", + "seoTitle": "OnSkillIT | Software and skills for Bangladesh", + "seoDescription": "Explore business software and practical IT training from OnSkillIT in Bangladesh.", + "sections": [ + { + "type": "text", + "heading": "Who we serve", + "body": "Hospitals and diagnostic centers, clinics, hotels and resorts, restaurants, retailers and F-commerce sellers, teachers and learners.", + "source": "owner-brief-2026-09-29" + }, + { + "type": "cta", + "heading": "Explore our services", + "body": "See the software and training services described by OnSkillIT.", + "href": "/services", + "source": "owner-brief-2026-09-29" + } + ] + }, + "bn": { + "title": "বাংলাদেশের ব্যবসার জন্য সফটওয়্যার ও দক্ষতা", + "slug": "home", + "description": "অনস্কিলআইটি বাংলাদেশের ব্যবসার জন্য ক্লাউড সফটওয়্যার তৈরি করে এবং বাংলায় ব্যবহারিক তথ্যপ্রযুক্তি প্রশিক্ষণ দেয়।", + "seoTitle": "অনস্কিলআইটি | বাংলাদেশের জন্য সফটওয়্যার ও দক্ষতা", + "seoDescription": "অনস্কিলআইটির ব্যবসায়িক সফটওয়্যার ও ব্যবহারিক তথ্যপ্রযুক্তি প্রশিক্ষণ সম্পর্কে জানুন।", + "sections": [ + { + "type": "text", + "heading": "যাঁদের জন্য কাজ করি", + "body": "হাসপাতাল ও ডায়াগনস্টিক সেন্টার, ক্লিনিক, হোটেল ও রিসোর্ট, রেস্তোরাঁ, খুচরা বিক্রেতা ও সামাজিক মাধ্যমে বিক্রেতা, শিক্ষক ও শিক্ষার্থী।", + "source": "owner-brief-2026-09-29" + }, + { + "type": "cta", + "heading": "সেবাগুলো দেখুন", + "body": "অনস্কিলআইটির সফটওয়্যার ও প্রশিক্ষণ সেবা সম্পর্কে জানুন।", + "href": "/services", + "source": "owner-brief-2026-09-29" + } + ] + } + }, + { + "page_key": "about", + "kind": "page", + "en": { + "title": "About OnSkillIT", + "slug": "about", + "description": "OnSkillIT, a brand of CODE GENARATE, builds cloud software and offers practical IT training in Bangla.", + "seoTitle": "About OnSkillIT", + "seoDescription": "Learn about OnSkillIT's software and Bangla IT training for businesses in Bangladesh.", + "sections": [ + { + "type": "text", + "heading": "What we do", + "body": "We build cloud software for hospitals, clinics, hotels and online shops in Bangladesh, and offer practical IT training in Bangla. Our products are designed for mobile use and support Bangla and English.", + "source": "owner-brief-2026-09-29" + }, + { + "type": "text", + "heading": "Working with us", + "body": "Tell us what you need by email or WhatsApp. We discuss your requirements, arrange a demo and provide a quotation before setup. We serve clients across Bangladesh with online support.", + "source": "owner-brief-2026-09-29" + } + ] + }, + "bn": { + "title": "অনস্কিলআইটি সম্পর্কে", + "slug": "about", + "description": "কোড জেনারেটের একটি ব্র্যান্ড অনস্কিলআইটি ক্লাউড সফটওয়্যার তৈরি করে এবং বাংলায় ব্যবহারিক তথ্যপ্রযুক্তি প্রশিক্ষণ দেয়।", + "seoTitle": "অনস্কিলআইটি সম্পর্কে", + "seoDescription": "বাংলাদেশের ব্যবসার জন্য অনস্কিলআইটির সফটওয়্যার ও প্রশিক্ষণ সম্পর্কে জানুন।", + "sections": [ + { + "type": "text", + "heading": "আমাদের কাজ", + "body": "আমরা বাংলাদেশের হাসপাতাল, ক্লিনিক, হোটেল ও অনলাইন দোকানের জন্য ক্লাউড সফটওয়্যার তৈরি করি এবং বাংলায় ব্যবহারিক তথ্যপ্রযুক্তি প্রশিক্ষণ দিই। আমাদের পণ্য মুঠোফোনে ব্যবহারের উপযোগী এবং বাংলা ও ইংরেজি সমর্থন করে।", + "source": "owner-brief-2026-09-29" + }, + { + "type": "text", + "heading": "আমাদের সঙ্গে কাজের ধাপ", + "body": "ইমেইল বা হোয়াটসঅ্যাপে আপনার প্রয়োজন জানান। আলোচনা ও প্রদর্শনের পর আমরা মূল্যপ্রস্তাব দিই এবং তারপর সেটআপ করি। সারা বাংলাদেশে অনলাইন সহায়তা দিই।", + "source": "owner-brief-2026-09-29" + } + ] + } + }, + { + "page_key": "services", + "kind": "page", + "en": { + "title": "Services", + "slug": "services", + "description": "Explore hospital and clinic software, hotel management, online stores, custom development and IT training.", + "seoTitle": "Software and IT training services | OnSkillIT", + "seoDescription": "See OnSkillIT's software, web development and Bangla IT training services.", + "sections": [ + { + "type": "text", + "heading": "Choose a service", + "body": "Explore the service descriptions below, then contact us to discuss your requirements and request a demo or quotation.", + "source": "owner-brief-2026-09-29" + } + ] + }, + "bn": { + "title": "সেবাসমূহ", + "slug": "services", + "description": "হাসপাতাল ও ক্লিনিক সফটওয়্যার, হোটেল ব্যবস্থাপনা, অনলাইন দোকান, নিজস্ব ওয়েব উন্নয়ন ও তথ্যপ্রযুক্তি প্রশিক্ষণ দেখুন।", + "seoTitle": "সফটওয়্যার ও প্রশিক্ষণ সেবা | অনস্কিলআইটি", + "seoDescription": "অনস্কিলআইটির সফটওয়্যার, ওয়েব উন্নয়ন ও বাংলা প্রশিক্ষণ সেবা দেখুন।", + "sections": [ + { + "type": "text", + "heading": "সেবা বেছে নিন", + "body": "নিচের সেবাগুলোর বিবরণ দেখুন। আপনার প্রয়োজন নিয়ে আলোচনা, প্রদর্শন বা মূল্যপ্রস্তাবের জন্য যোগাযোগ করুন।", + "source": "owner-brief-2026-09-29" + } + ] + } + }, + { + "page_key": "hospital-clinic", + "kind": "service", + "category": "software", + "en": { + "title": "Hospital and Clinic Management Software", + "slug": "hospital-clinic", + "description": "Cloud software for hospital and clinic operations, with setup, data import, staff training and support.", + "seoTitle": "Hospital and clinic software | OnSkillIT", + "seoDescription": "Explore OnSkillIT hospital and clinic management software and its service scope.", + "sections": [ + { + "type": "text", + "heading": "Included workflows", + "body": "Appointments, e-prescriptions, pharmacy, laboratory, patient admission and beds, ambulance, staff, accounting, patient portal and SMS reminders.", + "source": "owner-brief-2026-09-29" + }, + { + "type": "text", + "heading": "Delivery", + "body": "Setup, data import, staff training and support are part of the service discussion. Contact us to confirm the fit and request a quotation.", + "source": "owner-brief-2026-09-29" + } + ] + }, + "bn": { + "title": "হাসপাতাল ও ক্লিনিক ব্যবস্থাপনা সফটওয়্যার", + "slug": "hospital-clinic", + "description": "হাসপাতাল ও ক্লিনিক পরিচালনার ক্লাউড সফটওয়্যার; সেটআপ, তথ্য আমদানি, কর্মী প্রশিক্ষণ ও সহায়তাসহ।", + "seoTitle": "হাসপাতাল ও ক্লিনিক সফটওয়্যার | অনস্কিলআইটি", + "seoDescription": "অনস্কিলআইটির হাসপাতাল ও ক্লিনিক ব্যবস্থাপনা সফটওয়্যারের পরিধি জানুন।", + "sections": [ + { + "type": "text", + "heading": "কাজের পরিধি", + "body": "অ্যাপয়েন্টমেন্ট, ইলেকট্রনিক ব্যবস্থাপত্র, ফার্মেসি, পরীক্ষাগার, রোগী ভর্তি ও শয্যা, অ্যাম্বুলেন্স, কর্মী, হিসাবরক্ষণ, রোগী পোর্টাল এবং খুদে বার্তার স্মারক।", + "source": "owner-brief-2026-09-29" + }, + { + "type": "text", + "heading": "সেবা প্রদানের ধাপ", + "body": "সেটআপ, তথ্য আমদানি, কর্মী প্রশিক্ষণ ও সহায়তা নিয়ে আলোচনা করা যায়। উপযোগিতা যাচাই ও মূল্যপ্রস্তাবের জন্য যোগাযোগ করুন।", + "source": "owner-brief-2026-09-29" + } + ] + } + }, + { + "page_key": "hotel-booking", + "kind": "service", + "category": "software", + "en": { + "title": "Hotel Booking and Management Software", + "slug": "hotel-booking", + "description": "Hotel software for room booking, front desk, food orders, invoices and local payments.", + "seoTitle": "Hotel booking software | OnSkillIT", + "seoDescription": "Explore OnSkillIT's hotel booking and management software scope.", + "sections": [ + { + "type": "text", + "heading": "Included workflows", + "body": "Online room booking designed to prevent double booking, front desk operations, restaurant and food orders, and A4 invoices with the amount in Bangla words.", + "source": "owner-brief-2026-09-29" + }, + { + "type": "text", + "heading": "Payments", + "body": "The described payment options are bKash, Nagad and SSLCommerz. Discuss configuration and availability for your hotel during the consultation.", + "source": "owner-brief-2026-09-29" + } + ] + }, + "bn": { + "title": "হোটেল বুকিং ও ব্যবস্থাপনা সফটওয়্যার", + "slug": "hotel-booking", + "description": "কক্ষ বুকিং, অভ্যর্থনা, খাবারের অর্ডার, চালান ও স্থানীয় পেমেন্টের জন্য হোটেল সফটওয়্যার।", + "seoTitle": "হোটেল বুকিং সফটওয়্যার | অনস্কিলআইটি", + "seoDescription": "অনস্কিলআইটির হোটেল বুকিং ও ব্যবস্থাপনা সফটওয়্যারের পরিধি জানুন।", + "sections": [ + { + "type": "text", + "heading": "কাজের পরিধি", + "body": "একই কক্ষের দ্বৈত বুকিং ঠেকানোর ব্যবস্থা, অভ্যর্থনা বিভাগের কাজ, রেস্তোরাঁ ও খাবারের অর্ডার এবং বাংলায় কথায় টাকার পরিমাণসহ কাগজের চালান।", + "source": "owner-brief-2026-09-29" + }, + { + "type": "text", + "heading": "পেমেন্ট", + "body": "উল্লিখিত পেমেন্ট পদ্ধতিগুলো হলো বিকাশ, নগদ ও এসএসএলকমার্স। আপনার হোটেলের জন্য সংযোগ ও প্রাপ্যতা আলোচনার সময় নিশ্চিত করুন।", + "source": "owner-brief-2026-09-29" + } + ] + } + }, + { + "page_key": "ecommerce", + "kind": "service", + "category": "software", + "en": { + "title": "E-commerce Store Platform", + "slug": "ecommerce", + "description": "An online store platform with products, orders, local payment options and a mobile storefront.", + "seoTitle": "E-commerce store platform | OnSkillIT", + "seoDescription": "Explore OnSkillIT's online store platform for Bangladeshi sellers.", + "sections": [ + { + "type": "text", + "heading": "Store features", + "body": "Ready online store, product and order management, Bangla and English storefronts, and a mobile-friendly shopping experience.", + "source": "owner-brief-2026-09-29" + }, + { + "type": "text", + "heading": "Payment options", + "body": "The described options are bKash, Nagad, SSLCommerz and cash on delivery. Contact us to discuss setup for your store.", + "source": "owner-brief-2026-09-29" + } + ] + }, + "bn": { + "title": "অনলাইন দোকান প্ল্যাটফর্ম", + "slug": "ecommerce", + "description": "পণ্য, অর্ডার, স্থানীয় পেমেন্ট পদ্ধতি ও মুঠোফোনের উপযোগী দোকানসহ অনলাইন বিক্রয় প্ল্যাটফর্ম।", + "seoTitle": "অনলাইন দোকান প্ল্যাটফর্ম | অনস্কিলআইটি", + "seoDescription": "বাংলাদেশের বিক্রেতাদের জন্য অনস্কিলআইটির অনলাইন দোকান প্ল্যাটফর্ম জানুন।", + "sections": [ + { + "type": "text", + "heading": "দোকানের সুবিধা", + "body": "প্রস্তুত অনলাইন দোকান, পণ্য ও অর্ডার ব্যবস্থাপনা, বাংলা ও ইংরেজি দোকান এবং মুঠোফোনে কেনাকাটার উপযোগী নকশা।", + "source": "owner-brief-2026-09-29" + }, + { + "type": "text", + "heading": "পেমেন্ট পদ্ধতি", + "body": "উল্লিখিত পদ্ধতিগুলো হলো বিকাশ, নগদ, এসএসএলকমার্স এবং পণ্য হাতে পেয়ে মূল্য পরিশোধ। আপনার দোকানের সেটআপ নিয়ে আলোচনা করতে যোগাযোগ করুন।", + "source": "owner-brief-2026-09-29" + } + ] + } + }, + { + "page_key": "custom-development", + "kind": "service", + "category": "development", + "en": { + "title": "Custom Website and Web App Development", + "slug": "custom-development", + "description": "Business websites and custom web applications tailored to your requirements.", + "seoTitle": "Website and web app development | OnSkillIT", + "seoDescription": "Discuss business websites, WordPress sites and Laravel web apps with OnSkillIT.", + "sections": [ + { + "type": "text", + "heading": "What we build", + "body": "Business websites, WordPress websites and Laravel web applications. We review your requirements and provide a quotation for custom work.", + "source": "owner-brief-2026-09-29" + } + ] + }, + "bn": { + "title": "নিজস্ব ওয়েবসাইট ও ওয়েব অ্যাপ উন্নয়ন", + "slug": "custom-development", + "description": "আপনার প্রয়োজন অনুযায়ী ব্যবসায়িক ওয়েবসাইট ও নিজস্ব ওয়েব অ্যাপ তৈরি।", + "seoTitle": "ওয়েবসাইট ও ওয়েব অ্যাপ উন্নয়ন | অনস্কিলআইটি", + "seoDescription": "অনস্কিলআইটির সঙ্গে ব্যবসায়িক ওয়েবসাইট এবং নিজস্ব ওয়েব অ্যাপ নিয়ে আলোচনা করুন।", + "sections": [ + { + "type": "text", + "heading": "যা তৈরি করি", + "body": "ব্যবসায়িক ওয়েবসাইট, ওয়ার্ডপ্রেস ওয়েবসাইট এবং লারাভেল ওয়েব অ্যাপ। আপনার প্রয়োজন পর্যালোচনার পর নিজস্ব কাজের মূল্যপ্রস্তাব দিই।", + "source": "owner-brief-2026-09-29" + } + ] + } + }, + { + "page_key": "it-training", + "kind": "service", + "category": "training", + "en": { + "title": "IT Training in Bangla", + "slug": "it-training", + "description": "Practical Bangla courses for teachers, website builders and aspiring app developers.", + "seoTitle": "Bangla IT training | OnSkillIT", + "seoDescription": "Explore OnSkillIT's practical IT training in Bangla.", + "sections": [ + { + "type": "text", + "heading": "Courses", + "body": "Digital Teacher's Training and WordPress Website Development are described in the owner brief. Android App Development is in progress. Contact us for course details and current availability.", + "source": "owner-brief-2026-09-29" + } + ] + }, + "bn": { + "title": "বাংলায় তথ্যপ্রযুক্তি প্রশিক্ষণ", + "slug": "it-training", + "description": "শিক্ষক, ওয়েবসাইট নির্মাতা ও নতুন অ্যাপ নির্মাতাদের জন্য বাংলায় ব্যবহারিক কোর্স।", + "seoTitle": "বাংলায় তথ্যপ্রযুক্তি প্রশিক্ষণ | অনস্কিলআইটি", + "seoDescription": "অনস্কিলআইটির ব্যবহারিক বাংলা তথ্যপ্রযুক্তি প্রশিক্ষণ সম্পর্কে জানুন।", + "sections": [ + { + "type": "text", + "heading": "কোর্সসমূহ", + "body": "ডিজিটাল শিক্ষক প্রশিক্ষণ ও ওয়ার্ডপ্রেস ওয়েবসাইট উন্নয়নের কথা মালিকের বিবরণে রয়েছে। অ্যান্ড্রয়েড অ্যাপ উন্নয়ন কোর্সটি তৈরির কাজ চলছে। বর্তমান প্রাপ্যতা জানতে যোগাযোগ করুন।", + "source": "owner-brief-2026-09-29" + } + ] + } + }, + { + "page_key": "contact", + "kind": "page", + "en": { + "title": "Contact OnSkillIT", + "slug": "contact", + "description": "Contact OnSkillIT by email, phone or WhatsApp to discuss software and training services.", + "seoTitle": "Contact OnSkillIT", + "seoDescription": "Find OnSkillIT's confirmed contact details and discuss your requirements.", + "sections": [ + { + "type": "text", + "heading": "Start a conversation", + "body": "Tell us what you need by email or WhatsApp. We can discuss your requirements, arrange a demo and provide a quotation before setup. We offer online support across Bangladesh.", + "source": "owner-brief-2026-09-29" + } + ] + }, + "bn": { + "title": "অনস্কিলআইটির সঙ্গে যোগাযোগ", + "slug": "contact", + "description": "সফটওয়্যার ও প্রশিক্ষণ নিয়ে আলোচনা করতে ইমেইল, ফোন বা হোয়াটসঅ্যাপে যোগাযোগ করুন।", + "seoTitle": "অনস্কিলআইটির সঙ্গে যোগাযোগ", + "seoDescription": "অনস্কিলআইটির নিশ্চিত যোগাযোগের তথ্য দেখুন এবং আপনার প্রয়োজন জানান।", + "sections": [ + { + "type": "text", + "heading": "আলোচনা শুরু করুন", + "body": "ইমেইল বা হোয়াটসঅ্যাপে আপনার প্রয়োজন জানান। আমরা আলোচনা, প্রদর্শন ও সেটআপের আগে মূল্যপ্রস্তাব দিতে পারি। সারা বাংলাদেশে অনলাইন সহায়তা দিই।", + "source": "owner-brief-2026-09-29" + } + ] + } + }, + { + "page_key": "faq", + "kind": "page", + "en": { + "title": "Frequently Asked Questions", + "slug": "faq", + "description": "Answers to common questions about OnSkillIT software, language, demos, hosting and training.", + "seoTitle": "Frequently asked questions | OnSkillIT", + "seoDescription": "Read answers about OnSkillIT software and training services.", + "sections": [ + { + "type": "text", + "heading": "Which payment options do you support?", + "body": "The described software options include bKash, Nagad and SSLCommerz. The e-commerce platform also describes cash on delivery. Confirm the configuration for your service with us.", + "source": "owner-brief-2026-09-29" + }, + { + "type": "text", + "heading": "Is the software available in Bangla?", + "body": "Our product descriptions include Bangla and English support.", + "source": "owner-brief-2026-09-29" + }, + { + "type": "text", + "heading": "Do I need my own server?", + "body": "It depends on your requirements. We can discuss hosting for you or installation on your own server or hosting.", + "source": "owner-brief-2026-09-29" + }, + { + "type": "text", + "heading": "Can I request a demo?", + "body": "Yes. Contact us on WhatsApp to request a free demo.", + "source": "owner-brief-2026-09-29" + }, + { + "type": "text", + "heading": "Is staff training included?", + "body": "Basic staff training is included with setup.", + "source": "owner-brief-2026-09-29" + } + ] + }, + "bn": { + "title": "সাধারণ জিজ্ঞাসা", + "slug": "faq", + "description": "অনস্কিলআইটির সফটওয়্যার, ভাষা, প্রদর্শন, হোস্টিং ও প্রশিক্ষণ নিয়ে সাধারণ প্রশ্নের উত্তর।", + "seoTitle": "সাধারণ জিজ্ঞাসা | অনস্কিলআইটি", + "seoDescription": "অনস্কিলআইটির সফটওয়্যার ও প্রশিক্ষণ সেবা সম্পর্কে উত্তর পড়ুন।", + "sections": [ + { + "type": "text", + "heading": "কোন পেমেন্ট পদ্ধতি সমর্থন করেন?", + "body": "বর্ণিত সফটওয়্যারে বিকাশ, নগদ ও এসএসএলকমার্সের কথা রয়েছে। অনলাইন দোকানে পণ্য হাতে পেয়ে মূল্য পরিশোধের কথাও রয়েছে। আপনার সেবার জন্য সংযোগের বিষয়টি আমাদের সঙ্গে নিশ্চিত করুন।", + "source": "owner-brief-2026-09-29" + }, + { + "type": "text", + "heading": "সফটওয়্যার কি বাংলায় পাওয়া যায়?", + "body": "আমাদের পণ্যের বিবরণে বাংলা ও ইংরেজি সমর্থনের কথা রয়েছে।", + "source": "owner-brief-2026-09-29" + }, + { + "type": "text", + "heading": "নিজস্ব সার্ভার কি দরকার?", + "body": "এটি আপনার প্রয়োজনের ওপর নির্ভর করে। আপনার জন্য হোস্টিং বা নিজস্ব সার্ভারে স্থাপনের বিষয়ে আলোচনা করতে পারি।", + "source": "owner-brief-2026-09-29" + }, + { + "type": "text", + "heading": "প্রদর্শন দেখতে পারি?", + "body": "হ্যাঁ। বিনামূল্যে প্রদর্শনের জন্য হোয়াটসঅ্যাপে যোগাযোগ করুন।", + "source": "owner-brief-2026-09-29" + }, + { + "type": "text", + "heading": "কর্মী প্রশিক্ষণ কি অন্তর্ভুক্ত?", + "body": "সেটআপের সঙ্গে প্রাথমিক কর্মী প্রশিক্ষণ অন্তর্ভুক্ত।", + "source": "owner-brief-2026-09-29" + } + ] + } + } + ] +} diff --git a/docs/PHASE-05-CONTENT-DRAFT.md b/docs/PHASE-05-CONTENT-DRAFT.md new file mode 100644 index 0000000..3f01565 --- /dev/null +++ b/docs/PHASE-05-CONTENT-DRAFT.md @@ -0,0 +1,29 @@ +# Phase 5 content intake — owner clarification and open approvals + +Source: my 2026-09-29 content brief and subsequent 2026-09-30 clarification in the current work session. The clarification confirms the unmarked business descriptions for editorial use. These remain owner statements rather than independent evidence that a product or payment integration is operational. Items explicitly marked `[CONFIRM]` and all policy text remain open. No CMS publication or production seed follows from this document. + +## Confirmed public contact details + +I explicitly confirmed these values for exact public display: + +| Field | Confirmed value | +|---|---| +| Email | `onskillitbd@gmail.com` | +| Phone / WhatsApp | `+8801617301184`; `https://wa.me/8801617301184` | +| Address | `40/1 Kalicharan Shaha Road, Gandaria, Dhaka-1204, Bangladesh` | +| Facebook | `https://www.facebook.com/onskillit` | +| YouTube | `https://www.youtube.com/@onskillit` | + +Office hours were marked `[CONFIRM]` and must remain unpublished until approved. + +## Owner-confirmed business and service descriptions + +The owner confirmed the unmarked brief content: the tagline “Software and skills for growing Bangladeshi businesses” / “বাংলাদেশের ব্যবসার জন্য সফটওয়্যার ও দক্ষতা”, the About description including the stated CODE GENARATE brand relationship, the audience sectors, five service groups and their listed scope, the unmarked course descriptions, the consultation flow, the Bangladesh-wide online-support statement, and the unmarked FAQ answers. These can become bilingual CMS drafts with the brief as their owner source. Named projects are owner-supplied portfolio leads for PHASE-11, where their live status and publication rights need review. The brief does not establish accreditation, production readiness of every module, or independent payment-provider verification. + +Prices, office hours, a one-working-day response promise, subscription cancellation, course refunds, hotel SaaS status, and ecommerce SaaS status were explicitly tagged `[CONFIRM]`; omit them from any publishable copy until I resolve each one. I prohibited testimonials, client logos, user counts and statistics. I requested **Hind Siliguri** for Bangla UI text. + +I said there is **no approved Privacy, Terms, or Accessibility policy**. [Bilingual policy proposals](PHASE-05-POLICY-REVIEW.md) are now prepared outside the CMS for legal/operational review; they are not approved or publishable. The inquiry form collects name, email, optional phone and message only after a published privacy notice is available; it retains the notice revision as consent evidence. No payment or card-data policy claim is published from this draft. + +## Editorial next step + +Ten bilingual [CMS editorial drafts](PHASE-05-CMS-COPY.json) now map the owner-confirmed unmarked descriptions to home, about, services, five service details, contact and FAQ. Review the Bangla translations and the claims in a preview, import the chosen copy as drafts, then use the Phase 4 independent review/publish workflow. Resolve each `[CONFIRM]` item and approve complete policy text before including it in published content. Contact details above may appear on the noindex contact route. The inquiry form remains disabled until a privacy notice is published. diff --git a/docs/PHASE-05-POLICY-REVIEW.md b/docs/PHASE-05-POLICY-REVIEW.md new file mode 100644 index 0000000..d28b0c4 --- /dev/null +++ b/docs/PHASE-05-POLICY-REVIEW.md @@ -0,0 +1,56 @@ +# Phase 5 policy text for owner review — NOT APPROVED OR PUBLISHED + +Source: owner content brief, 2026-09-29; current Phase 5 inquiry and cookie implementation; official sources linked below. These are proposed words for review, not live policies or a claim of legal compliance. Do not import them into the CMS or enable the inquiry form until the owner approves complete bilingual text and the operational questions below are resolved. + +## Privacy notice — proposed English + +**Who to contact.** For privacy questions or a request to access, correct or delete inquiry information, email `onskillitbd@gmail.com`. + +**Inquiry information.** If you use our contact form, we collect your name, email address, optional phone number and message. We use them to reply to your inquiry and discuss a requested consultation, demo or quotation. We do not sell inquiry information. Please do not send patient records, payment card details or other sensitive information in the message. + +**Website preferences and accounts.** The site uses a cookie to remember the selected light or dark theme. If you use an account, session and request-protection cookies are used to keep the account signed in and protect actions. The public inquiry form is available only after this notice has been approved and published. + +**Requests and retention.** Email us to request correction or deletion. After checking that the request concerns your inquiry, an authorized operator can remove its name, email, phone and message; a minimal case receipt and audit record remain. **Owner decisions required before publication:** how long inquiries, receipts, logs and backups are kept; when backup copies are removed; who handles requests; and what hosting or service providers receive data. + +**Other services.** WhatsApp, Facebook and YouTube links take you to services with their own privacy practices. Any later payment flow needs its own provider and card-data wording after the actual integration is reviewed. + +## গোপনীয়তা বিজ্ঞপ্তি — প্রস্তাবিত বাংলা + +**যোগাযোগ।** গোপনীয়তা সম্পর্কে প্রশ্ন বা অনুসন্ধানের তথ্য দেখা, সংশোধন কিংবা মুছে ফেলার অনুরোধ জানাতে `onskillitbd@gmail.com` ঠিকানায় ইমেইল করুন। + +**অনুসন্ধানের তথ্য।** যোগাযোগ ফর্ম ব্যবহার করলে আমরা আপনার নাম, ইমেইল ঠিকানা, ঐচ্ছিক ফোন নম্বর ও বার্তা সংগ্রহ করি। আপনার অনুসন্ধানের উত্তর দিতে এবং চাওয়া পরামর্শ, প্রদর্শন বা মূল্যপ্রস্তাব নিয়ে আলোচনা করতে এই তথ্য ব্যবহার করি। অনুসন্ধানের তথ্য আমরা বিক্রি করি না। বার্তায় রোগীর নথি, পেমেন্ট কার্ডের তথ্য বা অন্য সংবেদনশীল তথ্য পাঠাবেন না। + +**ওয়েবসাইটের পছন্দ ও অ্যাকাউন্ট।** উজ্জ্বল বা গাঢ় রূপের পছন্দ মনে রাখতে ওয়েবসাইট একটি কুকি ব্যবহার করে। অ্যাকাউন্ট ব্যবহার করলে সাইন ইন অবস্থা বজায় রাখতে এবং কাজের নিরাপত্তায় সেশন ও অনুরোধ সুরক্ষার কুকি ব্যবহার করা হয়। এই বিজ্ঞপ্তি অনুমোদিত ও প্রকাশিত না হওয়া পর্যন্ত অনুসন্ধান ফর্ম চালু হবে না। + +**অনুরোধ ও তথ্য রাখার সময়।** তথ্য সংশোধন বা মুছে ফেলার অনুরোধ ইমেইলে জানান। অনুরোধটি আপনার অনুসন্ধানের সঙ্গে সম্পর্কিত কি না যাচাই করার পর অনুমোদিত কর্মী নাম, ইমেইল, ফোন ও বার্তা মুছে দিতে পারেন; ন্যূনতম কার্যক্রমের নথি ও নিরীক্ষার তথ্য থাকে। **প্রকাশের আগে মালিকের সিদ্ধান্ত প্রয়োজন:** অনুসন্ধান, নথি, লগ ও ব্যাকআপ কত দিন রাখা হবে; ব্যাকআপ থেকে কখন তথ্য সরবে; অনুরোধ কে সামলাবেন; এবং কোন হোস্টিং বা সেবাদাতা তথ্য পাবেন। + +**অন্যান্য সেবা।** হোয়াটসঅ্যাপ, ফেসবুক ও ইউটিউবের সংযোগে গেলে সেসব সেবার নিজস্ব গোপনীয়তা পদ্ধতি প্রযোজ্য হতে পারে। ভবিষ্যতের পেমেন্ট ব্যবস্থার জন্য প্রকৃত সংযোগ যাচাইয়ের পর আলাদা বিবরণ প্রয়োজন। + +## Website terms — proposed English + +This website describes OnSkillIT services and provides ways to contact us. A service description or demo request does not create a subscription or fix a price. The scope, availability, price, setup, support and any cancellation or refund terms for a particular service must be stated in a separate quotation or agreement. Please use the contact form for legitimate inquiries and do not submit sensitive patient, card or account information. Contact `onskillitbd@gmail.com` if you have a question about using the site. + +**Owner decisions required before publication:** registered contract issuer; applicable account terms; content ownership and permitted reuse; how quotes become binding; dispute/contact process; and approved cancellation/refund terms. This draft intentionally makes no claim about the marked `[CONFIRM]` terms. + +## ওয়েবসাইট ব্যবহারের শর্ত — প্রস্তাবিত বাংলা + +এই ওয়েবসাইটে অনস্কিলআইটির সেবার বিবরণ ও যোগাযোগের উপায় রয়েছে। সেবার বিবরণ দেখা বা প্রদর্শন চাওয়ার মাধ্যমে সদস্যতা শুরু হয় না কিংবা মূল্য নির্ধারিত হয় না। নির্দিষ্ট সেবার কাজের পরিধি, প্রাপ্যতা, মূল্য, সেটআপ, সহায়তা এবং বাতিল বা ফেরতের শর্ত আলাদা মূল্যপ্রস্তাব বা চুক্তিতে উল্লেখ করতে হবে। যোগাযোগ ফর্ম কেবল প্রকৃত অনুসন্ধানের জন্য ব্যবহার করুন; রোগীর সংবেদনশীল তথ্য, কার্ড বা অ্যাকাউন্টের তথ্য পাঠাবেন না। ওয়েবসাইট ব্যবহার নিয়ে প্রশ্ন থাকলে `onskillitbd@gmail.com` ঠিকানায় যোগাযোগ করুন। + +**প্রকাশের আগে মালিকের সিদ্ধান্ত প্রয়োজন:** চুক্তির নিবন্ধিত পক্ষ, অ্যাকাউন্ট ব্যবহারের শর্ত, বিষয়বস্তুর মালিকানা ও পুনর্ব্যবহার, মূল্যপ্রস্তাব কখন বাধ্যতামূলক হবে, বিরোধ নিষ্পত্তির যোগাযোগ এবং অনুমোদিত বাতিল বা ফেরতের শর্ত। + +## Accessibility statement — proposed English + +We aim to make the OnSkillIT website usable by people with disabilities and use [WCAG 2.1 Level AA](https://www.w3.org/TR/WCAG21/) as our design goal. The website has not yet received a full manual accessibility audit, so this is a goal, not a conformity claim. If you find a barrier, email `onskillitbd@gmail.com` with the page address, the problem and a way to contact you. We will review the report and work on a practical remedy. + +**Owner decisions required before publication:** named accessibility owner, response process, supported assistive-technology/browser scope, audit results and any known limitations. + +## প্রবেশগম্যতা বিবৃতি — প্রস্তাবিত বাংলা + +প্রতিবন্ধী ব্যক্তিসহ সবার জন্য অনস্কিলআইটির ওয়েবসাইট ব্যবহারযোগ্য করার লক্ষ্য আমাদের। নকশার লক্ষ্য হিসেবে আমরা [প্রবেশগম্যতার আন্তর্জাতিক নির্দেশিকার দ্বিতীয় সংস্করণের দ্বৈত স্তর](https://www.w3.org/TR/WCAG21/) অনুসরণ করি। পূর্ণাঙ্গ হাতে-কলমে প্রবেশগম্যতা যাচাই এখনো হয়নি; তাই এটি লক্ষ্য, মান পূরণের দাবি নয়। কোনো বাধা পেলে পাতার ঠিকানা, সমস্যার বিবরণ ও আপনার সঙ্গে যোগাযোগের উপায়সহ `onskillitbd@gmail.com` ঠিকানায় জানান। আমরা প্রতিবেদন পর্যালোচনা করে বাস্তবসম্মত সমাধানের চেষ্টা করব। + +**প্রকাশের আগে মালিকের সিদ্ধান্ত প্রয়োজন:** প্রবেশগম্যতার দায়িত্বপ্রাপ্ত ব্যক্তি, উত্তর দেওয়ার পদ্ধতি, সমর্থিত সহায়ক প্রযুক্তি ও ব্রাউজার, নিরীক্ষার ফল এবং পরিচিত সীমাবদ্ধতা। + +## Review sources and limits + +- [Bangladesh Personal Data Protection Act, 2026 — collection and processing](https://bdlaws.minlaw.gov.bd/act-1692/act-chapter-print-2694.html), [data-subject rights](https://bdlaws.minlaw.gov.bd/act-1692/act-chapter-print-2695.html), [security duties](https://bdlaws.minlaw.gov.bd/act-print-1692/section-print-57283.html) and [retention](https://bdlaws.minlaw.gov.bd/act-print-1692/section-print-57284.html) are current official texts reviewed on 2026-09-30. Their application to the final OnSkillIT operation requires qualified review; the draft makes no compliance assertion. +- [W3C WCAG 2.1](https://www.w3.org/TR/WCAG21/) defines the stated accessibility goal. Automated CI checks and a goal statement alone do not establish conformity. diff --git a/docs/PHASE-05-WORKLOG.md b/docs/PHASE-05-WORKLOG.md new file mode 100644 index 0000000..d764360 --- /dev/null +++ b/docs/PHASE-05-WORKLOG.md @@ -0,0 +1,31 @@ +# PHASE-05 work log — public business site + +Current phase: **IN PROGRESS**. Gate: **PUBLIC CORE VERIFIED OPEN**. Source work starts from `main` at `74a91a3` on branch `phase-05/public-core`. No Phase 5 page, service, inquiry, or legal copy is published on the production domain by creating this branch. + +| Field | Value | +|---|---| +| Current phase / gate | PHASE-05 / PUBLIC CORE VERIFIED (open) | +| Task | Build the bilingual public core, published-only service catalog, inquiry intake, and SEO baseline | +| Modules | Public frontend, CMS delivery, Service, Inquiry, SEO | +| Page IDs | PAGE-HOME, PAGE-ABOUT, PAGE-SERVICES, PAGE-SERVICE-DETAIL, PAGE-CONTACT, PAGE-FAQ, PAGE-PRIVACY, PAGE-TERMS, PAGE-ACCESSIBILITY | +| Requirements | R05, R09, R13, R14, R16, R19, R20; R17 for CI | +| Dependencies | PHASE-02 EXPERIENCE FOUNDATION VERIFIED and PHASE-04 PUBLISHING CORE VERIFIED passed | +| Acceptance | Published-only bilingual rendering, no invented offerings or legal claims, two-theme responsive pages, semantic navigation, abuse-controlled inquiry, published-only metadata and sitemap, GitHub CI evidence | + +## Publication inputs + +I supplied exact **confirmed contact details** and a broader content brief on 2026-09-29, then clarified on 2026-09-30 that the unmarked business and service information was already confirmed. Its classification and remaining review needs are recorded in [PHASE-05-CONTENT-DRAFT](PHASE-05-CONTENT-DRAFT.md). Explicit `[CONFIRM]` items and legal policies remain unapproved. Public routes show a clear unpublished state and stay `noindex` until their CMS records are reviewed and published. The inquiry form is disabled until a privacy notice is published. Production indexing and domain cutover remain separate owner gates. + +## Source and verification + +The branch includes SQL pair `0004_public_core`, a published-only CMS reader, nine public route templates, a rate-limited inquiry endpoint, conditional sitemap/robots, bilingual content states and Hind Siliguri. The owner-confirmed unmarked brief is now mapped to ten bilingual [CMS editorial drafts](PHASE-05-CMS-COPY.json): home, about, services, five service details, contact and FAQ. These are import-ready content shapes, not production CMS records or published pages. GitHub CI checks their structure, language scripts and exclusion of marked commercial terms, then loads them into a disposable test database to produce browser screenshots for review. Policy proposals are never loaded by this fixture. I found that the original append-only inquiry record could not meet the owner's deletion-on-request brief, so the migration now permits one reviewed personal-field redaction. The operator-only procedure requires a request reference and separate review reference, and the database trigger writes an audit event. The exact paths and requirement links are in [TRACEABILITY](TRACEABILITY.md). I used the approved Phase 2 token system for the UI; the Superdesign login session opened but expired without authorization, so no canvas draft or visual approval is claimed. + +The first [Public Core run 36605215028](https://github.com/soobujmiah/onskillit-platform/actions/runs/36605215028) on `cd91a70` failed because the synthetic test counted an unavailable-privacy attempt outside the limiter, while the endpoint counted it. Browser checks in the earlier shell, identity and CMS workflows also found duplicate unlabeled navigation landmarks and old home/tab-order expectations. I corrected the test and navigation. On `7ef718d`, all eleven draft PR checks passed, including [Public Core 36606061038](https://github.com/soobujmiah/onskillit-platform/actions/runs/36606061038), [Identity 36606061114](https://github.com/soobujmiah/onskillit-platform/actions/runs/36606061114), [CMS 36606061052](https://github.com/soobujmiah/onskillit-platform/actions/runs/36606061052), and [locale/browser 36606061223](https://github.com/soobujmiah/onskillit-platform/actions/runs/36606061223). These are synthetic GitHub checks; a subsequent documentation/security refinement still needs final-head CI. + +Source and documentation commit `2075065` passed all [PR checks](https://github.com/soobujmiah/onskillit-platform/pull/17/checks), including [Public Core 36628307560](https://github.com/soobujmiah/onskillit-platform/actions/runs/36628307560), identity, CMS, locale/browser, web build, migrations, documentation, repository checks and deploy smoke. GitHub browser artifacts show the owner-copy Home, About, Services, Service Detail, Contact and FAQ at 320 and 1440 pixels in both languages. I inspected representative desktop/mobile screenshots, found a repeated Home tagline, removed it and inspected the corrected final Home artifact. Automated browser checks found no horizontal overflow or axe violations. Nine Phase 5 page templates are **CI-VERIFIED for synthetic source behavior**, not released. No local build or test has been run. After `main` advanced independently, I merged it into this branch and aligned its machine-readable phase register with the accepted [canonical phase register](PHASES.md). + +The phase gate remains **OPEN** because legal policies are unapproved and owner visual/content review is outstanding. A GitHub Pages review host was selected on 2026-09-30; see below. Explicit `[CONFIRM]` claims remain excluded; they do not block publication if omitted. [Bilingual policy proposals](PHASE-05-POLICY-REVIEW.md) are outside the CMS and require owner and qualified operational/legal review before publication. No production deployment, data migration or domain change occurred. + +## GitHub Pages review preview + +On 2026-09-30 I clarified that GitHub should host a preview now, while deployment on the external OnSkillIT domain waits until the full project is complete. The existing Next.js application uses server rendering, CMS/database reads, authentication and POST APIs; it cannot be fully hosted by GitHub Pages. I therefore added `scripts/build_pages_preview.py` and a branch-scoped GitHub Actions Pages workflow that render the ten owner-confirmed editorial records as twenty static English/Bangla pages, with navigation, dark/light controls and confirmed contact links. The review site carries `noindex` and `robots.txt` disallow, an explicit preview banner, and no inquiry form, policy text, prices or `[CONFIRM]` claims. It is a content and visual preview, not a deployment of the server app or a production release. The first workflow run failed before checkout because I used invalid action revisions; the next run was initially blocked by the Pages environment's `main`-only branch policy. I corrected the pinned revisions, authorized only `phase-05/public-core` in that environment and reran the workflow. [Deployment run 36634726227](https://github.com/soobujmiah/onskillit-platform/actions/runs/36634726227) passed at `9057b95`. The [GitHub Pages preview](https://soobujmiah.github.io/onskillit-platform/) returned HTTP 200 for the root, both locale homepages, a Bangla service detail and the stylesheet; the English home response contains the confirmed tagline and `noindex` meta. No local build or test was run. diff --git a/docs/PHASES.md b/docs/PHASES.md index cce14db..1e1cc82 100644 --- a/docs/PHASES.md +++ b/docs/PHASES.md @@ -1,6 +1,6 @@ # Canonical phase architecture -Status: **canonical baseline accepted by founding-partner instruction, 2026-09-24**. This is the sole phase register. PHASE-00 through PHASE-04 are **COMPLETE**, with their respective gates passed after GitHub evidence and founding-partner instructions. PHASE-03's **IDENTITY VERIFIED** decision followed the 2026-09-29 instruction to complete PHASE-03 and then PHASE-04, the passing [gate-closure CI](https://github.com/soobujmiah/onskillit-platform/actions/runs/36562919856), security procedure review and synthetic page inspection; see [PHASE-03-WORKLOG](PHASE-03-WORKLOG.md). PHASE-04 is **COMPLETE; PUBLISHING CORE VERIFIED passed** after [final CMS CI](https://github.com/soobujmiah/onskillit-platform/actions/runs/36570696266) on `53954c5` and the other passing PR checks. See [PHASE-04-WORKLOG](PHASE-04-WORKLOG.md). PHASE-05 through PHASE-15 are **NOT STARTED**. Phase numbers name milestones, not permission to skip dependencies. V1 runs through PHASE-14; PHASE-15 is controlled post-launch evolution. Page IDs and their single owning phase are authoritative in [PAGES.md](PAGES.md). +Status: **canonical baseline accepted by founding-partner instruction, 2026-09-24**. This is the sole phase register. PHASE-00 through PHASE-04 are **COMPLETE**, with their respective gates passed after GitHub evidence and founding-partner instructions. PHASE-03's **IDENTITY VERIFIED** decision followed the 2026-09-29 instruction to complete PHASE-03 and then PHASE-04, the passing [gate-closure CI](https://github.com/soobujmiah/onskillit-platform/actions/runs/36562919856), security procedure review and synthetic page inspection; see [PHASE-03-WORKLOG](PHASE-03-WORKLOG.md). PHASE-04 is **COMPLETE; PUBLISHING CORE VERIFIED passed** after [final CMS CI](https://github.com/soobujmiah/onskillit-platform/actions/runs/36570696266) on `53954c5` and the other passing PR checks. See [PHASE-04-WORKLOG](PHASE-04-WORKLOG.md). PHASE-05 is **IN PROGRESS** with PUBLIC CORE VERIFIED open; PHASE-06 through PHASE-15 are **NOT STARTED**. Phase numbers name milestones, not permission to skip dependencies. V1 runs through PHASE-14; PHASE-15 is controlled post-launch evolution. Page IDs and their single owning phase are authoritative in [PAGES.md](PAGES.md). ## Master register @@ -11,7 +11,7 @@ Status: **canonical baseline accepted by founding-partner instruction, 2026-09-2 | PHASE-02 | Design and localization shell | Establish shared accessible bilingual and themed UI | Tokens, shells, language and theme infrastructure | 01 | EXPERIENCE FOUNDATION VERIFIED | COMPLETE | | PHASE-03 | Identity, RBAC and audit | Secure all private operations | Accounts, sessions, scoped roles, audit | 01,02 | IDENTITY VERIFIED | COMPLETE | | PHASE-04 | CMS and publishing core | Enable controlled content operations | Typed pages, revisions, media, navigation, SEO fields | 03 | PUBLISHING CORE VERIFIED | COMPLETE | -| PHASE-05 | Public business site | Publish verified business and service content | Public core, services, inquiries, legal and SEO baseline | 02,04 | PUBLIC CORE VERIFIED | NOT STARTED | +| PHASE-05 | Public business site | Publish verified business and service content | Public core, services, inquiries, legal and SEO baseline | 02,04 | PUBLIC CORE VERIFIED | IN PROGRESS | | PHASE-06 | LMS and course catalog | Deliver real learning workflows | Courses, editions, lessons, assessments, progress | 03,04 | LEARNING VERIFIED | NOT STARTED | | PHASE-07 | TMS and training catalog | Manage scheduled delivery | Programs, batches, sessions, attendance, results | 06 | TRAINING VERIFIED | NOT STARTED | | PHASE-08 | Enrollment and finance | Connect learning/training to payment | Enrollment, capacity, invoices, provider adapters, refunds | 06,07,03 | COMMERCE VERIFIED | NOT STARTED | diff --git a/docs/READINESS.md b/docs/READINESS.md index 95d1c8e..cc27b26 100644 --- a/docs/READINESS.md +++ b/docs/READINESS.md @@ -1,6 +1,6 @@ # Phase 0 final readiness decision — 2026-09-25 (historical gate record; current phase: PHASE-04 COMPLETE) -> **Current lifecycle update (2026-09-29):** `PHASE-00` through `PHASE-04` are **COMPLETE** (`DOCUMENTATION READY`, `FOUNDATION VERIFIED`, `SHELL VERIFIED`, `IDENTITY VERIFIED`, and `PUBLISHING CORE VERIFIED` passed on GitHub Actions CI; see [PHASES.md](PHASES.md), [PHASE-01-WORKLOG.md](PHASE-01-WORKLOG.md), [PHASE-02-WORKLOG.md](PHASE-02-WORKLOG.md), [PHASE-03-WORKLOG.md](PHASE-03-WORKLOG.md), and [PHASE-04-WORKLOG.md](PHASE-04-WORKLOG.md)). `PHASE-05` (`PUBLIC CORE VERIFIED`) is the next implementation phase. The sections below preserve the dated `2026-09-25` Phase 0 gate closure record unchanged. +> **Current lifecycle update (2026-09-30):** `PHASE-00` through `PHASE-04` are **COMPLETE** (`DOCUMENTATION READY`, `FOUNDATION VERIFIED`, `EXPERIENCE FOUNDATION VERIFIED`, `IDENTITY VERIFIED`, and `PUBLISHING CORE VERIFIED` passed on GitHub Actions CI; see [PHASES.md](PHASES.md), [PHASE-01-WORKLOG.md](PHASE-01-WORKLOG.md), [PHASE-02-WORKLOG.md](PHASE-02-WORKLOG.md), [PHASE-03-WORKLOG.md](PHASE-03-WORKLOG.md), and [PHASE-04-WORKLOG.md](PHASE-04-WORKLOG.md)). `PHASE-05` is **IN PROGRESS** with `PUBLIC CORE VERIFIED` open; see [PHASE-05-WORKLOG.md](PHASE-05-WORKLOG.md). The sections below preserve the dated `2026-09-25` Phase 0 gate closure record unchanged. **Historical Phase 0 snapshot (2026-09-25):** PHASE-00 **COMPLETE**. **Gate:** DOCUMENTATION READY **PASSED** under the founding partner's explicit final Phase 0 architectural delegation. At that 2026-09-25 milestone, PHASE-01 through PHASE-15 were NOT STARTED; phases PHASE-01 through PHASE-04 subsequently completed through 2026-09-29. This gate accepted an implementation-ready architecture, not unverified business facts or production readiness. The 16-phase/74-V1-template registers and ADR 0005 are unchanged. Dispositions below are exactly `PASS`, `PASS WITH OWNER DECISION`, `EXTERNAL DEPENDENCY`, `UNKNOWN NON-BLOCKING`, or `BLOCKED`. diff --git a/docs/README.md b/docs/README.md index d928f82..51272c7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # Documentation map -PHASE-00 through PHASE-03 are complete. PHASE-04 is in progress with its gate open; see `PHASE-04-WORKLOG.md`, `CMS-PUBLISHING.md` and `openapi/cms.json`. The documentation is a living specification with explicit evidence states. Branch implementation does not imply production verification. +PHASE-00 through PHASE-04 are complete. PHASE-05 is in progress with PUBLIC CORE VERIFIED open; see `PHASE-05-WORKLOG.md`, `PHASE-05-CONTENT-DRAFT.md` and `openapi/public.json`. The documentation is a living specification with explicit evidence states. Branch implementation does not imply production verification. | Question | Document | |---|---| diff --git a/docs/SEO-CONTENT.md b/docs/SEO-CONTENT.md index 6121cc6..8bbcaaa 100644 --- a/docs/SEO-CONTENT.md +++ b/docs/SEO-CONTENT.md @@ -1,5 +1,9 @@ # SEO, content and current-site transition plan +The Phase 5 GitHub Pages editorial preview is excluded from search with page-level `noindex` and a disallowing `robots.txt`. It is separate from the CMS-backed public sitemap and does not establish production canonical URLs or legal-policy publication. + +PHASE-05 source contract (CI review in progress): `src/lib/public-seo.ts` emits locale metadata from the current published CMS revision. `src/app/sitemap.ts` lists only complete published page/service routes, and `src/app/robots.ts` disallows crawling until an HTTPS `PUBLIC_SITE_URL`, `PUBLIC_INDEXING_ENABLED=true` and approved CMS `robots_enabled=true` are all present. The unpublished route state is always `noindex`; confirmed contact details can be displayed there without making unapproved service or legal claims indexable. Reciprocal alternate links are emitted only for paired published translations. The 2026-09-29 owner content intake is classified in [PHASE-05-CONTENT-DRAFT](PHASE-05-CONTENT-DRAFT.md); business/legal copy remains unapproved. + Status: **accepted architecture under ADR 0009; content and redirect release evidence remain pending**. Search visibility and current index state must be checked with Search Console and a complete URL export before launch. ## Content model and quality gates diff --git a/docs/SITEMAP.md b/docs/SITEMAP.md index ebcd32e..be36fe1 100644 --- a/docs/SITEMAP.md +++ b/docs/SITEMAP.md @@ -1,5 +1,7 @@ # Public sitemap and navigation projection +PHASE-05 source (CI review in progress): the existing nine Phase 5 templates are routed under `/en/` and `/bn/`. CMS navigation renders only published destinations; the XML sitemap contains only published complete static and service records when production indexing is explicitly enabled. Unpublished pages return a noindex information state, while unknown service slugs return 404. No proposed or demo offering is seeded. See [PHASE-05-WORKLOG](PHASE-05-WORKLOG.md). + [PAGES.md](PAGES.md) owns page IDs, template counts, phase assignments and route patterns. This file is the public URL/navigation projection only. Every published page has complete approved English and Bangla content; locale URL format is an open ADR. Public routes are proposals until real content/legal text is approved. No draft, private, auth or staff URL enters XML sitemap. Dynamic records create URLs only when published and indexable. | Section | Page IDs / route templates | Navigation | Phase | diff --git a/docs/SPECIFICATION.md b/docs/SPECIFICATION.md index 15f50af..20367f4 100644 --- a/docs/SPECIFICATION.md +++ b/docs/SPECIFICATION.md @@ -1,5 +1,7 @@ # OnSkillIT Digital Platform — Product & Technical Specification +PHASE-05 is in source implementation on draft PR #17. The founding partner's 2026-09-29 content brief confirms exact contact details but labels the wider business/service text as draft for owner review and provides no approved Privacy, Terms or Accessibility policy. See [PHASE-05-CONTENT-DRAFT](PHASE-05-CONTENT-DRAFT.md). Public routes therefore render only published CMS content, plus the confirmed contact details on a noindex contact state; inquiry intake remains disabled until a privacy notice is published. Source and synthetic GitHub CI do not attest that draft product/payment/service claims are operational. + Status: **Phase 0 product architecture accepted, 2026-09-25; factual catalog/legal inputs remain external**. This document records the founding partner's requested product direction. Specific business offerings remain unverified. Read the [audit](SITE-AUDIT.md), [architecture](ARCHITECTURE.md), [requirements matrix](TRACEABILITY.md), and [readiness report](READINESS.md) together. ## Executive summary, business understanding and capability map diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index d86bfc7..1050e14 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -1,5 +1,7 @@ # Requirements traceability matrix +PHASE-05 source mapping: PAGE-HOME → `src/app/[locale]/page.tsx`; PAGE-ABOUT, PAGE-CONTACT, PAGE-FAQ, PAGE-PRIVACY, PAGE-TERMS, PAGE-ACCESSIBILITY → `src/app/[locale]/[slug]/page.tsx`; PAGE-SERVICES → `src/app/[locale]/services/page.tsx`; PAGE-SERVICE-DETAIL → `src/app/[locale]/services/[slug]/page.tsx`. R05 maps to `catalog_service` and published CMS reads; R09 maps to `public_inquiry`, `POST /api/v1/public/inquiries` and [public OpenAPI](openapi/public.json); R13 maps to metadata, sitemap and robots; R16/R19/R20 map to the public CSS, paired dictionaries and Hind Siliguri. `scripts/public_ci.mjs`, `playwright/public.spec.ts` and `.github/workflows/public-core.yml` are the GitHub source evidence path. `scripts/build_pages_preview.py` and `.github/workflows/pages-preview.yml` render the owner-confirmed editorial copy as a static, noindex GitHub Pages review site; [deployment run 36634726227](https://github.com/soobujmiah/onskillit-platform/actions/runs/36634726227) and the [live preview](https://soobujmiah.github.io/onskillit-platform/) verify R05/R13/R16/R19/R20 only for this static review surface. The server application, inquiry and policies remain unreleased. The PHASE-05 gate remains open for legal and owner review inputs. + PHASE-01 through PHASE-03 are COMPLETE (see their work logs and [HANDOFF](HANDOFF.md)). Their shell infrastructure contributes to R16/R19/R20, but these cross-cutting rows cannot reach `CI-verified` until every owning V1 page is implemented. PHASE-03 source merged in PR #12 and gate closure is in PR #14; IDENTITY VERIFIED passed on synthetic GitHub evidence and the founding partner’s completion instruction. The founding partner's 2026-09-29 decision in [PHASE-03-PLAN](PHASE-03-PLAN.md) defers and disables mobile-only recovery, permits unverified-email registration/sign-in but gates email reset on mailbox verification, and requires reviewed/audited first-owner grant. These are acceptance boundaries for R01, R02, R10 and R15; passing GitHub CI and the gate decision are recorded in PHASE-03-WORKLOG. PHASE-03 mapping: R01 → `src/lib/identity.ts`, `src/app/api/v1/identity/[...path]/route.ts`, `src/app/[locale]/account/`, `src/app/[locale]/learn/profile/`; R02 → `identity_assignment`/`identity_grant_request`, staff routes, `scripts/bootstrap_owner.ts`, `scripts/bootstrap_second_owner.ts`; R10 → `identity_audit` and its immutable trigger; R15 → session/CSRF/rate-limit controls. Contract: `docs/openapi/identity.json`. GitHub checks: `.github/workflows/identity.yml`, `scripts/identity_ci.mjs`, `scripts/bootstrap_race_ci.mjs`, `playwright/identity.spec.ts`. Final branch commit `4f0b3e5` passed [Identity CI](https://github.com/soobujmiah/onskillit-platform/actions/runs/36556831214) and all PR checks, including the concurrent first-owner check, then merged as `90d4fa0`. Post-merge [Identity CI](https://github.com/soobujmiah/onskillit-platform/actions/runs/36560439248) passed on the merge commit. The second-owner operator procedure passed all nine PR checks on `5f21544`, including Identity run 36562919856. Phase 3 page state is CI-VERIFIED; cross-cutting R16/R19/R20 remain incomplete across V1. diff --git a/docs/openapi/public.json b/docs/openapi/public.json new file mode 100644 index 0000000..c584214 --- /dev/null +++ b/docs/openapi/public.json @@ -0,0 +1,47 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OnSkillIT public core API", + "version": "0.1.0", + "description": "PHASE-05 anonymous inquiry intake. Public pages and service catalog render from published CMS records on the server." + }, + "servers": [{ "url": "/api/v1/public" }], + "paths": { + "/inquiries": { + "post": { + "summary": "Submit an inquiry after consenting to the current published privacy notice", + "operationId": "submitInquiry", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["name", "email", "message", "locale", "consent", "consentVersion", "sourcePath"], + "properties": { + "name": { "type": "string", "minLength": 2, "maxLength": 120 }, + "email": { "type": "string", "format": "email", "maxLength": 254 }, + "phone": { "type": "string", "maxLength": 25 }, + "message": { "type": "string", "minLength": 10, "maxLength": 4000 }, + "locale": { "type": "string", "enum": ["en", "bn"] }, + "consent": { "type": "boolean", "const": true }, + "consentVersion": { "type": "string", "format": "uuid" }, + "sourcePath": { "type": "string", "enum": ["/en/contact", "/bn/contact"] }, + "website": { "type": "string", "description": "Honeypot; must be empty" } + } + } + } + } + }, + "responses": { + "202": { "description": "Received without disclosing internal record identifiers" }, + "400": { "description": "Invalid request or consent" }, + "403": { "description": "Origin mismatch" }, + "429": { "description": "Rate limited" }, + "503": { "description": "Intake or current privacy notice unavailable" } + } + } + } + } +} diff --git a/playwright/public.spec.ts b/playwright/public.spec.ts new file mode 100644 index 0000000..7414dcf --- /dev/null +++ b/playwright/public.spec.ts @@ -0,0 +1,28 @@ +import { test, expect } from "@playwright/test"; +import AxeBuilder from "@axe-core/playwright"; + +for (const locale of ["en", "bn"] as const) { + for (const width of [320, 1440]) { + test(`${locale} public core at ${width}px`, async ({ page }, testInfo) => { + await page.setViewportSize({ width, height: 900 }); + if (locale === "bn") await page.context().addCookies([{ name: "theme", value: "dark", url: "http://localhost:3000" }]); + for (const path of ["", "about", "services", "services/hospital-clinic", "contact", "faq", "privacy", "terms", "accessibility"]) { + await page.goto(path ? `/${locale}/${path}` : `/${locale}`); + await expect(page.locator("html")).toHaveAttribute("lang", locale); + await expect(page.getByRole("heading", { level: 1 })).toBeVisible(); + expect(await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)).toBeLessThanOrEqual(0); + const results = await new AxeBuilder({ page }).analyze(); + expect(results.violations).toEqual([]); + await page.screenshot({ path: testInfo.outputPath(`${path ? path.replaceAll("/", "-") : "home"}.png`), fullPage: true }); + } + await page.goto(`/${locale}/contact`); + await expect(page.getByRole("link", { name: "onskillitbd@gmail.com" })).toHaveAttribute("href", "mailto:onskillitbd@gmail.com"); + await expect(page.getByRole("link", { name: "+8801617301184" })).toHaveAttribute("href", "tel:+8801617301184"); + await expect(page.getByRole("button", { name: locale === "bn" ? "অনুরোধ পাঠান" : "Send inquiry" })).toBeVisible(); + await page.goto(`/${locale}`); + await expect(page.getByRole("heading", { level: 1 })).toHaveText(locale === "bn" + ? "বাংলাদেশের ব্যবসার জন্য সফটওয়্যার ও দক্ষতা" + : "Software and skills for growing Bangladeshi businesses"); + }); + } +} diff --git a/playwright/shell.spec.ts b/playwright/shell.spec.ts index 0447aec..815e16c 100644 --- a/playwright/shell.spec.ts +++ b/playwright/shell.spec.ts @@ -6,13 +6,13 @@ test.describe("locale routing", () => { await page.goto("/"); await expect(page).toHaveURL(/\/en$/); await expect(page.locator("html")).toHaveAttribute("lang", "en"); - await expect(page.getByRole("heading", { level: 1 })).toHaveText("OnSkillIT platform foundation"); + await expect(page.getByRole("heading", { level: 1 })).toHaveText("Home"); }); test("/bn renders Bangla content with lang=bn", async ({ page }) => { await page.goto("/bn"); await expect(page.locator("html")).toHaveAttribute("lang", "bn"); - await expect(page.getByRole("heading", { level: 1 })).toHaveText("অনস্কিলআইটি প্ল্যাটফর্ম ভিত্তি"); + await expect(page.getByRole("heading", { level: 1 })).toHaveText("প্রচ্ছদ"); }); test("language switcher moves between locales", async ({ page }) => { @@ -47,11 +47,13 @@ test.describe("theme — no first-paint flash", () => { }); test.describe("keyboard and focus", () => { - test("tab order reaches skip link, account, language switcher, then theme toggle", async ({ page }) => { + test("tab order reaches skip link, brand, account, language switcher, then theme toggle", async ({ page }) => { await page.goto("/en"); await page.keyboard.press("Tab"); await expect(page.locator(".skip-link")).toBeFocused(); await page.keyboard.press("Tab"); + await expect(page.getByRole("link", { name: "OnSkillIT" })).toBeFocused(); + await page.keyboard.press("Tab"); await expect(page.getByRole("link", { name: "Account" })).toBeFocused(); await page.keyboard.press("Tab"); await expect(page.getByRole("link", { name: "Switch to Bangla" })).toBeFocused(); diff --git a/scripts/build_pages_preview.py b/scripts/build_pages_preview.py new file mode 100644 index 0000000..8314242 --- /dev/null +++ b/scripts/build_pages_preview.py @@ -0,0 +1,129 @@ +"""Build the noindex GitHub Pages review site from the Phase 5 editorial copy. + +This intentionally does not export the server application or publish policy drafts. +Run in GitHub Actions only, in line with the project's build boundary. +""" + +from __future__ import annotations + +import html +import json +import os +from pathlib import Path +from shutil import copyfile + + +ROOT = Path(__file__).resolve().parents[1] +OUT = ROOT / "pages-preview-output" +COPY = json.loads((ROOT / "docs/PHASE-05-CMS-COPY.json").read_text(encoding="utf-8")) +PAGES = {page["page_key"]: page for page in COPY["pages"]} +BASE = "/" + os.environ.get("GITHUB_REPOSITORY", "soobujmiah/onskillit-platform").split("/")[-1] + "/" +SERVICES = ("hospital-clinic", "hotel-booking", "ecommerce", "custom-development", "it-training") + + +def esc(value: str) -> str: + return html.escape(value, quote=True) + + +def path_for(key: str, locale: str) -> str: + if key == "home": + return f"{BASE}{locale}/" + if key in SERVICES: + return f"{BASE}{locale}/services/{key}/" + return f"{BASE}{locale}/{key}/" + + +def label(locale: str, en: str, bn: str) -> str: + return bn if locale == "bn" else en + + +def render_page(key: str, locale: str) -> str: + page = PAGES[key][locale] + title = esc(page["title"]) + other = "bn" if locale == "en" else "en" + nav = "".join( + f'{esc(label(locale, en, bn))}' + for item, en, bn in ( + ("home", "Home", "হোম"), + ("about", "About", "পরিচিতি"), + ("services", "Services", "সেবাসমূহ"), + ("faq", "FAQ", "প্রশ্নোত্তর"), + ("contact", "Contact", "যোগাযোগ"), + ) + ) + sections = [] + for section in page["sections"]: + action = "" + if section["type"] == "cta" and section.get("href") == "/services": + action = f'{esc(section["heading"])}' + sections.append( + f'
' + f'

{esc(section["heading"])}

{esc(section["body"])}

{action}
' + ) + if key == "services": + cards = "".join( + f'
  • {esc(PAGES[service][locale]["title"])}

    ' + f'

    {esc(PAGES[service][locale]["description"])}

    ' + f'{esc(label(locale, "View service", "সেবা দেখুন"))}
  • ' + for service in SERVICES + ) + sections.append(f'') + if key == "contact": + sections.append( + '
    ' + '

    Email

    onskillitbd@gmail.com
    ' + '

    WhatsApp

    +8801617301184
    ' + '

    Address

    40/1 Kalicharan Shaha Road, Gandaria, Dhaka-1204, Bangladesh
    ' + '

    Social

    FacebookYouTube
    ' + '
    ' + ) + content = "".join(sections) + home = esc(path_for("home", locale)) + switch = esc(path_for(key, other)) + description = esc(page["seoDescription"]) + preview = esc(label(locale, "Phase 5 preview · inquiry form unavailable", "পঞ্চম ধাপের প্রিভিউ · অনুসন্ধান ফর্ম এখন বন্ধ")) + theme = esc(label(locale, "Toggle theme", "রং পরিবর্তন করুন")) + language = esc(label(locale, "বাংলা", "English")) + return f''' + + +{title} | OnSkillIT preview + + + +
    {preview}
    +
    OnSkillIT + +
    +

    OnSkillIT

    {title}

    +

    {esc(page["description"])}

    {content}
    + +''' + + +def main() -> None: + if COPY["status"] != "editorial_draft" or set(PAGES) != {"home", "about", "services", "contact", "faq", *SERVICES}: + raise SystemExit("Unexpected Phase 5 editorial copy shape") + OUT.mkdir(exist_ok=True) + assets = OUT / "assets" + assets.mkdir(exist_ok=True) + copyfile(ROOT / "src/styles/tokens.css", assets / "tokens.css") + copyfile(ROOT / "scripts/pages-preview.css", assets / "preview.css") + copyfile(ROOT / "scripts/pages-preview.js", assets / "preview.js") + (OUT / ".nojekyll").touch() + (OUT / "robots.txt").write_text("User-agent: *\nDisallow: /\n", encoding="utf-8") + (OUT / "index.html").write_text( + f'OnSkillIT previewEnglish · বাংলা', + encoding="utf-8", + ) + for locale in ("en", "bn"): + for key in PAGES: + relative = path_for(key, locale).removeprefix(BASE) + target = OUT / relative / "index.html" + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(render_page(key, locale), encoding="utf-8") + print(f"Generated {2 * len(PAGES)} localized preview pages in {OUT}") + + +if __name__ == "__main__": + main() diff --git a/scripts/check_docs_architecture.py b/scripts/check_docs_architecture.py index f544e8c..2f54c6b 100644 --- a/scripts/check_docs_architecture.py +++ b/scripts/check_docs_architecture.py @@ -106,6 +106,22 @@ declared_ids = [item.get("name") for item in declared_items] if declared_ids != phase_ids: errors.append(f".repo/phases.yaml sequence mismatch: {declared_ids}") + expected_phases = { + fields[0]: (fields[1], fields[5], { + "COMPLETE": "completed", + "IN PROGRESS": "in_progress", + "NOT STARTED": "not_started", + }.get(fields[6])) + for fields in ( + [part.strip() for part in line.strip("|").split("|")] + for line in phase_lines + ) + } + for item in declared_items: + phase_id = item.get("name") + expected = expected_phases.get(phase_id) + if expected and (item.get("title"), item.get("gate"), item.get("status")) != expected: + errors.append(f"{phase_id}: .repo/phases.yaml title, gate or state differs from docs/PHASES.md") doc_completed = [ line.split("|")[1].strip() for line in phase_lines diff --git a/scripts/check_openapi.mjs b/scripts/check_openapi.mjs index 583cd51..b6dbb83 100644 --- a/scripts/check_openapi.mjs +++ b/scripts/check_openapi.mjs @@ -2,4 +2,5 @@ import SwaggerParser from "@apidevtools/swagger-parser"; await SwaggerParser.validate("docs/openapi/identity.json"); await SwaggerParser.validate("docs/openapi/cms.json"); -console.log("Identity and CMS OpenAPI contracts validated"); +await SwaggerParser.validate("docs/openapi/public.json"); +console.log("Identity, CMS and public OpenAPI contracts validated"); diff --git a/scripts/check_public_copy.mjs b/scripts/check_public_copy.mjs new file mode 100644 index 0000000..a3b9a76 --- /dev/null +++ b/scripts/check_public_copy.mjs @@ -0,0 +1,43 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; + +// GitHub Actions validates the editorial import pack before a staff editor uses it. +const pack = JSON.parse(readFileSync(new URL("../docs/PHASE-05-CMS-COPY.json", import.meta.url), "utf8")); +assert.equal(pack.status, "editorial_draft"); +assert.equal(pack.pages.length, 10); +const expected = new Set(["home", "about", "services", "contact", "faq", "hospital-clinic", "hotel-booking", "ecommerce", "custom-development", "it-training"]); +const seen = new Set(); +for (const page of pack.pages) { + assert(expected.has(page.page_key), `unexpected page ${page.page_key}`); + assert(!seen.has(page.page_key), `duplicate page ${page.page_key}`); + seen.add(page.page_key); + assert.equal(page.kind, page.page_key === "home" ? "landing" : page.category ? "service" : "page"); + for (const locale of ["en", "bn"]) { + const value = page[locale]; + assert(value && typeof value === "object", `${page.page_key}/${locale} missing`); + for (const field of ["title", "description", "seoTitle", "seoDescription"]) { + assert.equal(typeof value[field], "string", `${page.page_key}/${locale}/${field}`); + assert(value[field].trim(), `${page.page_key}/${locale}/${field} empty`); + assert(value[field].length <= (field.endsWith("Description") || field === "description" ? 500 : 160)); + assert(locale === "bn" ? !/[A-Za-z]/.test(value[field]) && /[\u0980-\u09ff]/u.test(value[field]) + : !/[\u0980-\u09ff]/u.test(value[field]) && /[A-Za-z]/.test(value[field]), `${page.page_key}/${locale}/${field} script`); + } + assert(/^[a-z0-9][a-z0-9-]{0,79}$/.test(value.slug), `${page.page_key}/${locale}/slug`); + assert(Array.isArray(value.sections) && value.sections.length > 0 && value.sections.length <= 20); + for (const section of value.sections) { + assert(["hero", "text", "cta"].includes(section.type)); + assert.equal(section.source, "owner-brief-2026-09-29"); + for (const field of ["heading", "body"]) { + assert.equal(typeof section[field], "string"); + assert(section[field].trim() && section[field].length <= (field === "heading" ? 160 : 4000)); + assert(locale === "bn" ? !/[A-Za-z]/.test(section[field]) && /[\u0980-\u09ff]/u.test(section[field]) + : !/[\u0980-\u09ff]/u.test(section[field]) && /[A-Za-z]/.test(section[field]), `${page.page_key}/${locale}/${field} script`); + } + assert(section.type === "cta" ? /^\/[a-z0-9/-]+$/.test(section.href) : section.href === undefined); + } + } +} +assert.deepEqual(seen, expected); +const serialized = JSON.stringify(pack.pages); +assert(!/\[CONFIRM\]|৳|30 days|7 days|working day|office hours/i.test(serialized), "unapproved terms in editorial pack"); +console.log("Phase 5 editorial draft passed structure, locale and explicit-approval checks"); diff --git a/scripts/pages-preview.css b/scripts/pages-preview.css new file mode 100644 index 0000000..0fe3e11 --- /dev/null +++ b/scripts/pages-preview.css @@ -0,0 +1,14 @@ +@import url('https://fonts.googleapis.com/css2?family=Hind+Siliguri:wght@400;600;700&display=swap'); +html[lang="bn"] body { font-family: 'Hind Siliguri', sans-serif; } +html[lang="en"] body { font-family: Arial, sans-serif; line-height: 1.6; } +.preview-banner { padding: .5rem 1rem; text-align: center; background: var(--action-primary-bg); color: var(--action-primary-text); font-size: .875rem; } +.preview-header { width: min(100%, 82rem); margin: auto; padding: 1rem 1.5rem; display: flex; flex-wrap: wrap; align-items: center; gap: 1rem 2rem; justify-content: space-between; } +.site-nav { gap: .35rem 1rem; } +.site-nav a[aria-current="page"] { font-weight: 800; text-decoration-thickness: 2px; } +.preview-controls { display: flex; align-items: center; gap: 1rem; } +.preview-controls a { color: var(--text-primary); } +.preview-controls button { min-width: 44px; min-height: 44px; border: 1px solid var(--border-default); border-radius: var(--radius-field); background: var(--surface-raised); color: var(--text-primary); font: inherit; cursor: pointer; } +main { padding: var(--space-xl) var(--space-lg) var(--space-section); } +.public-card-grid { margin-bottom: 0; } +.public-contact-details { margin-bottom: 0; } +@media (max-width: 640px) { .preview-header { align-items: flex-start; } .site-nav { order: 3; width: 100%; } main { padding: var(--space-lg) var(--space-md); } } diff --git a/scripts/pages-preview.js b/scripts/pages-preview.js new file mode 100644 index 0000000..2f81046 --- /dev/null +++ b/scripts/pages-preview.js @@ -0,0 +1,11 @@ +(() => { + try { + const stored = localStorage.getItem('onskillit-preview-theme'); + if (stored === 'dark' || stored === 'light') document.documentElement.dataset.theme = stored; + } catch { /* storage may be unavailable */ } + document.getElementById('theme-toggle')?.addEventListener('click', () => { + const next = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'; + document.documentElement.dataset.theme = next; + try { localStorage.setItem('onskillit-preview-theme', next); } catch { /* no persistence */ } + }); +})(); diff --git a/scripts/public_ci.mjs b/scripts/public_ci.mjs new file mode 100644 index 0000000..56b07bd --- /dev/null +++ b/scripts/public_ci.mjs @@ -0,0 +1,105 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import postgres from "postgres"; + +const db = postgres(process.env.DATABASE_URL, { max: 1 }); +const base = "http://localhost:3000"; +const fixture = (slug, locale) => locale === "en" ? { + title: "Synthetic public title", slug, description: "Synthetic approved page description", + seoTitle: "Synthetic search title", seoDescription: "Synthetic search description", + sections: [{ type: "text", heading: "Sample section", body: "Synthetic CI content only", source: "ci-fixture" }], +} : { + title: "কৃত্রিম পাতার শিরোনাম", slug, description: "শুধু পরীক্ষার জন্য কৃত্রিম বিবরণ", + seoTitle: "কৃত্রিম অনুসন্ধান শিরোনাম", seoDescription: "কৃত্রিম অনুসন্ধান বিবরণ", + sections: [{ type: "text", heading: "নমুনা অংশ", body: "শুধু পরীক্ষার বিষয়বস্তু", source: "ci-fixture" }], +}; +async function page(key, kind, author, published = false, category = null) { + const [record] = await db`INSERT INTO cms_page(page_key,kind,created_by) VALUES (${key},${kind},${author}) RETURNING id`; + const [revision] = await db`INSERT INTO cms_revision(page_id,revision_no,en,bn,created_by) + VALUES (${record.id},1,${db.json(fixture(key === "service-sample" ? "sample-service" : key, "en"))}, + ${db.json(fixture(key === "service-sample" ? "sample-service-bn" : key, "bn"))},${author}) RETURNING id`; + if (category) await db`INSERT INTO catalog_service(page_id,category) VALUES (${record.id},${category})`; + if (published) await db`UPDATE cms_page SET state='published',published_revision_id=${revision.id} WHERE id=${record.id}`; + return { id: record.id, revisionId: revision.id }; +} +const get = (path) => fetch(`${base}${path}`, { cache: "no-store" }); +const inquiry = (body) => fetch(`${base}/api/v1/public/inquiries`, { + method: "POST", headers: { "content-type": "application/json", Origin: base }, body: JSON.stringify(body), +}); + +try { + const [author] = await db`INSERT INTO identity_user DEFAULT VALUES RETURNING id`; + const unpublished = await get("/en"); + assert.equal(unpublished.status, 200); + assert.match(await unpublished.text(), /Approved information is being prepared/); + assert.equal((await get("/en/services/sample-service")).status, 404); + assert.match(await (await get("/robots.txt")).text(), /Disallow: \/$/m); + + await page("home", "landing", author.id, false); + await page("about", "page", author.id, true); + await page("contact", "page", author.id, true); + await page("services", "page", author.id, true); + const service = await page("service-sample", "service", author.id, true, "synthetic-category"); + const invalid = { name: "Synthetic Sender", email: "sender@example.test", message: "Synthetic inquiry message", locale: "en", + consent: true, consentVersion: "00000000-0000-4000-8000-000000000000", sourcePath: "/en/contact", website: "" }; + assert.match(await (await get("/en/contact")).text(), /Inquiry intake is not available/); + assert.equal((await inquiry(invalid)).status, 503); + const privacy = await page("privacy", "page", author.id, true); + const valid = { ...invalid, consentVersion: privacy.revisionId }; + assert.equal((await fetch(`${base}/api/v1/public/inquiries`, { method: "POST", headers: { "content-type": "application/json", Origin: "https://other.example.test" }, body: JSON.stringify(valid) })).status, 403); + assert.equal((await inquiry({ ...valid, consent: false })).status, 400); + assert.equal((await inquiry({ ...valid, website: "bot.example" })).status, 400); + assert.equal((await inquiry(valid)).status, 202); + assert.equal((await db`SELECT count(*)::int AS count FROM public_inquiry`)[0].count, 1); + assert.equal((await get("/en/services/sample-service")).status, 200); + assert.equal((await get("/bn/services/sample-service-bn")).status, 200); + assert.equal((await get("/bn/services/sample-service")).status, 404); + assert.doesNotMatch(await (await get("/sitemap.xml")).text(), /sample-service/); + await db`INSERT INTO cms_setting(key,value,updated_by) VALUES ('robots_enabled','true',${author.id})`; + const sitemap = await (await get("/sitemap.xml")).text(); + assert.match(sitemap, /sample-service/); + assert.doesNotMatch(sitemap, /\/en\/?<\/loc>/); + assert.match(await (await get("/en/about")).text(), /Synthetic public title/); + assert.equal((await db`SELECT category FROM catalog_service WHERE page_id=${service.id}`)[0].category, "synthetic-category"); + for (let i = 0; i < 3; i++) assert.equal((await inquiry(valid)).status, 202); + assert.equal((await inquiry(valid)).status, 429); + const [intake] = await db`SELECT id FROM public_inquiry ORDER BY created_at LIMIT 1`; + await assert.rejects(db`UPDATE public_inquiry SET message='Tampered inquiry' WHERE id=${intake.id}`); + const redactionEnv = { ...process.env, INQUIRY_ID: intake.id, OPERATOR_IDENTITY: "ci-operator", + PRIVACY_REQUEST_REFERENCE: "synthetic-request-1", PRIVACY_REVIEW_REFERENCE: "synthetic-review-1" }; + assert.throws(() => execFileSync("./node_modules/.bin/tsx", ["scripts/redact_public_inquiry.ts"], { env: redactionEnv })); + execFileSync("./node_modules/.bin/tsx", ["scripts/redact_public_inquiry.ts"], { + env: { ...redactionEnv, PRIVACY_REDACTION_APPROVED: "yes" }, + }); + const [redacted] = await db`SELECT name,email,phone,message,redacted_at FROM public_inquiry WHERE id=${intake.id}`; + assert.equal(redacted.name, null); + assert.equal(redacted.email, null); + assert.equal(redacted.phone, null); + assert.equal(redacted.message, null); + assert(redacted.redacted_at); + const audits = await db`SELECT id FROM identity_audit WHERE action='operator.redact_public_inquiry' + AND metadata->>'inquiry_id'=${intake.id}`; + assert.equal(audits.length, 1); + await assert.rejects(db`DELETE FROM public_inquiry WHERE id=${intake.id}`); + // Render owner-supplied editorial drafts only inside this disposable CI database. + // No policy draft is included or published by this fixture. + await db`UPDATE cms_page SET state='archived',published_revision_id=NULL WHERE id=${service.id}`; + const editorial = JSON.parse(readFileSync(new URL("../docs/PHASE-05-CMS-COPY.json", import.meta.url), "utf8")); + for (const draft of editorial.pages) { + let [stored] = await db`SELECT id FROM cms_page WHERE page_key=${draft.page_key}`; + if (!stored) { + [stored] = await db`INSERT INTO cms_page(page_key,kind,created_by) + VALUES (${draft.page_key},${draft.kind},${author.id}) RETURNING id`; + } + if (draft.kind === "service") await db`INSERT INTO catalog_service(page_id,category) + VALUES (${stored.id},${draft.category})`; + const [version] = await db`SELECT coalesce(max(revision_no),0)::int + 1 AS next + FROM cms_revision WHERE page_id=${stored.id}`; + const [revision] = await db`INSERT INTO cms_revision(page_id,revision_no,en,bn,created_by) + VALUES (${stored.id},${version.next},${db.json(draft.en)},${db.json(draft.bn)},${author.id}) RETURNING id`; + await db`UPDATE cms_page SET state='published',published_revision_id=${revision.id} + WHERE id=${stored.id}`; + } + console.log("Public core integration passed: publication, inquiry controls and reviewed personal-field redaction"); +} finally { await db.end(); } diff --git a/scripts/redact_public_inquiry.ts b/scripts/redact_public_inquiry.ts new file mode 100644 index 0000000..b2c5aa3 --- /dev/null +++ b/scripts/redact_public_inquiry.ts @@ -0,0 +1,45 @@ +/** + * Operator-only privacy procedure. The protected operator environment must + * authenticate the operator and retain the requester's identity check plus an + * independent review reference. Never expose this script through HTTP. + */ +import postgres from "postgres"; + +const databaseUrl = process.env.DATABASE_URL; +const inquiryId = process.env.INQUIRY_ID?.trim(); +const operator = process.env.OPERATOR_IDENTITY?.trim(); +const requestReference = process.env.PRIVACY_REQUEST_REFERENCE?.trim(); +const reviewReference = process.env.PRIVACY_REVIEW_REFERENCE?.trim(); +const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; +const reference = /^[a-zA-Z0-9][a-zA-Z0-9._-]{3,99}$/; + +if (!databaseUrl || !inquiryId || !operator || !requestReference || !reviewReference || + process.env.PRIVACY_REDACTION_APPROVED !== "yes") { + throw new Error("Reviewed privacy request and operator approval marker are required"); +} +if (!uuid.test(inquiryId) || operator.length > 200 || !reference.test(requestReference) || + !reference.test(reviewReference) || requestReference === reviewReference) { + throw new Error("Invalid inquiry redaction context"); +} + +async function main(url: string, id: string, operatorIdentity: string, requestRef: string, reviewRef: string) { + const db = postgres(url, { max: 1 }); + try { + await db.begin(async (tx) => { + const rows = await tx`SELECT id,redacted_at FROM public_inquiry WHERE id=${id} FOR UPDATE`; + if (rows.length !== 1 || rows[0].redacted_at) throw new Error("Inquiry not found or already redacted"); + await tx`UPDATE public_inquiry SET name=NULL,email=NULL,phone=NULL,message=NULL, + redacted_at=now(),redacted_by=${operatorIdentity}, + redaction_request_reference=${requestRef},redaction_review_reference=${reviewRef} + WHERE id=${id}`; + }); + process.stdout.write(`Inquiry personal fields redacted: ${id}\n`); + } finally { + await db.end(); + } +} + +main(databaseUrl, inquiryId, operator, requestReference, reviewReference).catch((error: unknown) => { + process.stderr.write(`${error instanceof Error ? error.message : "Inquiry redaction failed"}\n`); + process.exitCode = 1; +}); diff --git a/src/app/[locale]/[slug]/page.tsx b/src/app/[locale]/[slug]/page.tsx new file mode 100644 index 0000000..a87e069 --- /dev/null +++ b/src/app/[locale]/[slug]/page.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { PublicContent, UnpublishedPage } from "@/components/public/PublicContent"; +import { getDictionary } from "@/i18n/get-dictionary"; +import { isLocale } from "@/i18n/locales"; +import { publishedPage } from "@/lib/public-content"; +import { publicMetadata } from "@/lib/public-seo"; +import { ContactForm } from "@/components/public/ContactForm"; +import { ContactDetails } from "@/components/public/ContactDetails"; + +const keys = ["about", "contact", "faq", "privacy", "terms", "accessibility"] as const; +type Key = typeof keys[number]; +function isKey(value: string): value is Key { return keys.includes(value as Key); } +export const dynamic = "force-dynamic"; + +export async function generateMetadata({ params }: { params: Promise<{ locale: string; slug: string }> }): Promise { + const { locale, slug } = await params; + if (!isLocale(locale) || !isKey(slug)) return { robots: { index: false } }; + return publicMetadata(await publishedPage(slug, locale), locale, `/${locale}/${slug}`); +} + +export default async function PublicStaticPage({ params }: { params: Promise<{ locale: string; slug: string }> }) { + const { locale, slug } = await params; + if (!isLocale(locale) || !isKey(slug)) notFound(); + const [page, privacy] = await Promise.all([publishedPage(slug, locale), slug === "contact" ? publishedPage("privacy", locale) : Promise.resolve(null)]); + const t = getDictionary(locale).public; + return <> + {page ? : } + {slug === "contact" && } + {slug === "contact" && page && (privacy && (process.env.INQUIRY_RATE_SECRET?.length ?? 0) >= 32 + ? + :

    {t.unavailable}

    )} + ; +} diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 35e4836..5232354 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -5,6 +5,8 @@ import { getDictionary } from "@/i18n/get-dictionary"; import { SkipLink } from "@/components/shell/SkipLink"; import { SiteHeader } from "@/components/shell/SiteHeader"; import { HtmlLangSync } from "@/components/shell/HtmlLangSync"; +import { publishedNavigation } from "@/lib/public-content"; +import Link from "next/link"; export default async function LocaleLayout({ children, @@ -21,15 +23,19 @@ export default async function LocaleLayout({ const cookieStore = await cookies(); const themeCookie = cookieStore.get("theme")?.value; const initialTheme: "light" | "dark" = themeCookie === "dark" ? "dark" : "light"; + const navigation = await publishedNavigation(locale); return ( <> - +
    {children}
    +
    {dictionary.brand}{navigation.some((item) => item.slot === "footer") && }
    ); } diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx index a0a628e..99df83f 100644 --- a/src/app/[locale]/page.tsx +++ b/src/app/[locale]/page.tsx @@ -1,21 +1,23 @@ -import { getDictionary } from "@/i18n/get-dictionary"; -import { isLocale, type Locale } from "@/i18n/locales"; +import type { Metadata } from "next"; import { notFound } from "next/navigation"; +import { getDictionary } from "@/i18n/get-dictionary"; +import { isLocale } from "@/i18n/locales"; +import { publishedPage } from "@/lib/public-content"; +import { publicMetadata } from "@/lib/public-seo"; +import { PublicContent, UnpublishedPage } from "@/components/public/PublicContent"; -export default async function LocaleHomePage({ params }: { params: Promise<{ locale: string }> }) { - const { locale: rawLocale } = await params; - if (!isLocale(rawLocale)) notFound(); - const locale: Locale = rawLocale; - const dictionary = getDictionary(locale); +export const dynamic = "force-dynamic"; - return ( -
    -

    - {dictionary.placeholder.heading} -

    -

    - {dictionary.placeholder.body} -

    -
    - ); +export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise { + const { locale } = await params; + if (!isLocale(locale)) return { robots: { index: false } }; + return publicMetadata(await publishedPage("home", locale), locale, `/${locale}`); +} + +export default async function LocaleHomePage({ params }: { params: Promise<{ locale: string }> }) { + const { locale } = await params; + if (!isLocale(locale)) notFound(); + const page = await publishedPage("home", locale); + const t = getDictionary(locale).public; + return page ? : ; } diff --git a/src/app/[locale]/services/[slug]/page.tsx b/src/app/[locale]/services/[slug]/page.tsx new file mode 100644 index 0000000..91b92a7 --- /dev/null +++ b/src/app/[locale]/services/[slug]/page.tsx @@ -0,0 +1,23 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { PublicContent } from "@/components/public/PublicContent"; +import { isLocale } from "@/i18n/locales"; +import { publishedServiceBySlug } from "@/lib/public-content"; +import { publicMetadata } from "@/lib/public-seo"; + +export const dynamic = "force-dynamic"; + +export async function generateMetadata({ params }: { params: Promise<{ locale: string; slug: string }> }): Promise { + const { locale, slug } = await params; + if (!isLocale(locale)) return { robots: { index: false } }; + const page = await publishedServiceBySlug(slug, locale); + return publicMetadata(page, locale, `/${locale}/services/${slug}`, page ? `/${locale === "en" ? "bn" : "en"}/services/${page.otherSlug}` : undefined); +} + +export default async function ServiceDetailPage({ params }: { params: Promise<{ locale: string; slug: string }> }) { + const { locale, slug } = await params; + if (!isLocale(locale)) notFound(); + const page = await publishedServiceBySlug(slug, locale); + if (!page) notFound(); + return ; +} diff --git a/src/app/[locale]/services/page.tsx b/src/app/[locale]/services/page.tsx new file mode 100644 index 0000000..51e9dd7 --- /dev/null +++ b/src/app/[locale]/services/page.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { getDictionary } from "@/i18n/get-dictionary"; +import { isLocale } from "@/i18n/locales"; +import { publishedPage, publishedServices } from "@/lib/public-content"; +import { publicMetadata } from "@/lib/public-seo"; + +export const dynamic = "force-dynamic"; + +export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise { + const { locale } = await params; + if (!isLocale(locale)) return { robots: { index: false } }; + return publicMetadata(await publishedPage("services", locale), locale, `/${locale}/services`); +} + +export default async function ServicesPage({ params }: { params: Promise<{ locale: string }> }) { + const { locale } = await params; + if (!isLocale(locale)) notFound(); + const [page, services] = await Promise.all([publishedPage("services", locale), publishedServices(locale)]); + const t = getDictionary(locale).public; + return
    +

    {locale === "bn" ? "অনস্কিলআইটি" : "OnSkillIT"}

    {page?.content.title ?? t.services}

    + {page &&

    {page.content.description}

    }
    + {services.length ?
      {services.map((service) =>
    • +

      {service.content.title}

      {service.content.description}

      + {t.serviceLink} +
    • )}
    :

    {t.noServices}

    } +
    ; +} diff --git a/src/app/api/v1/cms/[...path]/route.ts b/src/app/api/v1/cms/[...path]/route.ts index 72b9b96..c308f0b 100644 --- a/src/app/api/v1/cms/[...path]/route.ts +++ b/src/app/api/v1/cms/[...path]/route.ts @@ -112,10 +112,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ const body=await bodyOf(request); if (!body) return bad("INVALID_BODY"); if (part === "pages") { if (!await allowed("pages.write")) return bad("FORBIDDEN",403); - if (!only(body,["page_key","kind","en","bn"]) || typeof body.page_key !== "string" || !KEY.test(body.page_key) || !["page","landing"].includes(String(body.kind)) || !content(body.en,false,"en") || !content(body.bn,false,"bn")) return bad("INVALID_PAGE"); + if (!only(body,["page_key","kind","category","en","bn"]) || typeof body.page_key !== "string" || !KEY.test(body.page_key) || !["page","landing","service"].includes(String(body.kind)) || + (body.kind === "service" && (typeof body.category !== "string" || !KEY.test(body.category))) || + (body.kind !== "service" && body.category !== undefined) || !content(body.en,false,"en") || !content(body.bn,false,"bn")) return bad("INVALID_PAGE"); const en=body.en as LocaleContent, bn=body.bn as LocaleContent; try { const id=await db.begin(async tx=>{ const pages=await tx`INSERT INTO cms_page(page_key,kind,created_by) VALUES (${body.page_key as string},${body.kind as string},${session.userId}) RETURNING id`; await tx`INSERT INTO cms_revision(page_id,revision_no,en,bn,created_by) VALUES (${pages[0].id},1,${tx.json(en)},${tx.json(bn)},${session.userId})`; + if (body.kind === "service") await tx`INSERT INTO catalog_service(page_id,category) VALUES (${pages[0].id},${body.category as string})`; await tx`INSERT INTO identity_audit(actor_user_id,action,outcome,request_id,metadata) VALUES (${session.userId},'cms.page_create','success',${requestId},${tx.json({ page_id: pages[0].id })})`; return pages[0].id as string; }); return json({id},201); } catch (error) { if (typeof error === "object" && error && "code" in error && error.code === "23505") return bad("PAGE_KEY_IN_USE",409); throw error; } diff --git a/src/app/api/v1/public/inquiries/route.ts b/src/app/api/v1/public/inquiries/route.ts new file mode 100644 index 0000000..a88b913 --- /dev/null +++ b/src/app/api/v1/public/inquiries/route.ts @@ -0,0 +1,87 @@ +import { createHmac } from "node:crypto"; +import { NextRequest, NextResponse } from "next/server"; +import { createDbClient, getDatabaseUrl } from "@/db/client"; +import { isLocale } from "@/i18n/locales"; + +export const runtime = "nodejs"; +const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +function fail(code: string, status: number) { + return NextResponse.json({ code }, { status, headers: { "Cache-Control": "no-store" } }); +} + +function text(value: unknown, min: number, max: number): value is string { + return typeof value === "string" && value.trim().length >= min && value.length <= max; +} + +async function limitedBody(request: NextRequest): Promise | null> { + if (!request.headers.get("content-type")?.startsWith("application/json")) return null; + const reader = request.body?.getReader(); + if (!reader) return null; + const chunks: Uint8Array[] = []; + let length = 0; + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + length += value.length; + if (length > 10000) { await reader.cancel(); return null; } + chunks.push(value); + } + try { + const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8")); + return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record : null; + } catch { return null; } +} + +export async function POST(request: NextRequest) { + const databaseUrl = getDatabaseUrl(); + const secret = process.env.INQUIRY_RATE_SECRET; + if (!databaseUrl || !secret || secret.length < 32) return fail("UNAVAILABLE", 503); + const origin = request.headers.get("origin"); + if (origin) { + try { if (new URL(origin).origin !== request.nextUrl.origin) return fail("FORBIDDEN", 403); } + catch { return fail("FORBIDDEN", 403); } + } + const body = await limitedBody(request); + if (!body) return fail("INVALID_REQUEST", 400); + if (body.website) return fail("INVALID_REQUEST", 400); + if (!text(body.name, 2, 120) || !text(body.email, 3, 254) || !emailPattern.test(body.email) || + !text(body.message, 10, 4000) || typeof body.locale !== "string" || !isLocale(body.locale) || body.consent !== true || + (body.phone !== undefined && body.phone !== "" && (typeof body.phone !== "string" || !/^\+?[0-9 ()-]{7,25}$/.test(body.phone))) || + !text(body.consentVersion, 36, 36) || !/^[0-9a-f-]{36}$/i.test(body.consentVersion) || + body.sourcePath !== `/${body.locale}/contact`) return fail("INVALID_REQUEST", 400); + + const email = body.email.trim().toLowerCase(); + const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || request.headers.get("x-real-ip") || "unknown"; + const digest = (value: string) => createHmac("sha256", secret).update(value).digest("hex"); + const db = createDbClient(databaseUrl); + try { + const keys = [ + { key: digest("public-inquiry:global"), limit: 300, minutes: 60 }, + { key: digest(`public-inquiry:ip:${ip}`), limit: 10, minutes: 60 }, + { key: digest(`public-inquiry:email:${email}`), limit: 5, minutes: 60 }, + ]; + const result = await db.begin(async (tx) => { + for (const rule of keys) { + const rows = await tx<{ count: number }[]>`INSERT INTO identity_rate_limit(key_hash,count,window_start) + VALUES (${rule.key},1,now()) ON CONFLICT (key_hash) DO UPDATE SET + count=CASE WHEN identity_rate_limit.window_start < now() - (${rule.minutes} * interval '1 minute') + THEN 1 ELSE identity_rate_limit.count + 1 END, + window_start=CASE WHEN identity_rate_limit.window_start < now() - (${rule.minutes} * interval '1 minute') + THEN now() ELSE identity_rate_limit.window_start END RETURNING count`; + if (Number(rows[0]?.count ?? 0) > rule.limit) return "RATE_LIMITED"; + } + const privacy = await tx`SELECT p.published_revision_id FROM cms_page p + WHERE p.page_key='privacy' AND p.state='published' AND p.published_revision_id=${body.consentVersion as string} LIMIT 1`; + const contact = await tx`SELECT 1 FROM cms_page WHERE page_key='contact' AND state='published' LIMIT 1`; + if (!privacy.length || !contact.length) return "UNAVAILABLE"; + await tx`INSERT INTO public_inquiry(name,email,phone,message,locale,consent_version,source_path) + VALUES (${(body.name as string).trim()},${email},${typeof body.phone === "string" && body.phone.trim() ? body.phone.trim() : null},${(body.message as string).trim()},${body.locale as string}, + ${body.consentVersion as string},${body.sourcePath as string})`; + return "OK"; + }); + if (result === "RATE_LIMITED") return fail("RATE_LIMITED", 429); + if (result !== "OK") return fail("UNAVAILABLE", 503); + return NextResponse.json({ status: "received" }, { status: 202, headers: { "Cache-Control": "no-store" } }); + } finally { await db.end(); } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 28dceff..866a394 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,10 +1,10 @@ import type { Metadata } from "next"; import { cookies, headers } from "next/headers"; -import { Inter, Noto_Sans_Bengali } from "next/font/google"; +import { Inter, Hind_Siliguri } from "next/font/google"; import "@/styles/tokens.css"; const inter = Inter({ subsets: ["latin"], variable: "--font-en", display: "swap" }); -const notoSansBengali = Noto_Sans_Bengali({ +const hindSiliguri = Hind_Siliguri({ subsets: ["bengali"], weight: ["400", "500", "600", "700"], variable: "--font-bn", @@ -32,7 +32,7 @@ export default async function RootLayout({ children }: Readonly<{ children: Reac const locale = headerStore.get("x-locale") ?? "en"; return ( - +