Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
f49121d
Merge pull request #2312 from topcoder-platform/HOTFIX_add-updatedDat…
vas3a Sep 16, 2026
3d15a16
Merge pull request #2323 from topcoder-platform/dev
jmgasper Sep 18, 2026
9b50319
Merge pull request #2325 from topcoder-platform/dev
jmgasper Sep 18, 2026
2550fee
Add button for admins to rebuild recissions
vas3a Sep 20, 2026
9322839
Review feedback
vas3a Sep 20, 2026
d34df5d
lint
vas3a Sep 20, 2026
6174141
lint
vas3a Sep 20, 2026
63fae37
Show timeout status
vas3a Sep 21, 2026
37c2990
fix token render
vas3a Sep 21, 2026
52b6699
Merge pull request #2361 from topcoder-platform/HOTFIX_admin-retrigge…
vas3a Sep 21, 2026
0e5e850
PM-6392 Redesign the Sales dashboard for an executive view
jmgasper Sep 20, 2026
f28a9f2
Merge pull request #2367 from topcoder-platform/PM-6392-prod
jmgasper Sep 22, 2026
3811734
Stop automatic refresh on Sales page
jmgasper Sep 22, 2026
93a3d25
Merge pull request #2372 from topcoder-platform/hotfix-sf-rate-limit
jmgasper Sep 22, 2026
8d98416
Merge pull request #2397 from topcoder-platform/dev
jmgasper Sep 24, 2026
8dd9ebb
Enable production analytics tracking and reporting endpoint (#2410)
jmgasper Sep 28, 2026
dee1dd2
Refresh analytics report results every minute
jmgasper Sep 29, 2026
558aa4d
Merge pull request #2412 from topcoder-platform/codex/analytics-hourl…
jmgasper Sep 29, 2026
0752593
Fix production project asset uploads using the development bucket
jmgasper Oct 1, 2026
c4f21f9
Merge pull request #2425 from topcoder-platform/fix/work-project-asse…
jmgasper Oct 1, 2026
c920be8
October 2026 Release 1 (#2436)
jmgasper Oct 5, 2026
dc9a32b
Merge pull request #2439 from topcoder-platform/dev
jmgasper Oct 6, 2026
d4f7cb2
Merge pull request #2496 from topcoder-platform/dev
jmgasper Oct 15, 2026
b5acf63
fix(ci): increase production build memory limits
jmgasper Oct 15, 2026
c436bc1
Merge pull request #2497 from topcoder-platform/fix/prod-build-heap-m…
jmgasper Oct 15, 2026
7906794
Merge pull request #2507 from topcoder-platform/dev
jmgasper Oct 15, 2026
c718977
fix(opportunities): allow 10000-character forum comments
jmgasper Oct 16, 2026
dcbc61d
Merge pull request #2510 from topcoder-platform/fix/forum-post-length…
jmgasper Oct 16, 2026
6166fa2
Fix Topgear URL submissions from My Submissions
jmgasper Oct 16, 2026
d02757c
Merge pull request #2513 from topcoder-platform/fix/topgear-mine-url-…
jmgasper Oct 17, 2026
4f3d322
Merge pull request #2542 from topcoder-platform/dev
jmgasper Oct 21, 2026
c7909a6
Merge pull request #2544 from topcoder-platform/dev
jmgasper Oct 21, 2026
b2c4a67
Fix sales dashboards to show the April–March fiscal year
jmgasper Oct 23, 2026
a42577b
Merge pull request #2553 from topcoder-platform/fix/sales-fiscal-year…
jmgasper Oct 23, 2026
d02cac1
PM-6660 Update pipeline bookings, filters and column order
jmgasper Oct 25, 2026
afc9183
Merge pull request #2556 from topcoder-platform/PM-6660-master
jmgasper Oct 26, 2026
b569871
PM-6667 Add sales chart totals and monthly customer payment matrix
jmgasper Oct 26, 2026
9d8eb19
Merge pull request #2558 from topcoder-platform/PM-6667-master
jmgasper Oct 26, 2026
dfc8d15
PM-6668 Add annual totals and customer revenue tables
jmgasper Oct 27, 2026
a9dfee2
Merge pull request #2562 from topcoder-platform/PM-6668-master
jmgasper Oct 27, 2026
06413b6
Show sales fiscal year and prioritize close dates and customer totals
jmgasper Oct 27, 2026
2ca8953
Merge pull request #2569 from topcoder-platform/sales-dashboard-follo…
jmgasper Oct 27, 2026
2b5e014
PM-6672 consolidate sales dashboards and customer reporting
jmgasper Oct 27, 2026
342ff96
Merge pull request #2571 from topcoder-platform/PM-6672-master
jmgasper Oct 27, 2026
a3201f1
PM-6672 show chart breakdowns and expandable customer tables
jmgasper Oct 28, 2026
7316dff
Merge pull request #2573 from topcoder-platform/PM-6672-breakdowns-ma…
jmgasper Oct 28, 2026
83ab865
Merge master into dev to resolve PR #2574 conflicts
jmgasper Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,13 @@ jobs:

build-prod:
<<: *defaults
# 16 GB leaves room for build workers alongside the 8 GB Node heap.
resource_class: xlarge
environment:
DEPLOY_ENV: "PROD"
LOGICAL_ENV: "prod"
APPNAME: "platform-ui-mvp"
NODE_OPTIONS: "--max-old-space-size=8192"
steps: *build_steps

# Just tests commited code.
Expand Down
6 changes: 3 additions & 3 deletions .environments/.env.prod
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
REACT_APP_HOST_ENV=prod

# First-party AWS Clickstream analytics
REACT_APP_ANALYTICS_API_URL=
REACT_APP_AWS_ANALYTICS_APP_ID=
REACT_APP_AWS_ANALYTICS_ENDPOINT=
REACT_APP_ANALYTICS_API_URL=https://api.topcoder.com/v1/analytics
REACT_APP_AWS_ANALYTICS_APP_ID=topcoder_web
REACT_APP_AWS_ANALYTICS_ENDPOINT=https://events.topcoder.com/collect

REACT_APP_ENABLE_TCA_CERT_MONETIZATION=false

Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ NODE_OPTIONS=--max-old-space-size=8192 LOGICAL_ENV=dev yarn build

The `master` branch is auto-deployed to the production environment: https://platform-ui.topcoder.com.

The production build job uses CircleCI's `xlarge` Docker resource class (16 GB RAM)
and `NODE_OPTIONS=--max-old-space-size=8192` to allow an 8 GB Node heap. The optimized
build can exceed Node's default heap limit; the remaining container memory is
reserved for build workers and other process overhead. Keep the container memory
and heap limit sized together when changing these settings in `.circleci/config.yml`.

To run the same production build locally on a machine with sufficient memory:

```bash
nvm use
NODE_OPTIONS=--max-old-space-size=8192 LOGICAL_ENV=prod yarn build
```

## Pull Requests

If a Jira ticket requires any code changes, it should have its own pull request.
Expand Down
19 changes: 12 additions & 7 deletions infrastructure/analytics-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,18 @@ once within the same deadline. Clients can opt into resumable requests with
returns `202`, `Retry-After`, and a server-generated query token. The browser
polls with that token until the original statement completes instead of
starting another warehouse query. Tokens are accepted only for the exact SQL,
validated parameters, retry attempt, and current or previous four-hour window.
The four-hour window remains below the Data API's eight-hour idempotency
retention and permits one complete boundary-crossing window without making
tokens reusable for other reports. An EventBridge schedule invokes the default
Campaigns report, filter-option query, and `/opportunities` route report at each
new window, allowing their statements to finish before an interactive request
while preserving Redshift Serverless idle cost controls. Browser polling is
validated parameters, retry attempt, and the current or preceding fifteen
one-minute windows. New requests reuse a completed Data API statement for at
most one minute; a pending request keeps its original token while polling, so
slow warehouse startup does not restart work at each minute boundary. The
in-process report and filter caches also expire after 60 seconds. Consequently,
loaded data appears in a newly requested report within approximately two minutes,
plus query execution time. Processing runs hourly in both dev and production;
collection continues between batches.

The existing four-hour EventBridge schedule still prepares the default Campaigns
report, filter-option query, and `/opportunities` route report. Interactive requests
refresh independently of that schedule. Browser polling is
bounded. Concurrency and API throttles cap warehouse pressure, and successful
responses use `Cache-Control: private, no-store`. Logs contain request IDs and
service-owned error categories only.
Expand Down
19 changes: 10 additions & 9 deletions infrastructure/analytics-api/src/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@
MAX_RESULT_ROWS = 2_000
MAX_QUERY_ATTEMPTS = 2
REPORT_CACHE_SECONDS = 60
FILTER_CACHE_SECONDS = 300
# Redshift Data API retains idempotency tokens for eight hours. Four-hour
# windows leave a complete prior window available for safe boundary-crossing
# polls while allowing scheduled requests to prepare the default reports.
QUERY_TOKEN_WINDOW_SECONDS = 14_400
FILTER_CACHE_SECONDS = 60
# Reuse completed statements for at most one minute. Pending queries keep
# their original token across fifteen previous windows, so slow warehouse
# startup does not restart work when the freshness window advances.
QUERY_TOKEN_WINDOW_SECONDS = 60
QUERY_TOKEN_RESUME_WINDOWS = 15
QUERY_POLL_DELAY_MILLISECONDS = 1_000
SAFE_FILTER_PATTERN = re.compile(r"^[A-Za-z0-9._~-]{1,100}$")
QUERY_TOKEN_PATTERN = re.compile(r"^[0-9a-f]{64}$")
Expand Down Expand Up @@ -1509,10 +1510,10 @@ def _query_client_token(
sql: Server-owned SQL template.
parameters: Validated named parameters.
attempt: Provider retry index; failed statements receive a new token.
token_window: Optional explicit four-hour bucket used to validate a resume token.
token_window: Optional explicit one-minute bucket used to validate a resume token.

Returns:
Sixty-four-character SHA-256 token stable within a four-hour window.
Sixty-four-character SHA-256 token stable within a one-minute window.

Raises:
Does not raise for validated handler inputs and configured environment values.
Expand Down Expand Up @@ -1543,14 +1544,14 @@ def _query_token_attempt(
query_token: SHA-256 token returned by a pending response.

Returns:
Provider attempt encoded into the valid current or prior-window token.
Provider attempt encoded into a current or recent one-minute token.

Raises:
ValueError when the token belongs to another query, filter set, or expired window.
"""

current_window = int(time.time() // QUERY_TOKEN_WINDOW_SECONDS)
for token_window in (current_window, current_window - 1):
for token_window in range(current_window, current_window - QUERY_TOKEN_RESUME_WINDOWS - 1, -1):
for attempt in range(MAX_QUERY_ATTEMPTS):
expected = _query_client_token(sql, parameters, attempt, token_window)
if query_token == expected:
Expand Down
2 changes: 1 addition & 1 deletion infrastructure/analytics-api/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ Resources:
AnalyticsPrewarmRule:
Type: AWS::Events::Rule
Properties:
Description: Prepares default analytics results once per four-hour Data API token window.
Description: Prepares default analytics results every four hours; interactive requests refresh independently.
ScheduleExpression: cron(0 0/4 * * ? *)
State: ENABLED
Targets:
Expand Down
47 changes: 47 additions & 0 deletions infrastructure/analytics-api/tests/test_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,53 @@ def test_timeout_token_resumes_across_an_idempotency_window_boundary(self) -> No
)
cancel.assert_not_called()

def test_pending_query_resumes_after_multiple_freshness_windows(self) -> None:
"""Warehouse startup can span minutes without creating another statement."""

with patch.object(self.module.time, "time", return_value=6_000):
token = self.module._query_client_token("SELECT 1", [], 0)
with (
patch.object(self.module.time, "time", return_value=6_600),
patch.object(self.module, "_query_wait_seconds", return_value=0),
patch.object(
self.module._redshift_data,
"execute_statement",
return_value={"Id": "original-query"},
) as execute,
self.assertRaises(self.module.QueryTimeout),
):
self.module._execute_query("SELECT 1", [], None, token)
self.assertEqual(token, execute.call_args.kwargs["ClientToken"])

def test_expired_resume_token_does_not_reach_redshift(self) -> None:
"""Polling older than the fifteen-minute allowance fails before provider access."""

with patch.object(self.module.time, "time", return_value=6_000):
token = self.module._query_client_token("SELECT 1", [], 0)
with (
patch.object(self.module.time, "time", return_value=6_960),
patch.object(self.module._redshift_data, "execute_statement") as execute,
self.assertRaisesRegex(ValueError, "invalid or expired"),
):
self.module._execute_query("SELECT 1", [], None, token)
execute.assert_not_called()

def test_new_report_request_refreshes_after_one_minute(self) -> None:
"""A completed report is queried again after a minute instead of four hours."""

with (
patch.object(self.module._redshift_data, "execute_statement", return_value={"Id": "result"}) as execute,
patch.object(self.module._redshift_data, "describe_statement", return_value={"Status": "FINISHED"}),
patch.object(self.module, "_statement_rows", side_effect=[[{"count": 1}], [{"count": 20}]]),
):
with patch.object(self.module.time, "time", return_value=6_000):
first = self.module._execute_query("SELECT COUNT(*) FROM events", [], None)
with patch.object(self.module.time, "time", return_value=6_060):
second = self.module._execute_query("SELECT COUNT(*) FROM events", [], None)
self.assertEqual([{"count": 1}], first)
self.assertEqual([{"count": 20}], second)
self.assertNotEqual(execute.call_args_list[0].kwargs["ClientToken"], execute.call_args_list[1].kwargs["ClientToken"])

def test_resume_token_cannot_be_reused_for_another_query(self) -> None:
"""A server token is bound to the fixed SQL and validated parameter set."""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ const ScorecardHeader: FC = () => {
</span>
</span>
)}
{isAdmin && workflowRun.usage?.cacheCreationInputTokens && (
{isAdmin && !!workflowRun.usage?.cacheCreationInputTokens && (
<span>
<ArrowCircleUpIcon className={styles.md} />
<span>
Expand All @@ -153,7 +153,7 @@ const ScorecardHeader: FC = () => {
</span>
</span>
)}
{isAdmin && workflowRun.usage?.cacheCreationInputTokens && (
{isAdmin && !!workflowRun.usage?.cacheCreationInputTokens && (
<span>
<ArrowCircleUpIcon className={styles.md} />
<span>
Expand Down
9 changes: 9 additions & 0 deletions src/apps/work/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ default and white on a teal hover background.
- `WorkAppContextProvider`: authentication token and derived role/access flags
- `SWRConfigProvider`: shared SWR fetch configuration for the app

## Project asset uploads

The project asset file picker uploads to `submission-staging-prod` when
`REACT_APP_HOST_ENV=prod` and `submission-staging-dev` in other environments.
`REACT_APP_FILE_PICKER_SUBMISSION_CONTAINER_NAME` (or the legacy
`FILE_PICKER_SUBMISSION_CONTAINER_NAME`) overrides this default. The same bucket
is sent as `s3Bucket` to the Projects API when saving the attachment, so it can
transfer the uploaded file into project attachment storage.

## Extending The App

1. Add feature routes under `work-app.routes.tsx` children.
Expand Down
2 changes: 1 addition & 1 deletion src/apps/work/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ export const UPDATE_SKILLS_V5_API_URL = process.env.REACT_APP_UPDATE_SKILLS_V5_A

export const FILE_PICKER_SUBMISSION_CONTAINER_NAME = process.env.REACT_APP_FILE_PICKER_SUBMISSION_CONTAINER_NAME
|| process.env.FILE_PICKER_SUBMISSION_CONTAINER_NAME
|| 'submission-staging-dev'
|| (EnvironmentConfig.ENV === 'prod' ? 'submission-staging-prod' : 'submission-staging-dev')

export const PROJECT_ATTACHMENTS_FOLDER = process.env.REACT_APP_PROJECT_ATTACHMENTS_FOLDER
|| process.env.PROJECT_ATTACHMENTS_FOLDER
Expand Down
Loading