Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,8 @@ jobs:
path: apps/docs/.next
retention-days: 14
if-no-files-found: error
# Next.js writes its build output to `.next/` — a dot-prefixed
# directory which actions/upload-artifact@v4 treats as hidden
# and skips by default. Without this flag the upload step
# reports "No files were found" even when the build succeeded.
include-hidden-files: true
10 changes: 9 additions & 1 deletion apps/admin/src/app/(authenticated)/comments/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@
* (`apps/api/internal/admin/comments`). Keeping them in one place
* means the list, detail, status badge, and bulk-action components
* compile against a single contract.
*
* The admin endpoint (`/api/v1/admin/comments`) emits a richer row
* shape than the public spec's `Comment` schema (it joins the post
* title, threads, etc.), so the row interfaces below are NOT derived
* from the spec wholesale. The status enum, however, IS shared with
* the public API and is sourced from there — issue #514 follow-up so
* an enum value change shows up as a type error here.
*/
import type { components } from '@gonext/api-types';

/** Canonical moderation states. Mirrors the API's Status type. */
export type CommentStatus = 'pending' | 'approved' | 'spam' | 'trash';
export type CommentStatus = components['schemas']['Comment']['status'];

/** Bulk action verbs accepted by `/api/v1/admin/comments/bulk`. */
export type BulkAction = 'approve' | 'spam' | 'trash';
Expand Down
41 changes: 25 additions & 16 deletions apps/admin/src/app/(authenticated)/jobs/dlq/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@
* Field nullability follows the Go side: optional fields use `?` and
* map to `omitempty` JSON tags. When the Go contract changes, this
* file changes with it.
*
* Issue #514 follow-up: `ArchivedTask` is sourced from the OpenAPI
* spec via `@gonext/api-types` so the wire fields are in lock-step
* with the server's struct tags. The list rendering depends on
* `failed_at` being present (the row's primary timestamp), so we
* tighten that field locally — once the spec marks it required this
* override can go away.
*/
import type { components } from '@gonext/api-types';

/**
* A single archived (dead-letter) task as returned by the list and
Expand All @@ -22,21 +30,18 @@
* `redacted_fields` is present only when the task has an active
* redaction record — the UI uses its presence to render the masked
* badge and the "fields masked: X, Y" hint.
*
* `failed_at` is required client-side because every list row renders
* the relative "5m ago" timestamp — a missing value would degrade to
* "Invalid date". The spec marks it optional; this is a local tighten
* tracked as a spec follow-up.
*/
export interface ArchivedTask {
id: string;
queue: string;
type: string;
payload_preview: string;
/** Base64-encoded raw bytes on the detail endpoint; omitted on list. */
payload?: string;
last_error: string;
export type ArchivedTask = Omit<
components['schemas']['ArchivedTask'],
'failed_at'
> & {
failed_at: string;
retried: number;
max_retry: number;
redacted: boolean;
redacted_fields?: string[];
}
};

/**
* The paginated list response — matches `router.Page[ArchivedTask]`
Expand All @@ -52,11 +57,15 @@ export interface DLQListResponse {

/**
* The body shape for `POST /dlq/{id}/redact`.
*
* Extends the spec's `RedactRequest` (which only models `fields`) with
* the admin endpoint's `queue` discriminator. The spec will gain the
* field once the admin DLQ surface is folded into the public schema
* — tracked as #514 follow-up.
*/
export interface RedactRequest {
export type RedactRequest = components['schemas']['RedactRequest'] & {
queue: string;
fields: string[];
}
};

/**
* Common queue names for the filter chip. We don't fetch the actual
Expand Down
30 changes: 20 additions & 10 deletions apps/admin/src/app/(authenticated)/pages/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import Link from 'next/link';
import { Suspense, type ReactElement } from 'react';
import { Plus } from 'lucide-react';
import type { components } from '@gonext/api-types';
import { serverApiFetch } from '@/lib/server-api';
import { Headline } from '@/components/ui/headline';
import { Button } from '@/components/ui/button';
Expand All @@ -35,10 +36,11 @@ import { Badge } from '@/components/ui/badge';
export const dynamic = 'force-dynamic';

/** Status values the page list surfaces. Mirrors the WP-compat set
* the API speaks. The API uses `publish`/`scheduled`/`draft`; we
* normalise scheduled to `future` so the badge logic stays aligned
* with the post detail surface. */
export type PageStatus = 'publish' | 'draft' | 'future' | 'private' | 'trash';
* the API speaks. The spec's `Post.status` (Page is a Post alias)
* carries the canonical enum — we source it from there so a status
* alphabet change in `openapi.yaml` shows up as a type error rather
* than as silent UI drift (issue #514 follow-up). */
export type PageStatus = components['schemas']['Page']['status'];

/** Flatter shape the list UI renders. Pulled out of the adapter so
* the tests can exercise the field-mapping rules without spinning up
Expand All @@ -52,15 +54,23 @@ export interface SitePage {
updatedAt: string;
}

/** Wire shape we expect from `GET /api/v1/posts?post_type=page`. */
export type ApiPagePost = {
/**
* Wire shape we expect from `GET /api/v1/posts?post_type=page`.
*
* Issue #514 follow-up: derived from the OpenAPI spec's `Page` schema
* (which aliases `Post`). The list endpoint emits a sparse projection
* — most fields the spec marks required are absent on a list row — so
* we treat every field as optional via `Partial<>` and guarantee `id`
* explicitly (the row key MUST be present). `status` is explicitly
* widened back to `string` because the API sometimes returns the
* past-tense `published` / `scheduled` variants which the adapter
* normalises to the canonical enum (and the tests exercise that
* branch with fixture values like `"bogus"`).
*/
export type ApiPagePost = Omit<Partial<components['schemas']['Page']>, 'status'> & {
id: string;
title?: string;
slug?: string;
status?: string;
published_at?: string | null;
updated_at?: string;
created_at?: string;
};

/**
Expand Down
28 changes: 23 additions & 5 deletions apps/admin/src/app/(authenticated)/posts/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
} from '@gonext/blocks-editor';
import { BlockRegistry } from '@gonext/blocks-sdk';
import type { BlockTree } from '@gonext/blocks-sdk';
import type { components } from '@gonext/api-types';

import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
Expand All @@ -47,14 +48,31 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { api } from '@/lib/api-client';

type PostStatus = 'draft' | 'publish' | 'future' | 'private';
/**
* Status alphabet — sourced from the OpenAPI spec (issue #514 follow-up).
* The editor UI only exposes the four user-facing values; `pending` and
* `trash` are valid server states but flow in through the list screen
* (bulk actions) rather than this dropdown.
*/
type PostStatus = components['schemas']['Post']['status'];

interface UpdatePostBody {
title?: string;
slug?: string;
/**
* Body for `PATCH /api/v1/posts/{id}`.
*
* Issue #514 follow-up: based on the spec's `PostUpdate` schema. The
* spec types `status` as `string`; we tighten it here to the strict
* enum so the dropdown handler can't smuggle in a bogus value. The
* spec types `content_blocks` as an opaque `Record<string, never>`
* placeholder; the editor needs the typed `BlockTree`, so we override
* that field.
*/
type UpdatePostBody = Omit<
components['schemas']['PostUpdate'],
'status' | 'content_blocks'
> & {
status?: PostStatus;
content_blocks?: BlockTree;
}
};

export default function PostDetailPage(): ReactElement {
const params = useParams<{ id: string }>();
Expand Down
17 changes: 9 additions & 8 deletions apps/admin/src/app/(authenticated)/posts/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@
* deferred to the edit screen.
*/
import type { ReactElement } from 'react';
import type { components } from '@gonext/api-types';
import styles from './posts.module.css';

/** Canonical post status set used across the admin. */
export type PostStatus =
| 'publish'
| 'draft'
| 'pending'
| 'private'
| 'future'
| 'trash';
/**
* Canonical post status set used across the admin.
*
* Issue #514 follow-up: derived from the OpenAPI spec instead of
* hand-typed so a status alphabet change in `openapi.yaml` shows up
* here as a type error rather than as silent UI drift.
*/
export type PostStatus = components['schemas']['Post']['status'];

/** Shape of a single post row as returned by `/api/v1/posts`. */
export interface Post {
Expand Down
39 changes: 33 additions & 6 deletions apps/admin/src/app/(authenticated)/posts/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,52 @@ import {
type ReactElement,
} from 'react';
import { ChevronLeft, Loader2, Plus } from 'lucide-react';
import type { components } from '@gonext/api-types';

import { ApiError, api } from '@/lib/api-client';
import { Button } from '@/components/ui/button';
import { Headline } from '@/components/ui/headline';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';

type PostStatus = 'draft' | 'publish' | 'private' | 'future';
/**
* Status alphabet — sourced from the spec (issue #514 follow-up). The
* form only exposes the four operator-facing states; `pending` and
* `trash` (also in the spec enum) are workflow transitions that flow
* through other surfaces (review queue, list bulk actions).
*/
type PostStatus = components['schemas']['Post']['status'];

interface CreatePostBody {
/**
* Body posted to `/api/v1/posts`.
*
* Issue #514 follow-up: derived from the spec's `PostCreate` schema.
* Two local overrides:
* • `title`/`slug`/`status` are required by the form (the spec
* marks them optional because the server fills sensible defaults
* when omitted, but the UI always sends them).
* • `content_blocks` is typed as a `Record<string, never>` placeholder
* in the spec (the spec models block-tree JSON as opaque). The
* create form sends an empty array to seed the post; the spec will
* gain a typed block-tree schema later (issue #514 follow-up).
*/
type CreatePostBody = Omit<
components['schemas']['PostCreate'],
'title' | 'slug' | 'status' | 'content_blocks'
> & {
title: string;
slug: string;
status: PostStatus;
content_blocks: never[];
}
};

interface CreatePostResponse {
id: string;
}
/**
* Server response on success. The create endpoint returns the full
* `Post` projection per the spec; the form only reads the `id` to
* route to the editor, so we narrow with `Pick` to avoid coupling to
* fields the form doesn't care about.
*/
type CreatePostResponse = Pick<components['schemas']['Post'], 'id'>;

/**
* Derive a URL-safe slug from a free-form title. Mirrors the server's
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* On-wire shapes for /me/tokens. These mirror the Go IssuedTokenView /
* TokenView structs (apps/api/internal/admin/tokens/handler.go). Keep them
* TokenView structs (apps/api/internal/auth/pat/handler.go). Keep them
* in lock-step — a mismatched field is a runtime decode failure, not a
* type error.
*/
Expand Down
48 changes: 32 additions & 16 deletions apps/admin/src/app/(authenticated)/webhooks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,31 @@
* changes, this file changes with it — there's no separate mapping
* step, the fetch layer just asserts the response as one of these
* types.
*
* Issue #514 follow-up: the base wire fields (`id`, `url`, `events`,
* `active`, `created_at`) are sourced from the OpenAPI spec via
* `@gonext/api-types`. The admin endpoint emits a richer projection
* (delivery telemetry, name, updated_at) that the spec doesn't model
* yet — those fields stay as a local intersection so a spec change
* still flags drift on the shared fields.
*/
import type { components } from '@gonext/api-types';

type WebhookSpec = components['schemas']['Webhook'];

export interface Subscription {
id: string;
export type Subscription = Pick<
WebhookSpec,
'id' | 'url' | 'events' | 'active' | 'created_at'
> & {
name: string;
url: string;
events: string[];
active: boolean;
created_by?: string;
created_at: string;
updated_at: string;
last_delivery_at?: string;
last_delivery_status?: 'success' | 'retry' | 'failed' | '';
last_delivery_response_code?: number;
consecutive_failures: number;
degraded_at?: string;
}
};

/**
* Returned by POST /webhooks — includes the raw HMAC secret as a hex
Expand All @@ -34,19 +42,27 @@ export interface SubscriptionWithSecret extends Subscription {
secret: string;
}

export interface SubscriptionCreate {
/**
* Body for POST /webhooks. Extends the spec's `WebhookCreate` with the
* admin-only `name` field (the spec doesn't model it yet — tracked as
* follow-up). `active` is required by the spec; we relax it here so
* the create form can omit it and let the server default kick in.
*/
export type SubscriptionCreate = Omit<
components['schemas']['WebhookCreate'],
'secret' | 'active'
> & {
name: string;
url: string;
events: string[];
active?: boolean;
}
};

export interface SubscriptionUpdate {
/**
* Body for PATCH /webhooks/{id}. The admin form additionally allows
* editing `name`, which the spec doesn't model yet.
*/
export type SubscriptionUpdate = components['schemas']['WebhookUpdate'] & {
name?: string;
url?: string;
events?: string[];
active?: boolean;
}
};

export interface Delivery {
id: number;
Expand Down
8 changes: 6 additions & 2 deletions apps/api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# -t ghcr.io/singleton-solution/gonext-api:dev .
#
# Layer story (see docs/09-deployment-ops.md §2.2):
# 1. golang:1.25-alpine builder with module cache mount
# 1. golang:1.25.10-alpine builder with module cache mount
# 2. distroless/static-debian12:nonroot runtime, ~25-30 MB
#
# The Go workspace at the repo root (go.work) references this module plus
Expand All @@ -29,7 +29,11 @@
ARG IMAGEPROC_BACKEND=stdlib

# ---------- Stage 1: build ----------
FROM golang:1.25-alpine AS build
# Pinned to a specific patch so govulncheck's stdlib coverage is reproducible
# across runs. Bumped from 1.25 → 1.25.10 to clear the stdlib CVEs that the
# `security-scan` CI job flagged (net/mail, html/template, net, crypto/x509,
# crypto/tls, net/http, os, net/url all have fixes in 1.25.8–1.25.10).
FROM golang:1.25.10-alpine AS build

# IMAGEPROC_BACKEND selects between the pure-Go default (`stdlib`) and
# the libvips-backed `govips` build of the on-the-fly image proxy
Expand Down
Loading