Skip to content

Upgrade most dependencies and modernize codebase - #1363

Merged
RichDom2185 merged 23 commits into
masterfrom
deps-big-bang-2026
Aug 1, 2026
Merged

Upgrade most dependencies and modernize codebase#1363
RichDom2185 merged 23 commits into
masterfrom
deps-big-bang-2026

Conversation

@RichDom2185

@RichDom2185 RichDom2185 commented Jul 19, 2026

Copy link
Copy Markdown
Member

2026 maintenance update to maintain compatibility with newer Elixir versions to come


This is part 1 of 3 in a stack made with GitButler:

@RichDom2185 RichDom2185 self-assigned this Jul 19, 2026
@RichDom2185

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 21bdf28b-31a3-4cf0-8908-6792b8709f62

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added a dedicated processing queue for autograding jobs, improving separation and throughput.
    • Improved autograding result handling, including clearer failure reporting and reliable result storage.
  • Bug Fixes

    • Assessments and stories now handle opening and closing times more consistently.
    • Assessments without a closing date can now be saved successfully.
    • Improved token refresh handling and date/time display consistency.
  • Maintenance

    • Updated background job scheduling and database support for improved reliability.

Walkthrough

The changes migrate autograder processing from Que to Oban, replace Timex time operations with DateTime APIs across production and tests, update Oban schema/configuration, and apply assorted Phoenix, authentication, logging, dependency, schema, and unused-binding cleanups.

Changes

DateTime time handling

Layer / File(s) Summary
Assessment time semantics
lib/cadet/assessments/*, lib/cadet_web/admin_controllers/admin_assessments_controller.ex
Assessment availability, contest windows, XP decay, leaderboard logic, submission timestamps, and validation use DateTime comparisons and arithmetic.
Shared time utilities and domain flows
lib/cadet/code_exchange.ex, lib/cadet/helpers/model_helper.ex, lib/cadet/jobs/log.ex, lib/cadet/jobs/xml_parser.ex, lib/cadet/stories/*, lib/cadet_web/helpers/view_helper.ex, lib/cadet_web/controllers/auth_controller.ex
Token expiry, job throttling, XML scheduling, story filtering, token timestamps, timezone conversion, and datetime formatting use DateTime APIs.
DateTime test coverage
test/cadet/**/*, test/cadet_web/**/*, test/factories/**/*
Time-based fixtures, comparisons, ISO formatting, and XP-decay calculations are updated to DateTime-based construction and assertions.

Oban autograder migration

Layer / File(s) Summary
Oban configuration and startup
config/config.exs, lib/cadet/application.ex, mix.exs, priv/repo/migrations/20260715000000_update_oban_to_v14.exs
The autograder queue is configured, supervised children use explicit child specs, Que is removed, dependencies are updated, and the Oban v14 migration is added.
Autograder worker flow
lib/cadet/jobs/autograder/*, lib/cadet/jobs/log.ex
Lambda grading and result storage use Oban jobs, normalized arguments and results, and the autograder queue.
Oban autograder tests
test/cadet/jobs/autograder/*
Queue mocks are replaced with Oban testing mode and enqueue assertions, including result persistence verification.

Platform and code cleanup

Layer / File(s) Summary
Runtime and web integration
lib/cadet/auth/*, lib/cadet_web.ex, lib/cadet_web/endpoint.ex, lib/cadet/logger/cloudwatch_logger.ex, lib/cadet_web/controllers/generate_ai_comments.ex
Guardian refresh handling, OpenID claim bindings, Phoenix setup, CORS configuration, logger callbacks, and AI-comment controller argument handling are updated.
Unused bindings and documentation cleanup
lib/cadet/accounts/teams.ex, lib/cadet/assessments/version_manager.ex, lib/cadet/chatbot/llm_conversations.ex, lib/cadet_web/*, lib/cadet/jobs/xml_parser.ex
Private documentation, unused aliases and bindings, and an unused parser macro are removed or marked unused.
Dependency and schema updates
mix.exs, priv/repo/migrations/20230214140555_create_notification_preferences.exs
Dependency constraints and overrides are changed, and the notification foreign-key column loses its explicit non-null declaration.

Estimated code review effort: 4 (Complex) | ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the dependency upgrades and code modernization in the pull request.
Description check ✅ Passed The description relates the maintenance changes to compatibility with newer Elixir versions.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request migrates the background job processing from Que to Oban, replaces Timex usage with Elixir's native DateTime and Date modules, and updates several dependencies. The review feedback identifies a critical security issue in the CORS configuration where compile-time checks cause origins to default to wildcard access, and a bug in the Oban worker failure handler that incorrectly marks failed jobs as completed. Additionally, the feedback suggests removing a redundant variable calculation in teams.ex and simplifying several DateTime.add/4 calls by using native time units directly.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread lib/cadet_web/endpoint.ex Outdated
Comment on lines +5 to +10
cors_origins =
if Code.ensure_loaded?(__MODULE__) do
Application.compile_env(:cadet, :cors_endpoints, "*")
else
"*"
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-critical critical

The module body code is executed at compile time. Since CadetWeb.Endpoint is not yet loaded during its own compilation, Code.ensure_loaded?(__MODULE__) will always evaluate to false at compile time. This causes cors_origins to always be hardcoded to "*", completely ignoring the configured :cors_endpoints in production. You should use Application.compile_env/3 directly.

  cors_origins = Application.compile_env(:cadet, :cors_endpoints, "*")

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Resolved in 49dd427.

Removed the dead Code.ensure_loaded?(__MODULE__) guard (it always evaluated to false during the endpoint's own compilation, forcing origins to "*") and now read the value directly via Application.compile_env(:cadet, :cors_endpoints, "*").

One caveat worth flagging: I intentionally kept the top-level :cors_endpoints key rather than the [CadetWeb.Endpoint, :cors_endpoints] path where config/cadet.exs.example documents it. Production loads that config at runtime (config/releases.exs/etc/cadet.exs), so reading the Endpoint key through compile_env/3 would trip Elixir's compile-vs-runtime consistency check and fail release boot. Behaviour is therefore unchanged from before (the old get_env(:cadet, [CadetWeb.Endpoint, :cors_endpoints], "*") also always returned "*", since get_env/3 doesn't support key paths). Actually wiring the runtime CORS value into Corsica is a separate, pre-existing concern.

Comment on lines 117 to 121
ids = Enum.map(team, &Map.get(&1, "userId"))

unique_ids_count = ids |> Enum.uniq() |> Enum.count()
_unique_ids_count = ids |> Enum.uniq() |> Enum.count()

student_already_in_team?(-1, ids, assessment_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The variable _unique_ids_count is calculated but never used. Since Enum.uniq/1 and Enum.count/1 are pure functions, this calculation is redundant and can be safely removed to improve performance and code clarity.

      ids = Enum.map(team, &Map.get(&1, "userId"))

      student_already_in_team?(-1, ids, assessment_id)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Resolved in 7de83ce.

Removed the _unique_ids_count line entirely (pure computation with no side effects) rather than just underscore-prefixing it.

Comment on lines +2084 to +2088
if DateTime.compare(
cur_time,
DateTime.add(assessment.open_at, early_hours * 3_600, :second)
) ==
:lt do

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Since Elixir 1.14, DateTime.add/4 supports :hour and other units directly. You can simplify early_hours * 3_600, :second to early_hours, :hour to make the code more readable and idiomatic.

          if DateTime.compare(
               cur_time,
               DateTime.add(assessment.open_at, early_hours, :hour)
             ) ==
               :lt do

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Resolved in 0ad414f.

Switched to DateTime.add(assessment.open_at, early_hours, :hour). (hours_before_early_xp_decay is an :integer, so the :hour unit is safe here.)

Comment on lines +2258 to +2265
DateTime.compare(
DateTime.add(
assessment.close_at,
voting_question.question["reveal_hours"] * 3_600,
:second
),
DateTime.utc_now()
) == :lt

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Simplify the DateTime.add/4 call by using the :hour unit directly instead of multiplying by 3_600 seconds.

    DateTime.compare(
      DateTime.add(
        assessment.close_at,
        voting_question.question["reveal_hours"],
        :hour
      ),
      DateTime.utc_now()
    ) == :lt

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Resolved in 0ad414f.

Switched to the :hour unit directly. (reveal_hours is an :integer field.)

Comment thread lib/cadet/assessments/assessments.ex Outdated

def fetch_voting_questions_due_yesterday do
now = DateTime.utc_now()
yesterday = DateTime.add(now, -1 * 86_400, :second)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Simplify the DateTime.add/4 call by using the :day unit directly instead of multiplying by 86_400 seconds.

    yesterday = DateTime.add(now, -1, :day)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Resolved in 0ad414f.

Switched to DateTime.add(now, -1, :day).

Comment thread lib/cadet/jobs/xml_parser.ex Outdated
Comment on lines +74 to +80
open_at =
Timex.now()
|> Timex.beginning_of_day()
|> Timex.shift(days: 3)
|> Timex.shift(hours: 4)
today
|> DateTime.new!(~T[00:00:00.000000])
|> DateTime.add(3 * 86_400, :second)
|> DateTime.add(4 * 3_600, :second)

close_at = Timex.shift(open_at, days: 7)
close_at = DateTime.add(open_at, 7 * 86_400, :second)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Simplify the DateTime.add/4 calls by using :day and :hour units directly instead of multiplying by seconds.

    open_at =
      today
      |> DateTime.new!(~T[00:00:00.000000])
      |> DateTime.add(3, :day)
      |> DateTime.add(4, :hour)

    close_at = DateTime.add(open_at, 7, :day)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Resolved in 0ad414f.

Switched to :day/:hour units for both open_at and close_at.

overwrite: get_arg(args, :overwrite, false)
})

:ok

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In Oban, returning {:error, reason} is the standard way to report job failures. Returning :ok from handle_failure/4 will cause Oban to mark the job as completed rather than failed, which hides failures from Oban's monitoring and telemetry. Since max_attempts: 1 is configured, returning {:error, error_message} will correctly mark the job as failed without retrying.

    {:error, error_message}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Resolved in 9bd62e1.

handle_failure/4 now returns {:error, error_message} so Oban records the job as discarded (with max_attempts: 1) instead of completed, keeping the failure visible in Oban telemetry. The failed result is still enqueued to ResultStoreWorker before returning, so the answer is updated exactly as before, and the failure-handling test was updated to assert {:error, _}.

@coveralls

coveralls commented Jul 19, 2026

Copy link
Copy Markdown

Coverage Status

No base build to compare — deps-big-bang-2026 into master

@RichDom2185

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
lib/cadet/assessments/assessment.ex (1)

91-97: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle missing close_at to prevent runtime crashes.

If a changeset is submitted with an open_at value but a missing or invalid close_at value, Ecto's get_field(changeset, :close_at) will return nil. Passing nil to DateTime.compare/2 will raise a FunctionClauseError, resulting in a 500 Internal Server Error instead of returning a graceful validation failure.

  • lib/cadet/assessments/assessment.ex#L91-L97: Extract close_at, add a nil-check before DateTime.compare, and invert the condition to safely apply the error.
  • lib/cadet/stories/story.ex#L33-L39: Extract close_at, add a nil-check before DateTime.compare, and invert the condition to safely apply the error.
🛡️ Proposed fix (applies to both files)
     validate_change(changeset, :open_at, fn :open_at, open_at ->
-      if DateTime.compare(open_at, get_field(changeset, :close_at)) == :lt do
-        []
-      else
-        [open_at: "Open date must be before close date"]
-      end
+      close_at = get_field(changeset, :close_at)
+      if close_at && DateTime.compare(open_at, close_at) != :lt do
+        [open_at: "Open date must be before close date"]
+      else
+        []
+      end
     end)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/cadet/assessments/assessment.ex` around lines 91 - 97, Update the open_at
validation in lib/cadet/assessments/assessment.ex lines 91-97 and the
corresponding validation in lib/cadet/stories/story.ex lines 33-39: extract
close_at, check that it is non-nil before calling DateTime.compare/2, and invert
the condition so the existing error is returned when close_at is missing or
open_at is not before it.
lib/cadet_web/admin_controllers/admin_assessments_controller.ex (1)

145-158: 🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Cross-tenant authorization bypass (IDOR) on assessment modifications.

By explicitly mapping the URL's course_id to an unused variable (_course_id), the controller assumes the assessment_id safely belongs to the authenticated context without verifying it. A malicious or curious staff member of one course could supply an assessment_id that belongs to another course, bypassing tenant segregation to trigger unauthorized score calculations or premature XP distributions.

  • lib/cadet_web/admin_controllers/admin_assessments_controller.ex#L145-L158: Bind course_id instead of _course_id and wrap the core logic inside an is_same_course(course_id, assessment_id) check, returning a 403 Forbidden response if the validation fails.
  • lib/cadet_web/admin_controllers/admin_assessments_controller.ex#L160-L174: Bind course_id instead of _course_id and apply the exact same is_same_course(course_id, assessment_id) ownership check before distributing XP.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/cadet_web/admin_controllers/admin_assessments_controller.ex` around lines
145 - 158, Validate assessment ownership before performing either administrative
operation. In lib/cadet_web/admin_controllers/admin_assessments_controller.ex
lines 145-158, bind course_id and wrap calculate_contest_score’s existing logic
in is_same_course(course_id, assessment_id), returning 403 Forbidden when it
fails; apply the identical ownership check and course_id binding in lines
160-174 before distributing XP.
🧹 Nitpick comments (6)
lib/cadet_web/helpers/ai_comments_helpers.ex (1)

60-61: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove unnecessary variable assignment.

Since this is the last expression in the if block, its result is implicitly returned. You can remove the _encrypted = assignment entirely.

♻️ Proposed refactor
-      _encrypted =
-        Base.encode64(iv) <> ":" <> Base.encode64(tag) <> ":" <> Base.encode64(ciphertext)
+      Base.encode64(iv) <> ":" <> Base.encode64(tag) <> ":" <> Base.encode64(ciphertext)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/cadet_web/helpers/ai_comments_helpers.ex` around lines 60 - 61, Remove
the unnecessary _encrypted assignment in the final expression of the if block,
leaving the Base.encode64 concatenation as the block’s implicit return value.
lib/cadet_web/controllers/generate_ai_comments.ex (1)

53-67: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused parameter from the function signature.

Since _llm_api_key is a private function parameter and is completely unused in the function body, consider removing it from the signature and updating its caller, rather than just prefixing it with an underscore.

♻️ Proposed refactor
-  defp check_llm_grading_parameters(_llm_api_key, llm_model, llm_api_url, llm_course_level_prompt) do
+  defp check_llm_grading_parameters(llm_model, llm_api_url, llm_course_level_prompt) do
     cond do

Make sure to also update the caller at line 90:

-         check_llm_grading_parameters(
-           key,
-           course.llm_model,
-           course.llm_api_url,
-           course.llm_course_level_prompt
-         ),
+         check_llm_grading_parameters(
+           course.llm_model,
+           course.llm_api_url,
+           course.llm_course_level_prompt
+         ),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/cadet_web/controllers/generate_ai_comments.ex` around lines 53 - 67,
Remove the unused _llm_api_key argument from check_llm_grading_parameters and
update its caller to pass only llm_model, llm_api_url, and
llm_course_level_prompt. Preserve the existing validation behavior and return
values.
lib/cadet/accounts/teams.ex (1)

119-119: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove unused variable assignment.

Since _unique_ids_count is unused and its computation has no side effects, you can remove this line entirely rather than just prefixing it with an underscore.

♻️ Proposed refactor
-      _unique_ids_count = ids |> Enum.uniq() |> Enum.count()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/cadet/accounts/teams.ex` at line 119, Remove the unused _unique_ids_count
assignment and its Enum.uniq/Enum.count computation from the surrounding
function, leaving the remaining logic unchanged.
test/cadet/updater/xml_parser_test.exs (1)

84-92: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer DateTime.new!/2 over manual struct manipulation.

Manually updating fields of a DateTime struct using Map.put/3 is non-idiomatic and bypasses validation. Since the goal is to get midnight of the current day in UTC, consider using Date.utc_today/0 and Time sigils, which exactly matches the approach used in the production code (XMLParser.process_assessment/3).

  • test/cadet/updater/xml_parser_test.exs#L84-L92: Replace the Map.put pipeline with Date.utc_today() |> DateTime.new!(~T[00:00:00.000000]).
  • test/cadet_web/admin_controllers/admin_assessments_controller_test.exs#L467-L483: Replace the Map.put pipeline on new_open_at.
  • test/cadet_web/admin_controllers/admin_assessments_controller_test.exs#L500-L516: Replace the Map.put pipeline on new_open_at.
  • test/cadet_web/admin_controllers/admin_assessments_controller_test.exs#L532-L564: Replace the Map.put pipeline on open_at.
  • test/cadet_web/admin_controllers/admin_assessments_controller_test.exs#L584-L612: Replace the Map.put pipeline on open_at.
  • test/cadet_web/admin_controllers/admin_assessments_controller_test.exs#L632-L660: Replace the Map.put pipeline on open_at.
  • test/cadet_web/admin_controllers/admin_assessments_controller_test.exs#L680-L708: Replace the Map.put pipeline on open_at.
  • test/cadet_web/admin_controllers/admin_assessments_controller_test.exs#L728-L756: Replace the Map.put pipeline on open_at.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/cadet/updater/xml_parser_test.exs` around lines 84 - 92, Replace the
manual DateTime Map.put pipelines with Date.utc_today() |>
DateTime.new!(~T[00:00:00.000000]) in
test/cadet/updater/xml_parser_test.exs:84-92 and each listed range in
test/cadet_web/admin_controllers/admin_assessments_controller_test.exs (467-483,
500-516, 532-564, 584-612, 632-660, 680-708, 728-756), preserving the subsequent
date arithmetic and using the existing open_at or new_open_at variables.
lib/cadet/jobs/autograder/result_store_worker.ex (1)

24-40: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant public entrypoints: perform/1 (map clause) duplicates run/1.

Same pattern as in lib/cadet/jobs/autograder/lambda_worker.ex: perform(args) when is_map(args) and not is_struct(args) and run/1 do the same thing, giving two public ways to invoke the same logic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/cadet/jobs/autograder/result_store_worker.ex` around lines 24 - 40,
Remove the redundant map-based perform/1 clause in the result-store worker and
keep run/1 as the public direct-call entrypoint. Preserve the Oban.Worker
perform/1 clause for Oban.Job inputs and its delegation to run/1.
lib/cadet/jobs/autograder/lambda_worker.ex (1)

19-45: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant public entrypoints: perform/1 (map clause) duplicates run/1.

perform(args) when is_map(args) and not is_struct(args) and run(args) do exactly the same thing. Consider dropping the map-arg perform/1 clause and having callers/tests use run/1 directly, keeping perform/1 solely as the Oban entrypoint.

Separately, note that this bypass path (map-arg perform/1/run/1) has no rescue/catch, unlike the %Oban.Job{} clause — any future caller invoking it directly outside of Oban won't get the graceful failure-enqueue behavior. This is currently safe because production code always dispatches through Oban.insert/1, but it's worth documenting explicitly rather than relying on the docstring's brief "bypass Oban" note.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/cadet/jobs/autograder/lambda_worker.ex` around lines 19 - 45, Remove the
redundant map-argument perform/1 clause in the autograder worker, leaving
perform/1 solely for %Oban.Job{} dispatch and directing direct callers/tests to
run/1. Update the run/1 documentation to explicitly state that this bypass path
does not apply the rescue/catch failure-enqueue handling used by the Oban
entrypoint.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/cadet_web/endpoint.ex`:
- Around line 5-14: Remove the Code.ensure_loaded?(__MODULE__) conditional from
the cors_origins definition in CadetWeb.Endpoint and read :cors_endpoints
directly with Application.compile_env(:cadet, :cors_endpoints, "*"). Keep the
existing "*" fallback and pass the resulting value to Corsica unchanged.

---

Outside diff comments:
In `@lib/cadet_web/admin_controllers/admin_assessments_controller.ex`:
- Around line 145-158: Validate assessment ownership before performing either
administrative operation. In
lib/cadet_web/admin_controllers/admin_assessments_controller.ex lines 145-158,
bind course_id and wrap calculate_contest_score’s existing logic in
is_same_course(course_id, assessment_id), returning 403 Forbidden when it fails;
apply the identical ownership check and course_id binding in lines 160-174
before distributing XP.

In `@lib/cadet/assessments/assessment.ex`:
- Around line 91-97: Update the open_at validation in
lib/cadet/assessments/assessment.ex lines 91-97 and the corresponding validation
in lib/cadet/stories/story.ex lines 33-39: extract close_at, check that it is
non-nil before calling DateTime.compare/2, and invert the condition so the
existing error is returned when close_at is missing or open_at is not before it.

---

Nitpick comments:
In `@lib/cadet_web/controllers/generate_ai_comments.ex`:
- Around line 53-67: Remove the unused _llm_api_key argument from
check_llm_grading_parameters and update its caller to pass only llm_model,
llm_api_url, and llm_course_level_prompt. Preserve the existing validation
behavior and return values.

In `@lib/cadet_web/helpers/ai_comments_helpers.ex`:
- Around line 60-61: Remove the unnecessary _encrypted assignment in the final
expression of the if block, leaving the Base.encode64 concatenation as the
block’s implicit return value.

In `@lib/cadet/accounts/teams.ex`:
- Line 119: Remove the unused _unique_ids_count assignment and its
Enum.uniq/Enum.count computation from the surrounding function, leaving the
remaining logic unchanged.

In `@lib/cadet/jobs/autograder/lambda_worker.ex`:
- Around line 19-45: Remove the redundant map-argument perform/1 clause in the
autograder worker, leaving perform/1 solely for %Oban.Job{} dispatch and
directing direct callers/tests to run/1. Update the run/1 documentation to
explicitly state that this bypass path does not apply the rescue/catch
failure-enqueue handling used by the Oban entrypoint.

In `@lib/cadet/jobs/autograder/result_store_worker.ex`:
- Around line 24-40: Remove the redundant map-based perform/1 clause in the
result-store worker and keep run/1 as the public direct-call entrypoint.
Preserve the Oban.Worker perform/1 clause for Oban.Job inputs and its delegation
to run/1.

In `@test/cadet/updater/xml_parser_test.exs`:
- Around line 84-92: Replace the manual DateTime Map.put pipelines with
Date.utc_today() |> DateTime.new!(~T[00:00:00.000000]) in
test/cadet/updater/xml_parser_test.exs:84-92 and each listed range in
test/cadet_web/admin_controllers/admin_assessments_controller_test.exs (467-483,
500-516, 532-564, 584-612, 632-660, 680-708, 728-756), preserving the subsequent
date arithmetic and using the existing open_at or new_open_at variables.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cfd6ae81-c62f-4eda-a23f-65447f7a111a

📥 Commits

Reviewing files that changed from the base of the PR and between 5c4bd79 and 2fe5e78.

⛔ Files ignored due to path filters (1)
  • mix.lock is excluded by !**/*.lock
📒 Files selected for processing (50)
  • config/config.exs
  • lib/cadet/accounts/teams.ex
  • lib/cadet/application.ex
  • lib/cadet/assessments/assessment.ex
  • lib/cadet/assessments/assessments.ex
  • lib/cadet/assessments/version_manager.ex
  • lib/cadet/auth/guardian.ex
  • lib/cadet/auth/providers/openid/nus_entra_id_claim_extractor.ex
  • lib/cadet/chatbot/llm_conversations.ex
  • lib/cadet/code_exchange.ex
  • lib/cadet/helpers/model_helper.ex
  • lib/cadet/jobs/autograder/grading_job.ex
  • lib/cadet/jobs/autograder/lambda_worker.ex
  • lib/cadet/jobs/autograder/result_store_worker.ex
  • lib/cadet/jobs/autograder/utilities.ex
  • lib/cadet/jobs/log.ex
  • lib/cadet/jobs/xml_parser.ex
  • lib/cadet/logger/cloudwatch_logger.ex
  • lib/cadet/stories/stories.ex
  • lib/cadet/stories/story.ex
  • lib/cadet_web.ex
  • lib/cadet_web/admin_controllers/admin_assessments_controller.ex
  • lib/cadet_web/admin_controllers/admin_teams_controller.ex
  • lib/cadet_web/admin_views/admin_grading_view.ex
  • lib/cadet_web/controllers/assessments_controller.ex
  • lib/cadet_web/controllers/auth_controller.ex
  • lib/cadet_web/controllers/generate_ai_comments.ex
  • lib/cadet_web/endpoint.ex
  • lib/cadet_web/helpers/ai_comments_helpers.ex
  • lib/cadet_web/helpers/view_helper.ex
  • mix.exs
  • priv/repo/migrations/20230214140555_create_notification_preferences.exs
  • priv/repo/migrations/20260715000000_update_oban_to_v14.exs
  • test/cadet/assessments/assessment_test.exs
  • test/cadet/assessments/assessments_test.exs
  • test/cadet/jobs/autograder/grading_job_test.exs
  • test/cadet/jobs/autograder/lambda_worker_test.exs
  • test/cadet/jobs/autograder/result_store_worker_test.exs
  • test/cadet/jobs/autograder/utilities_test.exs
  • test/cadet/jobs/log_test.exs
  • test/cadet/stories/stories_test.exs
  • test/cadet/updater/xml_parser_test.exs
  • test/cadet_web/admin_controllers/admin_assessments_controller_test.exs
  • test/cadet_web/admin_controllers/admin_grading_controller_test.exs
  • test/cadet_web/admin_controllers/admin_stories_controller_test.exs
  • test/cadet_web/controllers/answer_controller_test.exs
  • test/cadet_web/controllers/assessments_controller_test.exs
  • test/cadet_web/controllers/stories_controller_test.exs
  • test/factories/assessments/assessment_factory.ex
  • test/factories/stories/story_factory.ex
💤 Files with no reviewable changes (2)
  • lib/cadet/chatbot/llm_conversations.ex
  • lib/cadet/assessments/version_manager.ex

Comment thread lib/cadet_web/endpoint.ex Outdated
@RichDom2185

Copy link
Copy Markdown
Member Author

Re: CodeRabbit review — resolutions

Addressing the findings that were posted in the review body (outside the diff range) and the nitpicks, since those don't have inline threads to reply to.

Outside diff range comments

  • lib/cadet/assessments/assessment.ex & lib/cadet/stories/story.ex — missing close_at handling — Resolved in a4a2e28. Both validate_open_close_date/1 functions now skip the comparison when close_at is nil (leaving the missing-field error to validate_required/2) instead of raising a FunctionClauseError in DateTime.compare/2. Note this was pre-existing — the previous Timex.before?/2 code raised on nil too — but the guard is a safe improvement.
  • lib/cadet_web/admin_controllers/admin_assessments_controller.ex — cross-course access on contest endpoints — Resolved in d674be3. calculate_contest_score/2 and dispatch_contest_xp/2 now verify the assessment belongs to the path course_id via the existing is_same_course/2 helper (403 on mismatch), matching delete/2. The shared lookup was extracted into a private helper to avoid code duplication. This was pre-existing (the course_id binding was already unused before this PR), but it's a real gap so it's fixed here.

Nitpick comments

  • ai_comments_helpers.ex — unnecessary _encrypted assignment — Resolved in 7de83ce.
  • generate_ai_comments.ex — unused _llm_api_key parameter — Resolved in 7de83ce (removed the parameter and updated the caller; key is still used afterwards for api_key).
  • teams.ex — unused _unique_ids_count — Resolved in 7de83ce.
  • Test datetime construction (xml_parser_test.exs, admin_assessments_controller_test.exs) — Resolved in 09fe17b (now Date.utc_today() |> DateTime.new!(~T[00:00:00.000000]), matching XMLParser.process_assessment/3).

Intentionally not changed

  • Redundant perform/1 map clause in lambda_worker.ex and result_store_worker.ex — Left as-is. The map-arg perform/1 clause is an intentional backwards-compatible direct-call entry point exercised by the existing test suites (e.g. ResultStoreWorker.perform(%{answer_id: ...}), LambdaWorker.perform(%{question_id: ...})); removing it would require rewriting those call sites to use run/1 for no functional gain. Happy to consolidate in a follow-up if preferred.

All changes verified locally: mix compile --warnings-as-errors (dev + test), mix format --check-formatted, mix credo --strict (no new issues vs. baseline), mix dialyzer (0 errors), the Oban v14 migration runs cleanly, and the affected test suites pass.

@RichDom2185

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/cadet_web/admin_controllers/admin_assessments_controller.ex (1)

189-189: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Avoid parsing nil dates in partial date edits.

openAt / closeAt can be omitted, but DateTime.from_iso8601(nil) raises before check_dates can return a bad request. Parse each supplied date independently, fall back to the persisted date when omitted, and only compare when both values are present.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/cadet_web/admin_controllers/admin_assessments_controller.ex` at line 189,
Update the date handling around check_dates so openAt and closeAt are parsed
independently only when supplied, while omitted values reuse their persisted
dates. Ensure nil values do not reach DateTime.from_iso8601, and perform the
DateTime.compare check only when both dates are present so partial edits can
return the intended bad request.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@lib/cadet_web/admin_controllers/admin_assessments_controller.ex`:
- Line 189: Update the date handling around check_dates so openAt and closeAt
are parsed independently only when supplied, while omitted values reuse their
persisted dates. Ensure nil values do not reach DateTime.from_iso8601, and
perform the DateTime.compare check only when both dates are present so partial
edits can return the intended bad request.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: eb65cc0c-bbf4-4d65-95a0-144cf1152643

📥 Commits

Reviewing files that changed from the base of the PR and between 2fe5e78 and 09fe17b.

📒 Files selected for processing (13)
  • lib/cadet/accounts/teams.ex
  • lib/cadet/assessments/assessment.ex
  • lib/cadet/assessments/assessments.ex
  • lib/cadet/jobs/autograder/lambda_worker.ex
  • lib/cadet/jobs/xml_parser.ex
  • lib/cadet/stories/story.ex
  • lib/cadet_web/admin_controllers/admin_assessments_controller.ex
  • lib/cadet_web/controllers/generate_ai_comments.ex
  • lib/cadet_web/endpoint.ex
  • lib/cadet_web/helpers/ai_comments_helpers.ex
  • test/cadet/jobs/autograder/lambda_worker_test.exs
  • test/cadet/updater/xml_parser_test.exs
  • test/cadet_web/admin_controllers/admin_assessments_controller_test.exs
💤 Files with no reviewable changes (1)
  • lib/cadet/accounts/teams.ex
🚧 Files skipped from review as they are similar to previous changes (11)
  • lib/cadet_web/endpoint.ex
  • lib/cadet_web/helpers/ai_comments_helpers.ex
  • lib/cadet/stories/story.ex
  • lib/cadet/assessments/assessment.ex
  • test/cadet/updater/xml_parser_test.exs
  • lib/cadet_web/controllers/generate_ai_comments.ex
  • lib/cadet/jobs/xml_parser.ex
  • test/cadet_web/admin_controllers/admin_assessments_controller_test.exs
  • test/cadet/jobs/autograder/lambda_worker_test.exs
  • lib/cadet/jobs/autograder/lambda_worker.ex
  • lib/cadet/assessments/assessments.ex

@RichDom2185
RichDom2185 marked this pull request as draft July 30, 2026 13:21
@sayomaki

Copy link
Copy Markdown
Contributor
  1. lib/cadet/application.ex:30 — OpenID worker gets its argument double-wrapped; prod won't boot

providers -> children ++ [{OpenIDConnect.Worker, [providers]}]

Supervisor.Spec.worker(Mod, args) treated the second element as the argument list for apply/3 → start_link(providers). The child-spec tuple {Mod, arg} passes arg as a single argument → start_link([providers]). Upstream (openid_connect 0.2.2, lib/openid_connect/worker.ex):

def start_link(provider_configs, name \\ :openid_connect)

def init(provider_configs) do
  Enum.into(provider_configs, %{}, fn {provider, config} -> ... end)
end

Failure scenario: with provider_configs = [providers], the single element is the keyword list itself, so the {provider, config} clause fails to match → FunctionClauseError in init/1 → the child never starts → the supervisor exhausts restarts → application boot fails.

RichDom2185 added a commit that referenced this pull request Jul 30, 2026
The Supervisor.Spec -> child-spec-tuple migration changed
`worker(OpenIDConnect.Worker, [providers])` into
`{OpenIDConnect.Worker, [providers]}`, which double-wraps the argument.

`worker(mod, [providers])` set the start MFA to
`{mod, :start_link, [providers]}`, i.e. `start_link(providers)`. The
child-spec tuple `{mod, arg}` instead invokes `mod.child_spec(arg)`,
whose default (from `use GenServer`) is `{mod, :start_link, [arg]}` ->
`start_link([providers])`.

OpenIDConnect.Worker.init/1 does
`Enum.into(provider_configs, %{}, fn {provider, config} -> ... end)`.
With `[providers]` the single element is the whole providers keyword
list, so the `{provider, config}` clause raises FunctionClauseError,
the child never starts, and (where openid_connect_providers is
configured, i.e. prod) the supervisor exhausts restarts and boot fails.

Pass `providers` directly so child_spec resolves to
`start_link(providers)`, exactly matching the pre-migration behaviour.

Reported in review: PR #1363.
@RichDom2185

Copy link
Copy Markdown
Member Author

I found three issues in the current PR head (4c5364c):

  • [P1] CORS configuration is read from the wrong path (lib/cadet_web/endpoint.ex:5)

    cors_endpoints is configured beneath CadetWeb.Endpoint, but the new code reads the top-level :cors_endpoints key. It therefore always falls back to "*", including production with allow_credentials: true. This should use:

    Application.compile_env(:cadet, [CadetWeb.Endpoint, :cors_endpoints], "*")
  • [P1] Rolling back the Oban upgrade also removes schema v11 (priv/repo/migrations/20260715000000_update_oban_to_v14.exs:9)

    Oban's down(version: n) rolls back version n inclusively. Consequently, down(version: 11) takes the database from v14 to v10 and drops the oban_peers table introduced by v11. The previous application expects schema v11, so a deployment rollback can break Oban. This should be down(version: 12).

  • [P2] Result storage lost its separate five-worker concurrency limit (lib/cadet/jobs/autograder/result_store_worker.ex:7)

    Both Lambda invocation and result storage now use the same autograder: 20 queue. Large grading batches can put all result jobs behind Lambda jobs and then execute up to 20 database-writing jobs concurrently, contrary to the module's stated five-worker design. Result storage should have a separate queue, such as autograder_results: 5.

The current-head CI passes formatting, Credo, migrations, tests, and Dialyzer.

RichDom2185 added a commit that referenced this pull request Aug 1, 2026
`cors_endpoints` is configured beneath `CadetWeb.Endpoint` (see
`config/cadet.exs.example`), but the endpoint read the top-level
`:cors_endpoints` key, so it always fell back to `"*"`. Combined with
`allow_credentials: true`, Corsica then echoes back whatever `Origin` the
request carries rather than sending a literal `*`:

    send_wildcard_origin?(%Options{origins: origins, allow_credentials: c}) ->
      origins == "*" and not c

so every origin is allowed to make credentialed requests.

Reading `[CadetWeb.Endpoint, :cors_endpoints]` fixes the path, but not
via `Application.compile_env/3`: this app is deployed as a Mix release
and `config/releases.exs` evaluates `/etc/cadet.exs` at boot, so
`cors_endpoints` is absent at build time and present at runtime. Releases
default to `validate_compile_env: true`, which compares the two and
aborts:

    the application :cadet has a different value set for path
    [:cors_endpoints] inside key CadetWeb.Endpoint during runtime
    compared to compile time.

Endpoint plugs are also initialised at compile time
(`Phoenix.plug_init_mode/0` defaults to `:compile`, and this app never
sets it), so the value has to be read outside `init/1` regardless. Move
Corsica behind a small plug that resolves the origins on first call and
caches the parsed options in `:persistent_term`.

Verified by setting `:cors_endpoints` after compilation and driving a
preflight request through the endpoint: the configured origin gets
`access-control-allow-origin`, others get no CORS headers at all; with
nothing configured the previous `"*"` behaviour is unchanged.

Reported in review: PR #1363.
RichDom2185 added a commit that referenced this pull request Aug 1, 2026
`Oban.Migrations.down/1` is inclusive of the version it is given:

    def down(opts) do
      ...
      if initial >= opts.version do
        change(initial..opts.version//-1, :down, opts)
      end
    end

and `change/3` then records `Enum.min(range) - 1` as the new schema
version. So `down(version: 11)` runs V14..V11 down and leaves the
database at v10, dropping the `oban_peers` table that V11 creates.

The preceding migration (20230214081421) migrates up to v11, so rolling
back this migration should land on v11, not v10 -- otherwise a
deployment rollback leaves the previous release running against a schema
missing `oban_peers`.

Confirmed against a live database. With `down(version: 11)` a rollback
gives schema version 10 and `to_regclass('public.oban_peers')` is NULL;
with `down(version: 12)` it gives 11 with `oban_peers` intact.

Reported in review: PR #1363.
RichDom2185 added a commit that referenced this pull request Aug 1, 2026
`ResultStoreWorker` was `use Que.Worker, concurrency: 5` before the move
to Oban, deliberately separate from the grading work so that database
writes stayed bounded -- as its `@moduledoc` still says:

    Separate worker is created with lower concurrency on the assumption
    that autograding time >> db IO time so as to reduce db load.

Putting it on the same `autograder: 20` queue as `LambdaWorker` lost
both halves of that: result jobs now queue behind a batch's Lambda
invocations, and up to 20 of them can then write to the database at
once.

Restore the separation with a dedicated `autograder_results: 5` queue.

Reported in review: PR #1363.
@RichDom2185 RichDom2185 changed the title Upgrade most dependencies Upgrade most dependencies and modernize codebase Aug 1, 2026
@RichDom2185

Copy link
Copy Markdown
Member Author

All three findings are valid — but one of the suggested fixes was wrong and would have broken production.

Verdicts

1. CORS wrong config path (P1) — valid diagnosis, unsafe fix

cors_endpoints does live under CadetWeb.Endpoint (config/cadet.exs.example:10), so the top-level read always fell back to "*". I confirmed this is a real vulnerability, not just cosmetic: Corsica only sends a literal * when allow_credentials is false, so it was echoing back whatever Origin arrived. Driving a preflight through the endpoint showed https://evil.example.com getting allow-origin plus allow-credentials: true.

The suggested Application.compile_env(:cadet, [CadetWeb.Endpoint, :cors_endpoints], "*") fixes the path but aborts release boot. config/releases.exs evaluates /etc/cadet.exs at runtime, so the key is absent at build time and present at boot; releases default to validate_compile_env: true. I reproduced the exact abort:

the application :cadet has a different value set for path [:cors_endpoints] inside key CadetWeb.Endpoint during runtime compared to compile time.

That's the same class of bug as 4c5364c. Since endpoint plugs are also compile-time-initialised (plug_init_mode defaults to :compile and is never set here), the read has to happen in call/2 — so I moved Corsica behind CadetWeb.Plug.CORS, which resolves origins at runtime and caches the parsed options in :persistent_term. Verified: configured origin allowed, others get no CORS headers, and the unconfigured default is unchanged.

2. Oban rollback drops schema v11 (P1) — valid, fixed as suggested

down/1 is inclusive. Confirmed against a live database rather than by reading alone:

schema version oban_peers
down(version: 11) 10 dropped
down(version: 12) 11 intact

3. Result storage lost its concurrency limit (P2) — valid, fixed as suggested

Pre-Oban it was use Que.Worker, concurrency: 5, and the moduledoc still describes that design. Added autograder_results: 5 and moved the worker onto it.

Verification

Everything CI runs, locally: format ✓, credo ✓ (exit 0), migrate + rollback ✓, mix test 1006 passed / 0 failed, dialyzer 0 errors. Pushed as daaced1, 1f78c4e, 16cc7e5 to deps-big-bang-2026; the pre-push hook re-ran the suite green.

One note: your local deps/ was stale (Oban 2.18.0 vs the locked 2.23.0) — I ran mix deps.get to sync, and mix.lock is unchanged. That mattered here, since 2.18 has no v13/v14 migrations at all.

I did not reply on the PR — say the word if you'd like me to post the assessment, particularly the pushback on the compile_env suggestion.

@RichDom2185
RichDom2185 marked this pull request as ready for review August 1, 2026 04:14
Remove the dead `Code.ensure_loaded?(__MODULE__)` guard, which always
evaluated to false during the endpoint's own compilation and forced
`origins` to "*". Read the config directly with compile_env.

Addresses PR review comments.
DateTime.add/4 supports :day and :hour units directly (Elixir >= 1.14),
so drop the manual second multiplications for readability.

Addresses PR review comments.
- teams.ex: drop the unused unique-id count (pure computation, no effect)
- ai_comments_helpers.ex: return the encrypted string directly
- generate_ai_comments.ex: drop the unused api-key parameter and its caller arg

Addresses PR review comments.
handle_failure/4 returned :ok, which made Oban mark the job as completed
even though the Lambda invocation failed. Return {:error, message} so the
job is recorded as discarded (max_attempts: 1) and stays visible in Oban
telemetry. The failed result is still enqueued beforehand, so the answer
is updated as before.

Addresses PR review comments.
calculate_contest_score/2 and dispatch_contest_xp/2 looked up the voting
question by assessment id only, ignoring the course id in the path. A staff
member of one course could therefore trigger score calculation or XP
dispatch on another course's assessment. Guard both with the existing
is_same_course/2 check (returning 403 on mismatch), matching delete/2, and
extract the shared lookup into a helper.

Addresses PR review comments.
get_field(changeset, :close_at) can be nil (e.g. missing on insert), which
made DateTime.compare/2 raise a FunctionClauseError and surface as a 500
instead of a validation error. Skip the comparison when close_at is nil and
let validate_required/2 report the missing field.

Addresses PR review comments.
Build midnight-today with Date.utc_today() |> DateTime.new!/2 instead of
mutating a DateTime via Map.put/3, matching XMLParser.process_assessment/3.

Addresses PR review comments.
The Timex -> DateTime migration expressed date offsets as magic-number
second arithmetic (e.g. `N * 86_400, :second`), while a few call sites
already used the clearer `:day`/`:hour` units that `DateTime.add/4`
supports. Normalize the rest to match, restoring the units the original
Timex code expressed (days:/hours:/from_minutes):

  * `N * 86_400, :second`    -> `N, :day`
  * `N * 3_600, :second`     -> `N, :hour`
  * `-25 * 60 * 60, :second` -> `-25, :hour`
  * `-86_400, :second`       -> `-1, :day`
  * `-60, :second`           -> `-1, :minute`

`DateTime.add/4` treats these units as fixed multiples (86400/3600/60
seconds), so the values are exactly equivalent. Genuinely second-valued
call sites (period_seconds, code_ttl, truncate) are left unchanged.
The Supervisor.Spec -> child-spec-tuple migration changed
`worker(OpenIDConnect.Worker, [providers])` into
`{OpenIDConnect.Worker, [providers]}`, which double-wraps the argument.

`worker(mod, [providers])` set the start MFA to
`{mod, :start_link, [providers]}`, i.e. `start_link(providers)`. The
child-spec tuple `{mod, arg}` instead invokes `mod.child_spec(arg)`,
whose default (from `use GenServer`) is `{mod, :start_link, [arg]}` ->
`start_link([providers])`.

OpenIDConnect.Worker.init/1 does
`Enum.into(provider_configs, %{}, fn {provider, config} -> ... end)`.
With `[providers]` the single element is the whole providers keyword
list, so the `{provider, config}` clause raises FunctionClauseError,
the child never starts, and (where openid_connect_providers is
configured, i.e. prod) the supervisor exhausts restarts and boot fails.

Pass `providers` directly so child_spec resolves to
`start_link(providers)`, exactly matching the pre-migration behaviour.

Reported in review: PR #1363.
`cors_endpoints` is configured beneath `CadetWeb.Endpoint` (see
`config/cadet.exs.example`), but the endpoint read the top-level
`:cors_endpoints` key, so it always fell back to `"*"`. Combined with
`allow_credentials: true`, Corsica then echoes back whatever `Origin` the
request carries rather than sending a literal `*`:

    send_wildcard_origin?(%Options{origins: origins, allow_credentials: c}) ->
      origins == "*" and not c

so every origin is allowed to make credentialed requests.

Reading `[CadetWeb.Endpoint, :cors_endpoints]` fixes the path, but not
via `Application.compile_env/3`: this app is deployed as a Mix release
and `config/releases.exs` evaluates `/etc/cadet.exs` at boot, so
`cors_endpoints` is absent at build time and present at runtime. Releases
default to `validate_compile_env: true`, which compares the two and
aborts:

    the application :cadet has a different value set for path
    [:cors_endpoints] inside key CadetWeb.Endpoint during runtime
    compared to compile time.

Endpoint plugs are also initialised at compile time
(`Phoenix.plug_init_mode/0` defaults to `:compile`, and this app never
sets it), so the value has to be read outside `init/1` regardless. Move
Corsica behind a small plug that resolves the origins on first call and
caches the parsed options in `:persistent_term`.

Verified by setting `:cors_endpoints` after compilation and driving a
preflight request through the endpoint: the configured origin gets
`access-control-allow-origin`, others get no CORS headers at all; with
nothing configured the previous `"*"` behaviour is unchanged.

Reported in review: PR #1363.
`Oban.Migrations.down/1` is inclusive of the version it is given:

    def down(opts) do
      ...
      if initial >= opts.version do
        change(initial..opts.version//-1, :down, opts)
      end
    end

and `change/3` then records `Enum.min(range) - 1` as the new schema
version. So `down(version: 11)` runs V14..V11 down and leaves the
database at v10, dropping the `oban_peers` table that V11 creates.

The preceding migration (20230214081421) migrates up to v11, so rolling
back this migration should land on v11, not v10 -- otherwise a
deployment rollback leaves the previous release running against a schema
missing `oban_peers`.

Confirmed against a live database. With `down(version: 11)` a rollback
gives schema version 10 and `to_regclass('public.oban_peers')` is NULL;
with `down(version: 12)` it gives 11 with `oban_peers` intact.

Reported in review: PR #1363.
`ResultStoreWorker` was `use Que.Worker, concurrency: 5` before the move
to Oban, deliberately separate from the grading work so that database
writes stayed bounded -- as its `@moduledoc` still says:

    Separate worker is created with lower concurrency on the assumption
    that autograding time >> db IO time so as to reduce db load.

Putting it on the same `autograder: 20` queue as `LambdaWorker` lost
both halves of that: result jobs now queue behind a batch's Lambda
invocations, and up to 20 of them can then write to the database at
once.

Restore the separation with a dedicated `autograder_results: 5` queue.

Reported in review: PR #1363.
@RichDom2185
RichDom2185 requested a review from sayomaki August 1, 2026 04:19

@sayomaki sayomaki left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM, although claude made a note on lib/cadet/jobs/autograder/lambda_worker.ex:30 where manual invocations of run will not handle errors gracefully and report them and fail silently. This is not an issue for prod as it will always call the right function.

@RichDom2185
RichDom2185 merged commit ad975fd into master Aug 1, 2026
3 checks passed
@RichDom2185
RichDom2185 deleted the deps-big-bang-2026 branch August 1, 2026 04:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants