Skip to content

Added Challenge Series Feature#15

Merged
yulmwu merged 5 commits into
mainfrom
feat/chall-series
May 19, 2026
Merged

Added Challenge Series Feature#15
yulmwu merged 5 commits into
mainfrom
feat/chall-series

Conversation

@yulmwu
Copy link
Copy Markdown
Member

@yulmwu yulmwu commented May 19, 2026

In the existing platform, challenges could be categorized by level or category. However, after actually using the platform and collecting feedback from students, we found that a curriculum-oriented series feature was needed. This PR implements the series feature.

The database tables that require migration are listed below.

BEGIN;

CREATE TABLE IF NOT EXISTS challenge_series (
    id BIGSERIAL PRIMARY KEY,
    title VARCHAR(255) NOT NULL UNIQUE,
    description TEXT NOT NULL,
    created_by_user_id BIGINT NULL REFERENCES users(id) ON DELETE SET NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS challenge_series_challenges (
    id BIGSERIAL PRIMARY KEY,
    series_id BIGINT NOT NULL REFERENCES challenge_series(id) ON DELETE CASCADE,
    challenge_id BIGINT NOT NULL REFERENCES challenges(id) ON DELETE CASCADE,
    position INT NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE(series_id, challenge_id),
    UNIQUE(series_id, position)
);

CREATE INDEX IF NOT EXISTS idx_challenge_series_created ON challenge_series (created_at DESC, id DESC);
CREATE INDEX IF NOT EXISTS idx_challenge_series_challenges_challenge ON challenge_series_challenges (challenge_id);

COMMIT;

For more details, please refer to the REST API documentation below and the updated source code.

List Challenge Series

GET /api/challenge-series

Query parameters:

  • page (optional, default 1)
  • page_size (optional, default 20, max 100)

Response 200

{
    "series": [
        {
            "id": 1,
            "title": "Warmup Challenge Series",
            "description": "Starter series",
            "created_at": "2026-05-19T12:00:00Z",
            "updated_at": "2026-05-19T12:00:00Z"
        }
    ],
    "pagination": {
        "page": 1,
        "page_size": 20,
        "total_count": 1,
        "total_pages": 1,
        "has_prev": false,
        "has_next": false
    }
}

Errors:

  • 400 invalid input

Get Challenge Series Detail

GET /api/challenge-series/{id}

Response 200

{
    "series": {
        "id": 1,
        "title": "Warmup Challenge Series",
        "description": "Starter series",
        "created_at": "2026-05-19T12:00:00Z",
        "updated_at": "2026-05-19T12:00:00Z"
    },
    "challenges": [
        {
            "id": 5,
            "title": "Warmup 1",
            "category": "Web",
            "created_at": "2026-01-24T12:00:00Z",
            "level": 0,
            "points": 100,
            "solve_count": 12,
            "is_active": true,
            "is_locked": false,
            "is_solved": false,
            "has_file": false,
            "stack_enabled": false,
            "stack_target_ports": []
        }
    ]
}

Notes:

  • Series challenges are returned in admin-defined order.
  • If a challenge is progression-locked, it is returned in reduced form with is_locked: true.

Errors:

  • 400 invalid input
  • 404 challenge series not found

Create Challenge Series

POST /api/admin/challenge-series

Headers

Cookie: access_token=<jwt>

Request

{
    "title": "Warmup Challenge Series",
    "description": "Starter set"
}

Response 201

{
    "id": 1,
    "title": "Warmup Challenge Series",
    "description": "Starter set",
    "created_at": "2026-05-19T12:00:00Z",
    "updated_at": "2026-05-19T12:00:00Z"
}

Errors:

  • 400 invalid input
  • 401 invalid token or missing access_token cookie
  • 403 forbidden
  • 409 challenge series already exists

Update Challenge Series

PUT /api/admin/challenge-series/{id}

Request fields are optional; omitted fields are unchanged.

Response 200: same schema as create.

Errors:

  • 400 invalid input
  • 401 invalid token or missing access_token cookie
  • 403 forbidden
  • 404 challenge series not found
  • 409 challenge series already exists

Delete Challenge Series

DELETE /api/admin/challenge-series/{id}

Response 200

{
    "status": "ok"
}

Errors:

  • 401 invalid token or missing access_token cookie
  • 403 forbidden
  • 404 challenge series not found

Replace Challenge Series Challenges

PUT /api/admin/challenge-series/{id}/challenges

Request

{
    "challenge_ids": [5, 3, 10]
}

Notes:

  • Replaces the full series order atomically.
  • Includes inactive challenges.

Response 200

{
    "status": "ok"
}

Errors:

  • 400 invalid input
  • 401 invalid token or missing access_token cookie
  • 403 forbidden
  • 404 challenge series not found

One thing to note is that pagination is not supported for the challenge list within a series. This was an intentional trade-off to allow challenge ordering to be changed more easily, though it may be revised in the future. For now, this is considered acceptable since series are not expected to contain a large number of challenges.

@yulmwu yulmwu added the enhancement New feature or request label May 19, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a curriculum-oriented “Challenge Series” feature to the platform, adding persistence, backend APIs, and frontend pages/admin tooling to create, order, and browse curated challenge tracks.

Changes:

  • Added DB schema for challenge_series and ordered challenge_series_challenges, plus rollback and seed/test truncation updates.
  • Implemented repo/service/HTTP handlers and routes for listing, viewing, and admin CRUD + atomic challenge-order replacement.
  • Added frontend series list/detail pages, admin management UI, API client/types, routing, and translations.

Reviewed changes

Copilot reviewed 43 out of 44 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
scripts/sql/seed_dummy_data.sql Truncates new series tables during seed reset.
migrations/2026-05-19/001_add_challenge_series.sql Adds series + series-challenges tables and indexes.
migrations/2026-05-19/999_rollback.sql Rollback drops new series tables.
internal/service/wargame_service.go Adds series lifecycle methods to service layer.
internal/service/wargame_service_test.go Adds service-level tests for series behavior/validation/locking.
internal/service/testenv_test.go Injects ChallengeSeriesRepo + truncates new tables in service tests.
internal/service/errors.go Introduces series-specific service errors.
internal/repo/testenv_test.go Truncates new tables in repo tests.
internal/repo/challenge_series_repo.go New Bun repo for series CRUD, listing, ordering, and detail queries.
internal/repo/challenge_series_repo_test.go Repo tests for CRUD/list/order/exists and error paths.
internal/models/challenge_series.go New DB models for series and join table.
internal/http/router.go Registers public + admin series endpoints.
internal/http/integration/testenv_test.go Wires series repo into HTTP integration test env; updates truncation.
internal/http/integration/challenges_test.go Adds integration tests for series CRUD/order + auth.
internal/http/handlers/types.go Adds request/response types and mapping for series payloads.
internal/http/handlers/testenv_test.go Wires series repo into handler test env; updates truncation.
internal/http/handlers/handler.go Implements series handlers (list/detail/admin CRUD/order replace).
internal/http/handlers/handler_test.go Adds handler unit tests for series endpoints and sorting.
internal/http/handlers/errors.go Maps series errors to HTTP status codes.
internal/db/db.go Adds AutoMigrate models and index creation for series tables.
cmd/server/main.go Instantiates series repo and injects into service at runtime.
frontend/src/routes/WriteupEditor.tsx UI style adjustments (layout/padding).
frontend/src/routes/WriteupDetail.tsx UI style adjustments (layout/padding).
frontend/src/routes/Users.tsx UI style adjustments.
frontend/src/routes/CommunityEditor.tsx UI style adjustments (layout/padding).
frontend/src/routes/CommunityDetail.tsx UI style adjustments (layout/padding).
frontend/src/routes/ChallengeSeriesDetail.tsx New public series detail page (ordered challenges, locked indicators).
frontend/src/routes/ChallengeSeries.tsx New public series list page with pagination UI.
frontend/src/routes/Challenges.tsx Adds “featured series” cards and layout tweaks on challenges page.
frontend/src/routes/challenge-detail/ChallengeInfoPanels.tsx UI style adjustments (author/solver panels).
frontend/src/routes/challenge-detail/ChallengeCommentsPanel.tsx UI style adjustments (comment card styling).
frontend/src/routes/admin/Users.tsx UI style adjustments (padding/container).
frontend/src/routes/admin/ChallengeSeriesManagement.tsx New admin UI to CRUD series and reorder challenges.
frontend/src/routes/admin/ChallengeManagement.tsx UI style adjustments (padding/container).
frontend/src/routes/Admin.tsx Adds new admin tab for series management.
frontend/src/locales/ko.json Adds series and admin-series translation strings.
frontend/src/locales/ja.json Adds series and admin-series translation strings.
frontend/src/locales/en.json Adds series and admin-series translation strings.
frontend/src/lib/types.ts Adds TS types for series APIs.
frontend/src/lib/api.ts Adds API client methods for series endpoints.
frontend/src/App.tsx Adds routing for /series and /series/:id.
frontend/public/seedling.svg Adds icon used in featured series cards.
docs/docs/challenges.md Documents public list/detail series endpoints.
docs/docs/admin.md Documents admin series endpoints (create/update/delete/replace).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/db/db.go
Comment thread docs/docs/challenges.md
Comment on lines +1841 to +1849
seen[challengeID] = struct{}{}
if _, err := s.challengeRepo.GetByID(ctx, challengeID); err != nil {
if errors.Is(err, repo.ErrNotFound) {
validator.fields = append(validator.fields, FieldError{Field: fmt.Sprintf("challenge_ids[%d]", i), Reason: "not_found"})
continue
}

return fmt.Errorf("wargame.ReplaceChallengeSeriesChallenges challenge: %w", err)
}
Comment thread internal/http/handlers/handler.go
Comment thread frontend/src/routes/admin/ChallengeSeriesManagement.tsx
Comment thread frontend/src/routes/admin/ChallengeSeriesManagement.tsx Outdated
yulmwu and others added 2 commits May 19, 2026 21:03
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@yulmwu yulmwu merged commit 9cf744f into main May 19, 2026
2 checks passed
@yulmwu yulmwu deleted the feat/chall-series branch May 19, 2026 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants