Skip to content

feat(admin): dedicated admin shell + nightly AI content review pipeline#1335

Merged
NiallJoeMaher merged 4 commits into
developfrom
feat/admin-shell-ai-content
Jun 14, 2026
Merged

feat(admin): dedicated admin shell + nightly AI content review pipeline#1335
NiallJoeMaher merged 4 commits into
developfrom
feat/admin-shell-ai-content

Conversation

@NiallJoeMaher

Copy link
Copy Markdown
Contributor

What & why

The admin dashboard was crushed: admin pages lived under app/(app)/admin/*, so they inherited the public 3-column AppShell and got squeezed into the feed's narrow center column. This PR gives admin its own home and lays the foundation for AI-assisted content management.

Design doc: docs/plans/2026-06-14-admin-shell-and-ai-content-design.md

Phase 1 — Admin shell (own route group)

  • New app/(admin)/ sibling route group with components/Admin/AdminShell.tsx: full-width cockpit, persistent sidebar nav, slim top bar. Outside the public rails entirely.
  • ADMIN-role gate enforced once in app/(admin)/layout.tsx; per-page gates removed.
  • URLs unchanged (/admin/*) — route groups don't affect paths.
  • Convention established: a page that doesn't want the rail shell doesn't live in (app) — it gets a sibling group. (The BARE_ROUTES hack is noted for later cleanup.)

Phase 2 — Nightly AI content review

Schema (migration 0038, additive + idempotent topic seed):

  • post_metadata (1:1): sentiment, sentimentScore, qualityScore, modelId, analyzedAt watermark, schemaVersion.
  • topic / post_topic: controlled vocabulary + provenance-aware edges (ai | manual). The cron only ever rewrites its own ai edges — manual tags are authoritative.
  • comments.moderatedAt watermark; reports.source (user | system) + nullable reporter so AI flags land in the existing moderation queue.

server/lib/contentAnalysis.ts: one Bedrock call per post → topics + sentiment + quality + moderation verdict. Gated and fail-open like autoReview; heuristic fallback when Bedrock is off. Unit-tested (7 cases).

app/api/cron/daily-review: incremental (per-row watermark — never re-tags unchanged content), capped per run (100 posts / 200 comments), each item isolated (try/catch + Sentry). Four passes:

  1. topic + sentiment tagging
  2. quality / spam scoring
  3. re-screen moderation (posts and comments) → reports queue
  4. daily digest → emails the founder only when something needs attention

Auth via CRON_SECRET. Wired into CDK as a new dailyReview invoker Lambda + daily EventBridge rule (cron-stack.ts), mirroring promoteScheduled.

Verification

  • tsc (source): 0 errors · eslint: clean · vitest: 7/7 pass · next build: succeeds, /api/cron/daily-review + all /admin/* routes present.
  • CDK already deployed to Dev (802956189746) and Prod (689829343490): DailyReviewRule = cron(0 6 * * ? *), ENABLED. Diffs were purely additive.

Post-merge / ops notes

  • Migration 0038 applies on the prod deploy via vercel-build's db:migrate.
  • The EventBridge rules are already live; the nightly route goes active once this merges and the app deploys. Tagging/quality passes require BEDROCK_MODEL_ID + creds — without them the cron gracefully does moderation-only.
  • ANALYSIS_SCHEMA_VERSION bump forces re-analysis of all posts.

Not in this PR (Phase 3, designed only)

Personalized feed ranking (explicit follow/mute, then implicit affinity) and the admin Content/Insights views (shown as "Soon" in the sidebar).

🤖 Generated with Claude Code

NiallJoeMaher and others added 3 commits June 14, 2026 20:41
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Admin pages lived under (app)/ and inherited the public 3-column AppShell,
crushing management views into the feed's narrow center column. Move them to
a new (admin) sibling route group with its own AdminShell — a full-width
cockpit with a persistent sidebar nav, slim top bar, and roomy content area.

- New app/(admin)/layout.tsx enforces the ADMIN-role gate once for the whole
  section; per-page gates removed.
- New components/Admin/AdminShell.tsx (sidebar + topbar + fluid content),
  reusing existing design tokens. Content/Insights shown as 'Soon' roadmap.
- URLs unchanged (/admin/*) — route groups don't affect paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the AI content pipeline backing the admin cockpit:

Schema (migration 0038, additive + idempotent topic seed):
- post_metadata (1:1): sentiment, sentimentScore, qualityScore, modelId,
  analyzedAt watermark, schemaVersion.
- topic / post_topic: controlled vocab + provenance-aware edges (ai|manual);
  the cron only ever rewrites its own 'ai' edges.
- comments.moderatedAt watermark; reports.source (user|system) + nullable
  reporter so AI flags share the existing moderation queue.

server/lib/contentAnalysis.ts: one Bedrock call per post returns topics +
sentiment + quality + a moderation verdict. Gated + fail-open like autoReview;
heuristic fallback when Bedrock is off. Unit-tested (7 cases).

app/api/cron/daily-review: incremental, capped, per-item-isolated cron doing
four passes (tag/sentiment, quality, re-screen posts+comments, digest email).
Auth via CRON_SECRET. Wired into CDK via a new dailyReview invoker Lambda +
daily EventBridge rule (cron-stack.ts), mirroring promoteScheduled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@NiallJoeMaher NiallJoeMaher requested a review from a team as a code owner June 14, 2026 19:42
@vercel

vercel Bot commented Jun 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
codu Ready Ready Preview, Comment Jun 14, 2026 7:49pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (2)
  • drizzle/meta/0038_snapshot.json is excluded by !**/*.json
  • drizzle/meta/_journal.json is excluded by !**/*.json

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b0592b98-fe09-4ba2-8465-1c925d26721e

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

Admin pages are moved from the (app) route group into a new (admin) route group with a shared AdminLayout that centralizes the admin-role auth gate. A new AdminShell client component provides the full-width admin UI chrome. Separately, a new AI content pipeline is introduced: new DB tables and enums for post metadata, topic vocabulary, and topic edges; a Bedrock-backed analyzePost library with heuristic fallback; a nightly cron HTTP route (/api/cron/daily-review) for incremental post/comment review and digest emails; and an AWS Lambda + EventBridge schedule to invoke it daily.

Changes

Admin Shell & Route Restructuring

Layer / File(s) Summary
AdminShell component and navigation
components/Admin/AdminShell.tsx
Adds a client-side admin layout with desktop sidebar, sticky top bar, mobile nav, NavLink, MobileNavLink, and Avatar sub-components. Active route is derived from usePathname; soon items render as disabled pills.
Centralized admin auth layout
app/(admin)/layout.tsx, docs/plans/2026-06-14-admin-shell-and-ai-content-design.md
Creates AdminLayout that fetches the server session, redirects non-admins to /, and renders AdminShell with the user's identity for all child routes. No-index metadata is exported.
Admin page migration
app/(admin)/admin/page.tsx, app/(admin)/admin/sources/page.tsx, app/(admin)/admin/tags/page.tsx, app/(admin)/admin/users/page.tsx, app/(admin)/admin/moderation/page.tsx, app/(admin)/admin/_client.tsx, app/(app)/admin/page.tsx, app/(app)/admin/sources/page.tsx, app/(app)/admin/tags/page.tsx, app/(app)/admin/users/page.tsx
Adds thin synchronous page modules under (admin)/admin/ (metadata + client component render, no per-page auth). Removes the equivalent (app)/admin/ pages that previously contained inline session fetches and redirects. Dashboard wrapper layout classes are simplified.

AI Content Pipeline

Layer / File(s) Summary
DB schema: AI metadata, topics, and moderation extensions
drizzle/0038_ai_content_metadata.sql, server/db/schema.ts
Adds enums (reportSource, sentiment, topicStatus, tagSource), creates post_metadata (1:1 per post), topic (controlled vocabulary), and post_topic (edge table with confidence/source). Makes reports.reporterId nullable, adds reports.source, adds comments.moderatedAt, seeds initial topic vocabulary, and extends postsRelations.
Content analysis library
server/lib/contentAnalysis.ts, server/lib/contentAnalysis.test.ts
Adds analyzePost() (Bedrock invocation or heuristic fallback), parseAnalysis() (defensive JSON extraction, vocab filtering, score clamping, verdict normalization), and supporting interfaces/constants. Tests cover vocab filtering, score clamping, JSON-in-prose extraction, fail-open on unparseable input, and Bedrock-disabled path.
Nightly cron HTTP route
app/api/cron/daily-review/route.ts, docs/plans/2026-06-14-admin-shell-and-ai-content-design.md
Adds GET/POST route with Bearer auth, loadVocab, reviewPosts (incremental staleness-watermark worklist, post_metadata upsert, post_topic AI edge rewrite, system report escalation), reviewComments (autoReview + moderatedAt update + system reports), and sendDigest (conditional HTML email).
Lambda invoker and CDK schedule
cdk/lambdas/dailyReview/index.ts, cdk/lib/cron-stack.ts
Lambda reads SSM for cron secret and site URL, POSTs to the cron route, and fails loudly on non-OK responses. CronStack adds DailyReviewLambda and an EventBridge rule at 06:00 UTC.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(100, 149, 237, 0.5)
    note over EventBridge,CronRoute: Nightly AI content review
    EventBridge->>DailyReviewLambda: trigger at 06:00 UTC
    DailyReviewLambda->>SSM: getSsmValue(/env/cronSecret, /env/siteUrl)
    SSM-->>DailyReviewLambda: secret, siteUrl
    DailyReviewLambda->>CronRoute: POST /api/cron/daily-review (Bearer token)
  end
  rect rgba(144, 238, 144, 0.5)
    note over CronRoute,DB: Post & comment review passes
    CronRoute->>DB: loadVocab (active topics)
    CronRoute->>DB: reviewPosts worklist (analyzedAt watermark)
    CronRoute->>Bedrock: analyzePost (or heuristic fallback)
    Bedrock-->>CronRoute: ContentAnalysis (topics, sentiment, moderation)
    CronRoute->>DB: upsert post_metadata, rewrite post_topic ai edges
    CronRoute->>DB: raiseSystemReport if verdict != allow
    CronRoute->>DB: reviewComments (moderatedAt watermark)
    CronRoute->>DB: update comments.moderatedAt, raise system reports
  end
  rect rgba(255, 165, 0, 0.5)
    note over CronRoute,Email: Digest
    CronRoute->>DB: count new users/posts/comments/pending reports
    CronRoute->>Email: send HTML digest (only when flags or pending exist)
  end
  CronRoute-->>DailyReviewLambda: JSON {ok, counts, digestSent}
Loading
sequenceDiagram
  participant Browser
  participant AdminLayout
  participant AdminShell
  participant AdminPage
  Browser->>AdminLayout: GET /admin/*
  AdminLayout->>AdminLayout: getServerAuthSession()
  alt session.user.role != ADMIN
    AdminLayout-->>Browser: redirect to /
  else authorized
    AdminLayout->>AdminShell: children + user (name, image)
    AdminShell->>AdminPage: render children in main content area
    AdminPage-->>Browser: full-width admin UI
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 Hoppity-hop through the admin gate,
One layout to rule them all — isn't that great?
Bedrock and heuristics, a pipeline so bright,
Topics and digests sent deep in the night.
The rabbit reviews posts while humans still sleep,
With fail-open fallbacks and watermarks deep! 🌙

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.72% 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
Title check ✅ Passed The title 'feat(admin): dedicated admin shell + nightly AI content review pipeline' accurately and concisely summarizes the main changes: a new admin shell and AI content review system.
Description check ✅ Passed The description follows the template structure, includes detailed context on both phases, references the design doc, lists verification steps, and notes post-merge operations; all critical sections are complete.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/admin-shell-ai-content

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 and usage tips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🧹 Nitpick comments (4)
server/lib/contentAnalysis.ts (1)

107-145: ⚡ Quick win

Consider isolating fetchPageText errors to avoid unnecessary heuristic fallbacks.

If fetchPageText(externalUrl) throws (e.g., network timeout, invalid URL), the entire analyzePost call falls back to heuristic analysis, even though the post's local content (title/body) could still be analyzed by Bedrock. This may cause unnecessary degradation for posts with link-fetching issues.

-    const pageText = externalUrl ? await fetchPageText(externalUrl) : "";
+    let pageText = "";
+    if (externalUrl) {
+      try {
+        pageText = await fetchPageText(externalUrl);
+      } catch {
+        // Link fetch failed; proceed with local content only.
+      }
+    }
🤖 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 `@server/lib/contentAnalysis.ts` around lines 107 - 145, The fetchPageText call
within the try block can throw errors (network timeout, invalid URL, etc.),
causing the entire analyzePost function to fall back to heuristic analysis even
though the post's local content (title, body) could still be analyzed by
Bedrock. Wrap the fetchPageText call in its own try-catch block so that if it
fails, pageText falls back to an empty string and the analysis continues with
the available local content instead of abandoning the entire Bedrock analysis.
server/db/schema.ts (1)

449-450: 💤 Low value

Verify the implicit one() relation mapping for post_metadata.

The metadata: one(post_metadata) relation lacks explicit fields and references configuration. Drizzle infers the relation from the FK on post_metadata.postId, but the inference direction matters: since the FK is on post_metadata pointing to posts, this should work correctly for the postspost_metadata direction. However, explicit mapping is safer for clarity and avoids surprises if table structures evolve.

-  metadata: one(post_metadata),
+  metadata: one(post_metadata, {
+    fields: [posts.id],
+    references: [post_metadata.postId],
+  }),
🤖 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 `@server/db/schema.ts` around lines 449 - 450, The metadata relation in the
posts schema uses implicit relation mapping without explicit fields and
references configuration. While Drizzle can infer the relation from the foreign
key on post_metadata.postId, add explicit fields and references properties to
the metadata: one(post_metadata) relation for better clarity and
maintainability. Specify which field in posts maps to which field in
post_metadata to make the relation dependency explicit and prevent unexpected
behavior if the schema structure changes.
app/api/cron/daily-review/route.ts (1)

96-98: ⚡ Quick win

Guard against undefined commentId when postId is also undefined.

If raiseSystemReport is called without either postId or commentId, the as string cast on line 98 would produce an invalid query condition. Consider adding a runtime guard or making the function signature require at least one.

 async function raiseSystemReport(opts: {
   postId?: string;
   commentId?: string;
   category: string;
   reason: string;
 }): Promise<boolean> {
+  if (!opts.postId && !opts.commentId) {
+    throw new Error("raiseSystemReport requires postId or commentId");
+  }
   const target = opts.postId
🤖 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 `@app/api/cron/daily-review/route.ts` around lines 96 - 98, The target variable
assignment lacks validation to ensure at least one of postId or commentId is
provided. Add a runtime guard before the ternary assignment to verify that if
postId is undefined, commentId must be defined as a non-empty string. This
prevents the as string cast from producing invalid undefined values that would
create malformed query conditions. Consider throwing an error or returning early
if neither option is provided, ensuring the query condition is always valid.
server/lib/contentAnalysis.test.ts (1)

89-109: ⚡ Quick win

Restore environment variables after test to prevent test pollution.

Deleting process.env.BEDROCK_MODEL_ID and process.env.ACCESS_KEY without cleanup could affect subsequent tests in the same process. Use beforeEach/afterEach or store and restore the original values.

 describe("analyzePost (disabled path)", () => {
+  const originalModelId = process.env.BEDROCK_MODEL_ID;
+  const originalAccessKey = process.env.ACCESS_KEY;
+
+  afterEach(() => {
+    if (originalModelId !== undefined) process.env.BEDROCK_MODEL_ID = originalModelId;
+    if (originalAccessKey !== undefined) process.env.ACCESS_KEY = originalAccessKey;
+  });
+
   it("returns heuristic-only analysis with no AI signals when Bedrock is disabled", async () => {
-    // Under Vitest isBedrockEnabled() is forced false (NODE_ENV==="test").
     delete process.env.BEDROCK_MODEL_ID;
     delete process.env.ACCESS_KEY;
🤖 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 `@server/lib/contentAnalysis.test.ts` around lines 89 - 109, The test "returns
heuristic-only analysis with no AI signals when Bedrock is disabled" within the
"analyzePost (disabled path)" describe block deletes
process.env.BEDROCK_MODEL_ID and process.env.ACCESS_KEY without restoring them,
which causes test pollution for subsequent tests. Store the original values of
these environment variables before deleting them, then restore them after the
test completes. You can accomplish this either by wrapping the deletion and
restoration within the test itself, or by using an afterEach hook that restores
the saved values after each test in this describe block.
🤖 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 `@cdk/lib/cron-stack.ts`:
- Around line 120-132: The timeout configuration for the DailyReviewLambda
construct in cdk/lib/cron-stack.ts (lines 120-132) is set to 60 seconds, which
does not match the route's maxDuration requirement of 300 seconds. Increase the
timeout property in the NodejsFunction configuration from 60 seconds to 300
seconds to allow the Lambda sufficient time to complete its processing. The
fetch call in cdk/lambdas/dailyReview/index.ts (lines 49-54) requires no direct
code change once the CDK timeout is increased to 300 seconds, as it will have
sufficient time to complete.

In `@components/Admin/AdminShell.tsx`:
- Around line 61-63: The isActive function uses startsWith to match active
routes, which over-matches prefix patterns (e.g., `/admin/users` would match
`/admin/users-archive`). Replace the startsWith check with exact-or-child
segment matching by verifying that either the pathname exactly matches the href
OR the pathname starts with the href followed by a forward slash character. This
ensures route matching respects path segment boundaries in the isActive arrow
function.

In `@docs/plans/2026-06-14-admin-shell-and-ai-content-design.md`:
- Around line 37-44: The markdown file has multiple fenced code blocks that are
missing language tags, which causes markdown lint (MD040 rule) violations. Add
appropriate language identifiers to each code block fence across the document at
the specified locations: Lines 37-44, 64-73, 105-114, 124-133, 196-199, and
225-230. For each code block that starts with three backticks, append a language
identifier (such as "text", "bash", or the appropriate language for the content)
immediately after the opening backticks to satisfy the markdown linting
requirements.

---

Nitpick comments:
In `@app/api/cron/daily-review/route.ts`:
- Around line 96-98: The target variable assignment lacks validation to ensure
at least one of postId or commentId is provided. Add a runtime guard before the
ternary assignment to verify that if postId is undefined, commentId must be
defined as a non-empty string. This prevents the as string cast from producing
invalid undefined values that would create malformed query conditions. Consider
throwing an error or returning early if neither option is provided, ensuring the
query condition is always valid.

In `@server/db/schema.ts`:
- Around line 449-450: The metadata relation in the posts schema uses implicit
relation mapping without explicit fields and references configuration. While
Drizzle can infer the relation from the foreign key on post_metadata.postId, add
explicit fields and references properties to the metadata: one(post_metadata)
relation for better clarity and maintainability. Specify which field in posts
maps to which field in post_metadata to make the relation dependency explicit
and prevent unexpected behavior if the schema structure changes.

In `@server/lib/contentAnalysis.test.ts`:
- Around line 89-109: The test "returns heuristic-only analysis with no AI
signals when Bedrock is disabled" within the "analyzePost (disabled path)"
describe block deletes process.env.BEDROCK_MODEL_ID and process.env.ACCESS_KEY
without restoring them, which causes test pollution for subsequent tests. Store
the original values of these environment variables before deleting them, then
restore them after the test completes. You can accomplish this either by
wrapping the deletion and restoration within the test itself, or by using an
afterEach hook that restores the saved values after each test in this describe
block.

In `@server/lib/contentAnalysis.ts`:
- Around line 107-145: The fetchPageText call within the try block can throw
errors (network timeout, invalid URL, etc.), causing the entire analyzePost
function to fall back to heuristic analysis even though the post's local content
(title, body) could still be analyzed by Bedrock. Wrap the fetchPageText call in
its own try-catch block so that if it fails, pageText falls back to an empty
string and the analysis continues with the available local content instead of
abandoning the entire Bedrock analysis.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3d0b49f4-3344-424d-8b63-a430aabda7a2

📥 Commits

Reviewing files that changed from the base of the PR and between 0893dcd and d4e1caf.

⛔ Files ignored due to path filters (4)
  • cdk/lambdas/dailyReview/package-lock.json is excluded by !**/package-lock.json, !**/*.json
  • cdk/lambdas/dailyReview/package.json is excluded by !**/*.json
  • drizzle/meta/0038_snapshot.json is excluded by !**/*.json
  • drizzle/meta/_journal.json is excluded by !**/*.json
📒 Files selected for processing (24)
  • app/(admin)/admin/_client.tsx
  • app/(admin)/admin/moderation/_client.tsx
  • app/(admin)/admin/moderation/page.tsx
  • app/(admin)/admin/page.tsx
  • app/(admin)/admin/sources/_client.tsx
  • app/(admin)/admin/sources/page.tsx
  • app/(admin)/admin/tags/_client.tsx
  • app/(admin)/admin/tags/page.tsx
  • app/(admin)/admin/users/_client.tsx
  • app/(admin)/admin/users/page.tsx
  • app/(admin)/layout.tsx
  • app/(app)/admin/page.tsx
  • app/(app)/admin/sources/page.tsx
  • app/(app)/admin/tags/page.tsx
  • app/(app)/admin/users/page.tsx
  • app/api/cron/daily-review/route.ts
  • cdk/lambdas/dailyReview/index.ts
  • cdk/lib/cron-stack.ts
  • components/Admin/AdminShell.tsx
  • docs/plans/2026-06-14-admin-shell-and-ai-content-design.md
  • drizzle/0038_ai_content_metadata.sql
  • server/db/schema.ts
  • server/lib/contentAnalysis.test.ts
  • server/lib/contentAnalysis.ts
💤 Files with no reviewable changes (4)
  • app/(app)/admin/sources/page.tsx
  • app/(app)/admin/tags/page.tsx
  • app/(app)/admin/users/page.tsx
  • app/(app)/admin/page.tsx

Comment thread cdk/lib/cron-stack.ts
Comment on lines +120 to +132
const dailyReviewFn = new NodejsFunction(this, "DailyReviewLambda", {
timeout: cdk.Duration.seconds(60),
runtime: lambda.Runtime.NODEJS_20_X,
entry: path.join(__dirname, "/../lambdas/dailyReview/index.ts"),
depsLockFilePath: path.join(
__dirname,
"/../lambdas/dailyReview/package-lock.json",
),
role: lambdaRole,
bundling: {
nodeModules: ["@aws-sdk/client-ssm"],
},
});

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Lambda timeout (60s) is insufficient for the route's maxDuration (300s).

The daily-review route can take up to 5 minutes to process 100 posts and 200 comments (with potential Bedrock API calls for each). The invoker Lambda's 60-second timeout will cause premature failure even when the route is still processing successfully.

  • cdk/lib/cron-stack.ts#L120-L132: Increase the DailyReviewLambda timeout to 300 seconds to match the route's maxDuration.
  • cdk/lambdas/dailyReview/index.ts#L49-L54: The fetch call will now have sufficient time to complete; no code change needed if CDK timeout is increased.
📍 Affects 2 files
  • cdk/lib/cron-stack.ts#L120-L132 (this comment)
  • cdk/lambdas/dailyReview/index.ts#L49-L54
🤖 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 `@cdk/lib/cron-stack.ts` around lines 120 - 132, The timeout configuration for
the DailyReviewLambda construct in cdk/lib/cron-stack.ts (lines 120-132) is set
to 60 seconds, which does not match the route's maxDuration requirement of 300
seconds. Increase the timeout property in the NodejsFunction configuration from
60 seconds to 300 seconds to allow the Lambda sufficient time to complete its
processing. The fetch call in cdk/lambdas/dailyReview/index.ts (lines 49-54)
requires no direct code change once the CDK timeout is increased to 300 seconds,
as it will have sufficient time to complete.

Comment on lines +61 to +63
const isActive = (href: string) =>
href === "/admin" ? pathname === "/admin" : pathname?.startsWith(href);

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Tighten active-route matching to path boundaries.

Line 62 can over-match route prefixes (for example, /admin/users-archive would highlight /admin/users). Use exact-or-child segment matching instead.

Proposed fix
-  const isActive = (href: string) =>
-    href === "/admin" ? pathname === "/admin" : pathname?.startsWith(href);
+  const isActive = (href: string) =>
+    href === "/admin"
+      ? pathname === "/admin"
+      : pathname === href || pathname?.startsWith(`${href}/`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const isActive = (href: string) =>
href === "/admin" ? pathname === "/admin" : pathname?.startsWith(href);
const isActive = (href: string) =>
href === "/admin"
? pathname === "/admin"
: pathname === href || pathname?.startsWith(`${href}/`);
🤖 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 `@components/Admin/AdminShell.tsx` around lines 61 - 63, The isActive function
uses startsWith to match active routes, which over-matches prefix patterns
(e.g., `/admin/users` would match `/admin/users-archive`). Replace the
startsWith check with exact-or-child segment matching by verifying that either
the pathname exactly matches the href OR the pathname starts with the href
followed by a forward slash character. This ensures route matching respects path
segment boundaries in the isActive arrow function.

Comment on lines +37 to +44
```
app/
(app)/ ← the rail shell (LeftRail / center / RightRail). The social surface.
(auth)/ ← auth chrome
(editor)/ ← editor chrome
(marketing)/ ← marketing chrome
(admin)/ ← NEW: AdminShell. Private management. Full width, own nav.
```

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add fence languages for Markdown code blocks (MD040).

Static analysis flags missing fenced-code languages at Lines 37, 64, 105, 124, 196, and 225. Add a language tag (e.g., text) to each block to keep markdown lint clean.

Also applies to: 64-73, 105-114, 124-133, 196-199, 225-230

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 37-37: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@docs/plans/2026-06-14-admin-shell-and-ai-content-design.md` around lines 37 -
44, The markdown file has multiple fenced code blocks that are missing language
tags, which causes markdown lint (MD040 rule) violations. Add appropriate
language identifiers to each code block fence across the document at the
specified locations: Lines 37-44, 64-73, 105-114, 124-133, 196-199, and 225-230.
For each code block that starts with three backticks, append a language
identifier (such as "text", "bash", or the appropriate language for the content)
immediately after the opening backticks to satisfy the markdown linting
requirements.

Source: Linters/SAST tools

@NiallJoeMaher NiallJoeMaher merged commit 5bdff59 into develop Jun 14, 2026
4 of 6 checks passed
@NiallJoeMaher NiallJoeMaher deleted the feat/admin-shell-ai-content branch June 14, 2026 20:14
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.

1 participant