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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ RUN --mount=type=secret,id=ghtoken,env=NODE_AUTH_TOKEN \
# ---------------------------------------------------------------------------
# Stage 3: bake the finished sibyl dist into the trex backend.
# ---------------------------------------------------------------------------
FROM ghcr.io/ohdsi/trexsql:latest@sha256:bdeea44d964b2eddf9346aacfc8090311a9cba1b2f1be5a59742445c9bd5cf64
FROM ghcr.io/ohdsi/trexsql:latest@sha256:6c3ec02c884766fd52b27c34b3dcd1a12cd385efa74edbfba03d620018794733

# --- R runtime for hades / Strategus ---------------------------------------
# The trexsql base ships the hades DuckDB extension but NOT R, so hades_execute
Expand Down
6 changes: 5 additions & 1 deletion central/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,11 @@ Resources:
PreTokenGenFunction:
Type: AWS::Serverless::Function
Properties:
Handler: index.preTokenGen
# esbuild emits the bundle named after the entry point (preTokenGen.mjs),
# so the handler module must be `preTokenGen`, not `index`. Using
# `index.preTokenGen` makes the runtime fail with "Cannot find module 'index'",
# which Cognito surfaces as UserLambdaValidationException on token generation.
Handler: preTokenGen.preTokenGen
CodeUri: api/
Metadata:
BuildMethod: esbuild
Expand Down
16 changes: 12 additions & 4 deletions central/web/src/auth/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,14 @@ export async function handleCallback(search: string): Promise<void> {
if (!code) throw new Error('missing authorization code');
if (state !== localStorage.getItem(STATE_KEY)) throw new Error('state mismatch');

const verifier = localStorage.getItem(VERIFIER_KEY) ?? '';
// Consume the PKCE verifier up-front so a duplicate or reloaded callback can't
// re-submit the same single-use code (Cognito rejects reuse as invalid_grant,
// which previously surfaced as the opaque "token exchange failed").
const verifier = localStorage.getItem(VERIFIER_KEY);
if (!verifier) throw new Error('no pending sign-in — start login again');
localStorage.removeItem(VERIFIER_KEY);
localStorage.removeItem(STATE_KEY);

const body = new URLSearchParams({
grant_type: 'authorization_code',
client_id: config.clientId,
Expand All @@ -89,8 +96,9 @@ export async function handleCallback(search: string): Promise<void> {
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body,
});
if (!res.ok) throw new Error('token exchange failed');
if (!res.ok) {
const detail = await res.text().catch(() => '');
throw new Error(`token exchange failed (${res.status}): ${detail}`);
}
storeTokens((await res.json()) as TokenResponse);
localStorage.removeItem(VERIFIER_KEY);
localStorage.removeItem(STATE_KEY);
}
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ services:
container_name: sibyl-trex-init
# Pinned to the same multi-arch index digest as the Dockerfile FROM (=
# trexsql:latest); each host pulls its native variant (amd64 or arm64).
image: ghcr.io/ohdsi/trexsql:latest@sha256:a57e5d3eadcb73b6f0b70cef28c0e42d2a0a7e0256310bb1eabc2a8ccd4375ae
image: ghcr.io/ohdsi/trexsql:latest@sha256:6c3ec02c884766fd52b27c34b3dcd1a12cd385efa74edbfba03d620018794733
entrypoint: /usr/local/bin/trex-init
environment:
TREX_SECRETS_DIR: /shared
Expand Down
30 changes: 30 additions & 0 deletions plugins/metadata-api/migrations/V3__notebook_graphql_grants.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-- Expose the `notebook` schema to PostGraphile. PostGraphile connects as the
-- unprivileged `authenticator` role and SET ROLEs to anon/authenticated/
-- service_role per request, so it only introspects objects those roles can see.
-- Plugin-created schemas get no grants by default (only core/schema/
-- V3__graphql_trexdb_grants.sql grants `trexdb`), so without this the study
-- editor's createNotebookAnalysisDefinition mutation is absent from the GraphQL
-- schema and saving a study fails with HTTP 400 "Cannot query field
-- createNotebookAnalysisDefinition on type Mutation".
--
-- notebook.* has no RLS and holds no secrets, so authenticated gets full table
-- CRUD (unscoped) and service_role gets ALL. Mirrors the non-sensitive-table
-- pattern in core/schema/V3__graphql_trexdb_grants.sql.

GRANT USAGE ON SCHEMA notebook TO anon, authenticated, service_role, authenticator;

GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA notebook TO authenticated;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA notebook TO authenticated;

GRANT ALL ON ALL TABLES IN SCHEMA notebook TO service_role;
GRANT ALL ON ALL SEQUENCES IN SCHEMA notebook TO service_role;

-- Future tables/sequences in the schema inherit the same grants.
ALTER DEFAULT PRIVILEGES IN SCHEMA notebook
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO authenticated;
ALTER DEFAULT PRIVILEGES IN SCHEMA notebook
GRANT USAGE ON SEQUENCES TO authenticated;
ALTER DEFAULT PRIVILEGES IN SCHEMA notebook
GRANT ALL ON TABLES TO service_role;
ALTER DEFAULT PRIVILEGES IN SCHEMA notebook
GRANT ALL ON SEQUENCES TO service_role;
70 changes: 36 additions & 34 deletions plugins/results-viewer/shinylive-app/app.R
Original file line number Diff line number Diff line change
Expand Up @@ -699,49 +699,51 @@ ui <- tagList(
}
"))),
tags$script(HTML("
if (window.top && window.top !== window) {
window.top.postMessage({type: 'SHINYLIVE_READY'}, '*');
}
// This inline script runs at HTML parse time, BEFORE Shiny's JS API exists.
// So we must NOT touch Shiny.* here: at parse time `typeof Shiny` is
// undefined, which previously meant (a) Shiny.setInputValue threw and
// (b) the APP_READY handler was never registered, so the host overlay never
// cleared. Instead we buffer incoming data and defer all Shiny work until
// shiny:connected (with a poll fallback), then flush + signal readiness.
var __resultFiles = {};
var __resultDb = '';
var __pending = null;
function __rvFlush() {
if (typeof Shiny === 'undefined' || typeof Shiny.setInputValue !== 'function' || !__pending) return;
if (__pending.k === 'db') Shiny.setInputValue('result_db', __pending.v, {priority: 'event'});
else Shiny.setInputValue('result_files', __pending.v, {priority: 'event'});
__pending = null;
}
function __rvAck() { if (window.top !== window) window.top.postMessage({type: 'DATA_RECEIVED'}, '*'); }
window.addEventListener('message', function(event) {
var d = event.data;
if (!d) return;
if (d.type === 'RESULT_DB_BEGIN') { __resultDb = ''; return; }
if (d.type === 'RESULT_DB_CHUNK') { __resultDb += d.content; return; }
if (d.type === 'RESULT_DB_END') {
Shiny.setInputValue('result_db', __resultDb, {priority: 'event'});
if (window.top !== window) window.top.postMessage({type: 'DATA_RECEIVED'}, '*');
return;
}
if (d.type === 'RESULT_FILES') {
Shiny.setInputValue('result_files', d.files, {priority: 'event'});
if (window.top !== window) window.top.postMessage({type: 'DATA_RECEIVED'}, '*');
return;
}
if (d.type === 'RESULT_FILES_BEGIN') {
__resultFiles = {};
return;
}
if (d.type === 'RESULT_FILES_CHUNK') {
__resultFiles[d.name] = d.content;
return;
}
if (d.type === 'RESULT_FILES_END') {
Shiny.setInputValue('result_files', __resultFiles, {priority: 'event'});
if (window.top !== window) window.top.postMessage({type: 'DATA_RECEIVED'}, '*');
return;
}
if (d.type === 'RESULT_DB_END') { __pending = {k: 'db', v: __resultDb}; __rvFlush(); __rvAck(); return; }
if (d.type === 'RESULT_FILES') { __pending = {k: 'files', v: d.files}; __rvFlush(); __rvAck(); return; }
if (d.type === 'RESULT_FILES_BEGIN') { __resultFiles = {}; return; }
if (d.type === 'RESULT_FILES_CHUNK') { __resultFiles[d.name] = d.content; return; }
if (d.type === 'RESULT_FILES_END') { __pending = {k: 'files', v: __resultFiles}; __rvFlush(); __rvAck(); return; }
});
// Relay R's APP_READY custom message to the parent Vue host so it can
// hide its loading overlay once tables are in DuckDB.
if (typeof Shiny !== 'undefined') {
Shiny.addCustomMessageHandler('APP_READY', function(payload) {
if (window.top && window.top !== window) {
window.top.postMessage({type: 'APP_READY', tables: payload && payload.tables}, '*');
}
});
function __rvReady() {
if (window.__rvReadyDone) return; window.__rvReadyDone = true;
try {
Shiny.addCustomMessageHandler('APP_READY', function(payload) {
if (window.top && window.top !== window) {
window.top.postMessage({type: 'APP_READY', tables: payload && payload.tables}, '*');
}
});
} catch (e) {}
__rvFlush();
if (window.top && window.top !== window) window.top.postMessage({type: 'SHINYLIVE_READY'}, '*');
}
document.addEventListener('shiny:connected', __rvReady);
var __rvTries = 0;
var __rvPoll = setInterval(function() {
if (typeof Shiny !== 'undefined' && typeof Shiny.setInputValue === 'function') { clearInterval(__rvPoll); __rvReady(); }
else if (++__rvTries > 1200) { clearInterval(__rvPoll); }
}, 500);
"))
)

Expand Down
39 changes: 37 additions & 2 deletions plugins/sibyl/src/components/NavBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,26 +53,61 @@
data-test="nav-settings"
@click="ui.toggleSettings()"
/>
<AtlasMenu v-if="auth.user">
<template #activator="{ props }">
<AtlasButton
variant="ghost"
v-bind="props"
icon="mdi-menu-down"
icon-position="end"
data-test="nav-user-menu"
>
<AtlasIcon left>mdi-account-circle</AtlasIcon>
{{ userDisplayName }}
</AtlasButton>
</template>
<AtlasList>
<AtlasListItem data-test="nav-logout" @click="handleLogout">
<template #prepend>
<AtlasIcon>mdi-logout</AtlasIcon>
</template>
<v-list-item-title>Sign out</v-list-item-title>
</AtlasListItem>
</AtlasList>
</AtlasMenu>
</div>
</div>
</header>
</template>

<script setup lang="ts">
import { ref, computed, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import {
generatePluginMenuItems,
filterTextNavItems,
type PluginMenuItem,
} from '@/plugins/navigation/PluginMenuIntegration'
import { useUiStore } from '@/stores/ui'
import { useAuthStore } from '@/stores/auth'
import { pluginRegistry } from '@/plugins/core/PluginRegistry'
import ohdsiLogo from '@/assets/ohdsi-logo.png'
import { AtlasIconButton } from '@ohdsi/atlas-ui'
import { AtlasIconButton, AtlasButton, AtlasIcon, AtlasList, AtlasListItem, AtlasMenu } from '@ohdsi/atlas-ui'

const route = useRoute()
const router = useRouter()
const ui = useUiStore()
const auth = useAuthStore()

// Prefer the profile name, fall back to email (mirrors trexAuth's username rule).
const userDisplayName = computed(
() => auth.user?.user_metadata?.name || auth.user?.email || 'Account',
)

function handleLogout(): void {
auth.logout()
void router.push({ name: 'login' })
}

const menuItems = ref<PluginMenuItem[]>(filterTextNavItems(generatePluginMenuItems()))

Expand Down
13 changes: 13 additions & 0 deletions plugins/strategus/src/models/ModuleSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ export interface CohortMethodAnalysis {
iptwTruncationFraction: number;
outcomeModelType: 'cox' | 'logistic' | 'poisson';
useCleanWindowForPriorOutcomeLookback: boolean;
// Per-analysis study-population window (createStudyPopArgs)
riskWindowStart: number;
startAnchor: 'cohort start' | 'cohort end';
riskWindowEnd: number;
endAnchor: 'cohort start' | 'cohort end';
minDaysAtRisk: number;
priorOutcomeLookback: number;
}

export interface CohortMethodSettings {
Expand Down Expand Up @@ -127,6 +134,12 @@ export interface PlpSettings {
splitType: 'time' | 'subject' | 'stratified';
runCalibration: boolean;
calibrationBins: number;
runFeatureEngineering: boolean;
runSampleData: boolean;
runPreprocessData: boolean;
runModelDevelopment: boolean;
runCovariateSummary: boolean;
skipDiagnostics: boolean;
}

export interface PlpValidationDesign {
Expand Down
16 changes: 15 additions & 1 deletion plugins/strategus/src/services/CovariateDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,13 @@ const ALL_FEATURE_FLAGS = [
'MeasurementLongTerm',
'MeasurementShortTerm',
'MeasurementRangeGroupLongTerm',
'MeasurementRangeGroupShortTerm',
'MeasurementValueAsConceptLongTerm',
'MeasurementValueAsConceptShortTerm',
'ObservationLongTerm',
'ObservationShortTerm',
'ObservationValueAsConceptLongTerm',
'ObservationValueAsConceptShortTerm',
'CharlsonIndex',
'Dcsi',
'Chads2',
Expand Down Expand Up @@ -69,13 +74,22 @@ export function createDefaultCovariateSettings(opts?: CovariateDefaultsOptions |
endDays = opts.endDays ?? 0;
}

// Build feature flags: defaults are all true, override with provided values
// Build feature flags: defaults are all true, override with provided values.
// Also emit any override-only feature keys not in the canonical list so that
// value-as-concept / range-group groups round-trip through the serializer.
const featureFlags: Record<string, boolean> = {};
for (const flag of ALL_FEATURE_FLAGS) {
featureFlags[flag] = featureOverrides !== undefined && flag in featureOverrides
? featureOverrides[flag]
: true;
}
if (featureOverrides) {
for (const flag of Object.keys(featureOverrides)) {
if (!(flag in featureFlags)) {
featureFlags[flag] = featureOverrides[flag];
}
}
}

return {
temporal: false,
Expand Down
17 changes: 17 additions & 0 deletions plugins/strategus/src/services/DefaultsFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ export function createDefaultCohortMethodAnalysis(analysisId = 1): CohortMethodA
iptwTruncationFraction: 0.99,
outcomeModelType: 'cox',
useCleanWindowForPriorOutcomeLookback: false,
riskWindowStart: 0,
startAnchor: 'cohort start',
riskWindowEnd: 0,
endAnchor: 'cohort end',
minDaysAtRisk: 1,
priorOutcomeLookback: 99999,
};
}

Expand Down Expand Up @@ -152,8 +158,13 @@ export function createDefaultCovariateFeatures(): Record<string, boolean> {
MeasurementLongTerm: true,
MeasurementShortTerm: true,
MeasurementRangeGroupLongTerm: true,
MeasurementRangeGroupShortTerm: true,
MeasurementValueAsConceptLongTerm: true,
MeasurementValueAsConceptShortTerm: true,
ObservationLongTerm: true,
ObservationShortTerm: true,
ObservationValueAsConceptLongTerm: true,
ObservationValueAsConceptShortTerm: true,
CharlsonIndex: true,
Dcsi: true,
Chads2: true,
Expand Down Expand Up @@ -256,6 +267,12 @@ export function createDefaultPlp(): PlpSettings {
splitType: 'subject',
runCalibration: true,
calibrationBins: 10,
runFeatureEngineering: false,
runSampleData: false,
runPreprocessData: true,
runModelDevelopment: true,
runCovariateSummary: true,
skipDiagnostics: false,
};
}

Expand Down
Loading
Loading