# Portfolio, Publishing, and Recruitment

Phase 10 adds three permission-separated business modules and public read
surfaces. All administrative writes require confirmed privileged sessions and
the module-specific permission.

## Portfolio

- Clients, projects, and partners use `draft`, `published`, and terminal
  `archived` states with optimistic `lock_version` checks.
- A published project requires a published client. Every referenced service and
  technology must also be published.
- Public `/work` queries published records only and orders them explicitly.
- Slugs are unique per record type. Project catalog pivots are unique and use
  restrictive dependency deletion.

## Publishing

- `posts` are stable identities; editorial content lives in immutable numbered
  `post_revisions`.
- `blog_page_settings` stores the editable blog landing-page structure: page
  title and description, ordered category filters, featured article, ordered
  popular articles, latest-articles labels, and the closing CTA.
- The administration page `/admin/posts` manages the landing-page sections and
  every article from one place. Categories and article content sections can be
  added or removed; articles can be created, revised, published, moved to the
  recycle bin, restored, or permanently removed by an authorized operator.
- Each article revision stores its title, category, author, reading time, cover
  image, introduction, excerpt, body sections, conclusion, and an independent
  CTA card. These fields are returned by the article-detail API.
- Revisions move through `draft`, `scheduled`, `published`, and `archived`.
  Publishing a revision atomically archives the prior live revision and records
  both transitions in append-only history.
- `posts:publish-due` runs every minute. It locks due revisions, rechecks their
  state, publishes once, writes history, and emits a system audit record.
- `/blog` and `/blog/{post}` expose only the current published revision.
- `GET /api/v1/public/posts` returns the page settings together with
  `sections.featured`, `sections.popular`, and `sections.latest` plus the
  backward-compatible paginated `data` list.
- `GET /api/v1/public/posts/{slug}` returns the full published article,
  including its dynamic sections, conclusion, and CTA.
- Protected read endpoints are `GET /api/v1/resources/blog-page-settings`,
  `GET /api/v1/resources/posts`, and `GET /api/v1/resources/post-revisions`;
  they require `blog.manage`.

## Recruitment

- Recruitment records use `job_postings`; Laravel's framework-owned `jobs`
  queue table remains untouched.
- Jobs move through `draft`, `open`, `closed`, and terminal `archived` states.
  Public listings and direct routes enforce both status and the half-open
  availability window (`opens_at <= now < closes_at`).
- An applicant may submit once per job and normalized email. CVs must be PDFs,
  are stored on the configured private media disk, and are downloadable only by
  users with `applications.manage`.
- Applications move through `submitted`, `reviewing`, `shortlisted`, and a
  terminal `rejected` or `hired` state. Initial submission and every later
  transition are appended to status history; reviewer writes use optimistic
  locking.

## Operations and recovery

- Keep the scheduler running and alert on overdue scheduled posts.
- Back up private CV storage with the database and reconcile media checksums
  after restore. The application cleans up both the media row and stored object
  when application persistence fails.
- Treat applicant data and CVs as restricted personal data. Limit
  `applications.manage`, log downloads at the reverse proxy if required by the
  retention policy, and never expose the private disk publicly.
- Run MySQL 8 CI before production certification; SQLite does not certify row
  locking behavior.
