From 54e8252dc98afdad8ac189505eba3e2427102039 Mon Sep 17 00:00:00 2001 From: Travis Rich Date: Wed, 15 Apr 2026 22:10:14 -0400 Subject: [PATCH 1/4] First pass at moving overview admin dashboard from metabase to native --- .gitignore | 1 - .../AdminDashboard/AdminDashboard.tsx | 40 -- client/containers/AdminDashboard/Chart.tsx | 30 - .../AdminDashboard/adminDashboard.scss | 6 - client/containers/App/paths.ts | 6 - .../SuperAdminDashboard/PlatformAnalytics.tsx | 551 ++++++++++++++++++ .../platformAnalytics.scss | 237 ++++++++ .../containers/SuperAdminDashboard/tabs.tsx | 5 + client/containers/index.ts | 1 - infra/.env.dev.enc | 117 ++-- infra/.env.enc | 119 ++-- infra/.env.test | 2 - infra/ENV.md | 1 - package.json | 1 - pnpm-lock.yaml | 12 +- server/apiRoutes.ts | 2 + server/envSchema.ts | 1 - server/platformAnalytics/api.ts | 217 +++++++ server/routes/adminDashboard.tsx | 57 +- server/routes/index.ts | 3 +- server/utils/metabase.ts | 72 --- utils/analytics/usePageOnce.ts | 2 +- utils/superAdmin.ts | 2 +- 23 files changed, 1143 insertions(+), 342 deletions(-) delete mode 100644 client/containers/AdminDashboard/AdminDashboard.tsx delete mode 100644 client/containers/AdminDashboard/Chart.tsx delete mode 100644 client/containers/AdminDashboard/adminDashboard.scss create mode 100644 client/containers/SuperAdminDashboard/PlatformAnalytics.tsx create mode 100644 client/containers/SuperAdminDashboard/platformAnalytics.scss create mode 100644 server/platformAnalytics/api.ts delete mode 100644 server/utils/metabase.ts diff --git a/.gitignore b/.gitignore index 3b1ce5d009..e60f25a967 100755 --- a/.gitignore +++ b/.gitignore @@ -70,7 +70,6 @@ tsconfig.tsbuildinfo .jest/secret-env.js infra/pgdata/ -infra/metabase-plugins/ tmp/ details.md diff --git a/client/containers/AdminDashboard/AdminDashboard.tsx b/client/containers/AdminDashboard/AdminDashboard.tsx deleted file mode 100644 index 8aeb766499..0000000000 --- a/client/containers/AdminDashboard/AdminDashboard.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; - -import IframeResizer from 'iframe-resizer-react'; - -import './adminDashboard.scss'; - -type Props = { - impactData: any; -}; - -const AdminDashboard = (props: Props) => { - const { impactData } = props; - const { baseToken } = impactData; - const dashUrl = `http://localhost:3030/embed/dashboard/${baseToken}#bordered=false&titled=false`; - const getOffset = (width) => { - return width < 960 ? 45 : 61; - }; - return ( -
-
-

PubPub Top Analytics

-
-
- ( - { - iframe.style.height = `${height - getOffset(width)}px`; - }} - /> - ) -
-
- ); -}; - -export default AdminDashboard; diff --git a/client/containers/AdminDashboard/Chart.tsx b/client/containers/AdminDashboard/Chart.tsx deleted file mode 100644 index a923bd6c5e..0000000000 --- a/client/containers/AdminDashboard/Chart.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; - -import PropTypes from 'prop-types'; -import { Bar, ComposedChart, Line, Tooltip, XAxis, YAxis } from 'recharts'; - -const propTypes = { - title: PropTypes.string.isRequired, - data: PropTypes.object.isRequired, -}; - -const Chart = function (props) { - return ( -
-

{props.title}

- - - - - - - - - - -
- ); -}; - -Chart.propTypes = propTypes; -export default Chart; diff --git a/client/containers/AdminDashboard/adminDashboard.scss b/client/containers/AdminDashboard/adminDashboard.scss deleted file mode 100644 index dffeee37c1..0000000000 --- a/client/containers/AdminDashboard/adminDashboard.scss +++ /dev/null @@ -1,6 +0,0 @@ -.admin-dashboard { - h3 { - margin-bottom: 4em; - text-align: center; - } -} diff --git a/client/containers/App/paths.ts b/client/containers/App/paths.ts index a0f3f060eb..0e3bb43e9d 100644 --- a/client/containers/App/paths.ts +++ b/client/containers/App/paths.ts @@ -1,6 +1,5 @@ import { About, - AdminDashboard, Collection, CommunityCreate, CommunityServices, @@ -46,11 +45,6 @@ export default (viewData, locationData, chunkName) => { ActiveComponent: About, hideNav: locationData.isBasePubPub, }, - AdminDashboard: { - ActiveComponent: AdminDashboard, - hideNav: true, - hideFooter: true, - }, Collection: { ActiveComponent: Collection, }, diff --git a/client/containers/SuperAdminDashboard/PlatformAnalytics.tsx b/client/containers/SuperAdminDashboard/PlatformAnalytics.tsx new file mode 100644 index 0000000000..153c2a041f --- /dev/null +++ b/client/containers/SuperAdminDashboard/PlatformAnalytics.tsx @@ -0,0 +1,551 @@ +import React, { useCallback, useEffect, useState } from 'react'; + +import { Button, ButtonGroup, NonIdealState } from '@blueprintjs/core'; +import { + Area, + AreaChart, + CartesianGrid, + Legend, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; + +import { apiFetch } from 'client/utils/apiFetch'; + +import './platformAnalytics.scss'; + +type MonthlySeries = { month: string; count: number }[]; + +type AnalyticsData = { + totals: { + communities: number; + users: number; + pubs: number; + pageviews: number; + }; + communitiesByMonth: MonthlySeries; + usersByMonth: MonthlySeries; + pubsByMonth: MonthlySeries; + pageviewsByMonth: MonthlySeries; + activeCommunityTrendActivity: MonthlySeries; + activeCommunityTrendPubs: MonthlySeries; +}; + +type PeriodData = { + counts: { + communities: number; + users: number; + pubs: number; + pageviews: number; + }; + newCommunities: { + title: string; + subdomain: string; + createdAt: string; + description: string; + }[]; + totalNewCommunities: number; + page: number; + pageSize: number; +}; + +const fmt = (n: number): string => { + if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; + if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`; + return n.toLocaleString(); +}; + +const fmtMonth = (s: string): string => { + const d = new Date(s); + return d.toLocaleDateString(undefined, { year: '2-digit', month: 'short' }); +}; + +const COLORS = { + communities: '#2B95D6', + users: '#15B371', + pubs: '#D9822B', + pageviews: '#7157D9', + activityTrend: '#F55656', + pubTrend: '#D9822B', +}; + +/** Convert raw monthly counts into a cumulative series. */ +function toCumulative(series: MonthlySeries): { month: string; count: number; cumulative: number }[] { + let total = 0; + return series.map((d) => { + total += d.count; + return { month: d.month, count: d.count, cumulative: total }; + }); +} + +const ScalarCard = ({ label, value, color }: { label: string; value: string; color: string }) => ( +
+
{value}
+
{label}
+
+); + +const GrowthChart = ({ + title, + data, + color, +}: { + title: string; + data: MonthlySeries; + color: string; +}) => { + const cumData = toCumulative(data); + return ( +
+

{title}

+ + + + + + [fmt(v), 'Cumulative']} + /> + + + +
+ ); +}; + +const MonthlyChart = ({ + title, + data, + color, +}: { + title: string; + data: MonthlySeries; + color: string; +}) => ( +
+

{title}

+ + + + + + [fmt(v), 'Count']} /> + + + +
+); + +const ActiveCommunityChart = ({ + activityData, + pubData, +}: { + activityData: MonthlySeries; + pubData: MonthlySeries; +}) => { + // Merge the two series by month + const monthMap = new Map(); + for (const d of activityData) { + monthMap.set(d.month, { month: d.month, byActivity: d.count, byPubCreation: 0 }); + } + for (const d of pubData) { + const existing = monthMap.get(d.month); + if (existing) { + existing.byPubCreation = d.count; + } else { + monthMap.set(d.month, { month: d.month, byActivity: 0, byPubCreation: d.count }); + } + } + const merged = [...monthMap.values()].sort( + (a, b) => new Date(a.month).getTime() - new Date(b.month).getTime(), + ); + + return ( +
+

Active Communities (Monthly)

+ + + + + + + + + + + +
+ ); +}; + +// ── Date range helpers ────────────────────────────────────────────────────── + +type PresetKey = '30d' | '90d' | '1y' | '2y' | 'custom'; +const PRESETS: { key: PresetKey; label: string; days?: number }[] = [ + { key: '30d', label: '30 days', days: 30 }, + { key: '90d', label: '90 days', days: 90 }, + { key: '1y', label: '1 year', days: 365 }, + { key: '2y', label: '2 years', days: 730 }, + { key: 'custom', label: 'Custom' }, +]; + +function presetRange(days: number) { + const end = new Date(); + const start = new Date(); + start.setDate(end.getDate() - days); + return { + startDate: start.toISOString().slice(0, 10), + endDate: end.toISOString().slice(0, 10), + }; +} + +// ── Period Explorer sub-component ─────────────────────────────────────────── + +const PeriodExplorer = () => { + const [preset, setPreset] = useState('1y'); + const [startDate, setStartDate] = useState(() => presetRange(365).startDate); + const [endDate, setEndDate] = useState(() => presetRange(365).endDate); + const [period, setPeriod] = useState(null); + const [loading, setLoading] = useState(false); + const [page, setPage] = useState(0); + + const fetchPeriod = useCallback( + async (sd: string, ed: string, pg: number) => { + setLoading(true); + try { + const res = await apiFetch.get( + `/api/platformAnalytics/period?startDate=${sd}&endDate=${ed}&page=${pg}`, + ); + setPeriod(res as PeriodData); + } finally { + setLoading(false); + } + }, + [], + ); + + // Fetch on mount and whenever dates or page change + useEffect(() => { + fetchPeriod(startDate, endDate, page); + }, [startDate, endDate, page, fetchPeriod]); + + const handlePreset = (p: PresetKey) => { + setPreset(p); + if (p !== 'custom') { + const days = PRESETS.find((x) => x.key === p)!.days!; + const range = presetRange(days); + setStartDate(range.startDate); + setEndDate(range.endDate); + setPage(0); + } + }; + + const totalPages = period ? Math.ceil(period.totalNewCommunities / period.pageSize) : 0; + + return ( +
+
+ + {PRESETS.map((p) => ( + + ))} + + {preset === 'custom' && ( + + { + setStartDate(e.target.value); + setPage(0); + }} + /> + + { + setEndDate(e.target.value); + setPage(0); + }} + /> + + )} +
+ + {loading && !period && ( + <> +
+ {[0, 1, 2, 3].map((i) => ( +
+
+
+
+ ))} +
+ {[0, 1, 2, 3, 4, 5].map((j) => ( +
+
+
+
+
+
+ ))} + + )} + + {period && ( + <> +
+ + + + +
+ + {period.newCommunities.length > 0 && ( + <> +

+ New Communities ({period.totalNewCommunities}) +

+ + + + + + + + + + + {period.newCommunities.map((c) => ( + + + + + + + ))} + +
TitleSubdomainCreatedDescription
{c.title} + + {c.subdomain} + + + {new Date(c.createdAt).toLocaleDateString( + undefined, + { + year: 'numeric', + month: 'short', + day: 'numeric', + }, + )} + {c.description}
+ {totalPages > 1 && ( +
+ + + Page {page + 1} of {totalPages} + + +
+ )} + + )} + {period.newCommunities.length === 0 && ( +

No new communities in this period.

+ )} + + )} +
+ ); +}; + +// ── Main component ────────────────────────────────────────────────────────── + +const PlatformAnalytics = () => { + const [data, setData] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + apiFetch.get('/api/platformAnalytics').then(setData).catch((err) => setError(err.message)); + }, []); + + if (error) { + return ; + } + + if (!data) { + return ( +
+

PubPub Platform Analytics

+ +
All-Time Totals
+
+ {[0, 1, 2, 3].map((i) => ( +
+
+
+
+ ))} +
+ +
Cumulative Growth
+
+ {[0, 1, 2, 3].map((i) => ( +
+
+
+
+ ))} +
+ +
Monthly Growth
+
+ {[0, 1, 2, 3].map((i) => ( +
+
+
+
+ ))} +
+ +
Active Communities
+
+
+
+
+
+
+ +
Period Explorer
+
+
+
+ {[0, 1, 2, 3].map((i) => ( +
+
+
+
+ ))} +
+ {[0, 1, 2, 3, 4, 5].map((j) => ( +
+
+
+
+
+
+ ))} +
+
+ ); + } + + const { totals } = data; + + return ( +
+

PubPub Platform Analytics

+ +
All-Time Totals
+
+ + + + +
+ +
Cumulative Growth
+
+ + + + +
+ +
Monthly Growth
+
+ + + + +
+ +
Active Communities
+
+ +
+ +
Period Explorer
+ +
+ ); +}; + +export default PlatformAnalytics; diff --git a/client/containers/SuperAdminDashboard/platformAnalytics.scss b/client/containers/SuperAdminDashboard/platformAnalytics.scss new file mode 100644 index 0000000000..e4b3fd7d87 --- /dev/null +++ b/client/containers/SuperAdminDashboard/platformAnalytics.scss @@ -0,0 +1,237 @@ +.platform-analytics { + // ── Skeleton loading ──────────────────────────────────────────────── + @keyframes skeleton-pulse { + 0% { opacity: 0.15; } + 50% { opacity: 0.25; } + 100% { opacity: 0.15; } + } + + .skeleton-line { + background: #1c2127; + border-radius: 3px; + animation: skeleton-pulse 1.5s ease-in-out infinite; + } + + .skeleton-stat { + .skeleton-value { + width: 80px; + height: 28px; + } + + .skeleton-label { + width: 100px; + height: 10px; + margin-top: 6px; + } + } + + .skeleton-heading { + width: 40%; + height: 13px; + margin-bottom: 10px; + } + + .skeleton-chart { + width: 100%; + height: 200px; + background: #1c2127; + border-radius: 3px; + animation: skeleton-pulse 1.5s ease-in-out infinite; + } + + .skeleton-button-row { + width: 260px; + height: 28px; + margin-bottom: 12px; + } + + .skeleton-table-row { + display: flex; + gap: 12px; + padding: 8px 0; + + .skeleton-cell { + flex: 1; + height: 12px; + } + + .skeleton-cell-short { + width: 60px; + height: 12px; + flex: none; + } + } + + // ── Layout ────────────────────────────────────────────────────────── + h3 { + margin-bottom: 1em; + } + + .section-label { + font-size: 13px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: #5c7080; + margin: 1.5em 0 0.5em; + border-bottom: 1px solid #e1e8ed; + padding-bottom: 4px; + } + + .scalar-row { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 12px; + margin-bottom: 1em; + + @media (max-width: 900px) { + grid-template-columns: repeat(2, 1fr); + } + @media (max-width: 500px) { + grid-template-columns: 1fr; + } + } + + .scalar-card { + background: #fff; + border: 1px solid #e1e8ed; + border-left-width: 4px; + border-radius: 4px; + padding: 12px 16px; + + .scalar-value { + font-size: 28px; + font-weight: 700; + line-height: 1.2; + } + + .scalar-label { + font-size: 12px; + color: #5c7080; + margin-top: 2px; + } + } + + .chart-row { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 12px; + margin-bottom: 1em; + + @media (max-width: 1100px) { + grid-template-columns: repeat(2, 1fr); + } + @media (max-width: 600px) { + grid-template-columns: 1fr; + } + } + + .chart-card { + background: #fff; + border: 1px solid #e1e8ed; + border-radius: 4px; + padding: 12px; + + h4 { + font-size: 13px; + font-weight: 600; + margin: 0 0 8px; + } + + &.chart-card-wide { + grid-column: 1 / -1; + } + } + + .table-title { + font-size: 14px; + font-weight: 600; + margin: 1em 0 0.5em; + } + + .new-communities-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; + margin-bottom: 2em; + overflow-x: auto; + display: block; + + th, + td { + text-align: left; + padding: 6px 10px; + border-bottom: 1px solid #e1e8ed; + } + + th { + font-weight: 600; + color: #5c7080; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.04em; + } + + .desc-cell { + max-width: 300px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + a { + color: #2b95d6; + text-decoration: none; + &:hover { + text-decoration: underline; + } + } + } + + .period-explorer { + margin-bottom: 2em; + + .period-controls { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; + flex-wrap: wrap; + + .custom-dates { + display: inline-flex; + align-items: center; + gap: 6px; + + input[type='date'] { + font-size: 13px; + padding: 3px 6px; + border: 1px solid #ced9e0; + border-radius: 3px; + } + + .date-sep { + color: #5c7080; + } + } + } + + .pagination { + display: flex; + align-items: center; + gap: 10px; + margin-top: 8px; + + .page-info { + font-size: 12px; + color: #5c7080; + } + } + + .no-data { + color: #5c7080; + font-size: 13px; + font-style: italic; + } + } +} diff --git a/client/containers/SuperAdminDashboard/tabs.tsx b/client/containers/SuperAdminDashboard/tabs.tsx index 8189500d6f..523921f62c 100644 --- a/client/containers/SuperAdminDashboard/tabs.tsx +++ b/client/containers/SuperAdminDashboard/tabs.tsx @@ -4,6 +4,7 @@ import React from 'react'; import CommunitySpam from './CommunitySpam'; import LandingPageFeatures from './LandingPageFeatures'; +import PlatformAnalytics from './PlatformAnalytics'; import UserSpam from './UserSpam'; type SuperAdminTab = { @@ -12,6 +13,10 @@ type SuperAdminTab = { }; export const superAdminTabs: Record = { + analytics: { + title: 'Analytics', + component: PlatformAnalytics, + }, landingPageFeatures: { title: 'Landing Page features', component: LandingPageFeatures, diff --git a/client/containers/index.ts b/client/containers/index.ts index f8ab7d357e..85e68f4713 100644 --- a/client/containers/index.ts +++ b/client/containers/index.ts @@ -1,5 +1,4 @@ export { default as About } from './About/About'; -export { default as AdminDashboard } from './AdminDashboard/AdminDashboard'; export { default as Collection } from './Collection/Collection'; export { default as CommunityCreate } from './CommunityCreate/CommunityCreate'; export { default as CommunityServices } from './CommunityServices/CommunityServices'; diff --git a/infra/.env.dev.enc b/infra/.env.dev.enc index 09cd8af1c6..ddb0d8da75 100644 --- a/infra/.env.dev.enc +++ b/infra/.env.dev.enc @@ -1,67 +1,66 @@ -AES_ENCRYPTION_KEY=ENC[AES256_GCM,data:sdsTMHdnONVMVLGEGEaLKSkzZwPRGVR7t0EqwPyYiOVU/RQWmwt5GLrkSpgxnslnB+vKqJjJ3EHy2Ra0yJTq2Q==,iv:ZFbJ2G5vvCx+Jpsc2W8lgU81JazvO1yvrFuwp8Vkw1Q=,tag:yZxVETMwMT7IQ4XklgeXEg==,type:str] -ALTCHA_HMAC_KEY=ENC[AES256_GCM,data:vVeD6Jj5kbnF1RanqGGOT4JQEvBZTNzWSTDN+DHgbysDpK8dLis9/mAnwZynet/n8KaUHEQowfGNyIrQqphE0Q==,iv:Rs6ztx9f0p229aYAaXOljp5mtaOi9JadcYMlvSmTLpg=,tag:iBY50HZddP7gy+G/HBkGww==,type:str] -AWS_ACCESS_KEY_ID=ENC[AES256_GCM,data:1IHTxJYOjCDuwa/iAm2nkIonu0c=,iv:CCTLJdrtuUjHDpoQlIkp1Ym/eBg8+O9qMBOztmiLANA=,tag:Q3WBPQm0l37XP4HmCWn0Rg==,type:str] -AWS_BACKUP_ACCESS_KEY_ID=ENC[AES256_GCM,data:Uq5mD7Hm5ZTN2elIBB5IWU1Sn+E=,iv:yn5VNT2Hk6hRrkxWPAKTSx34o1HHQV2PfJ5UjE4hwwA=,tag:TLTNtLOZNFeSXxsMw9qAwQ==,type:str] -AWS_BACKUP_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:Qcsvc4j/FKXblUEwGtQxnEoyWG6RjuD6NjpPl+t2r83h9h+zgso/HA==,iv:eknE9q5ogT45PqEbhb/eFfYELqeChbJwsijxRe4Eyc4=,tag:BQPAxgEi2sovfFMCJlKbDw==,type:str] -AWS_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:QmLjRtrDRWpZPf+6XqBbzcww2u3aP1yRHNucmNyMJ+tsV5ugLRv/Qg==,iv:K9jfcn9Ly9EV9y72BWUueBHTV6WrsHmDSfk1t16A0t8=,tag:OZNU6o6qSHpTjsnQLcHnJg==,type:str] -BACKUPS_SECRET=ENC[AES256_GCM,data:uQ6Lv6LVwLyXd9decuD3Dxeno7SNo2At33ZWjyZJ6Z6GYzO2kq3StgOyOnI=,iv:t/pTDeXi4gl/hudPE5SPd6KWAtOcbsSesxntwCes3cg=,tag:70TlQfceSjUJ+MX921w0oQ==,type:str] -CLOUDFLARE_ANALYTICS_API_TOKEN=ENC[AES256_GCM,data:GrMqgtaJrlFj+HihSJsg49DTrC+jREYnOcNOhTH2MZJHhfh1eAREIKmZTJjfId1ZbQGY2d0=,iv:w7uk9FcTgZTklLAsnctaU3FxR1/NEGEFLBiNbtitZ0U=,tag:+s//7IdBNXCe43R9HVklvA==,type:str] -CLOUDFLARE_ZONE_TAG=ENC[AES256_GCM,data:9RUjkbJfD08dxJkTF4jdT8AuCvjYEisYW/X8pZD2bS0=,iv:0QdNUvlE34SSas7OPaqTUkqk9WZlRTYolOXa07nlnIc=,tag:7rPxmkh1mLXvIxn0mXUBTw==,type:str] -DATABASE_URL=ENC[AES256_GCM,data:amJ2d6JCFQebBwQhY/uZ7ztjkzTS3X2e5pGhs0wF6Bz9wCSJjrYaPHmMbgk=,iv:0HUYjfe1xDBehq6mln0cwCek5Jebb5eABGbQeo5p7Yg=,tag:gfab6jNP2JIe5LyzH5S24A==,type:str] -DATACITE_DEPOSIT_URL=ENC[AES256_GCM,data:uItgPlYGb2CcR19FaweKJipS57xc6SN+nDasYPqirKQskQ==,iv:BCpQUB7Xwf27F06PlauCN3at+1TtslywCUQisrHIw9Q=,tag:7lA/jAwSP8jdhK4mjsmGcg==,type:str] -DOI_LOGIN_ID=ENC[AES256_GCM,data:+A9GJT3V,iv:tjfng+W8VYYSUohlEJPK4tW53Nt2u1pCqp2wsTwdQMk=,tag:TiaGIR16TIVv0kBOFAXZfg==,type:str] -DOI_LOGIN_PASSWORD=ENC[AES256_GCM,data:ST3ZUeTsDaqWkVBC1Oy2L3feGyg=,iv:ytcKZNuuQgKP4jNtPvK9BUsfwteB1iT/wM0iaDRIcN0=,tag:wWTJ22095Sb5piJGBF5xBA==,type:str] -DOI_SUBMISSION_URL=ENC[AES256_GCM,data:TARvuD9fMh0u6aH5sqc253v2ISGjl1qvuINCi/Qv829c0yrMN/MU+Vs=,iv:eZx7cCDbVu2LYauCiso9ujWrKGfygY1Sxrlr6KhJcAk=,tag:r3hIEN/yvlVGYgNPJGcKIQ==,type:str] -FASTLY_PURGE_TOKEN=ENC[AES256_GCM,data:OE+T/oDodxAvJlPw1tvZtdR1zl97IsFGV9pVFeCuI64=,iv:zylNicWvtko5REScZmOhAED1ip2tOslz0ShnDI6zeDs=,tag:uXKxG3fU0HdpFaZVSH49tQ==,type:str] -FASTLY_SERVICE_ID=ENC[AES256_GCM,data:3J9QKEXwJ9kbpTqAMvolnOcNVDWUvw==,iv:z/KatzFraDYg7UzWuuaG5Ip8dVHFRLfFnP+m3DA90UY=,tag:gu5c/Hzqe3y3qxM+3DALaQ==,type:str] -FIREBASE_SERVICE_ACCOUNT_BASE64=ENC[AES256_GCM,data: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,iv:4G3yDw2z0krSwefzgPTmbIrLiPKdw8Ahbyj4fCv6iLQ=,tag:agrJ5RrxRwGkUpRhDkMkrg==,type:str] -IS_DUQDUQ=ENC[AES256_GCM,data:lwH36A==,iv:WHgpJJX2qdo5iXJ52FGy1qFqTdUSjGxJLqnDqkCPR8U=,tag:MtUO7dZvFBk/CxMEONiYPQ==,type:str] -JWT_SIGNING_SECRET=ENC[AES256_GCM,data:lkkFBBKZtLWOVhQm1uQ7Trnz52XRF0JTRbb6bfX1EpwhiEJmz8WgRJlqc7Cs++xkTz4OxrDS3p86GOK6fZ/BO2rGKr/va4tSx3SrGkK+bSruTyzUC+chnb2g2mBfWPOjAYo87Wj8Acy4gwdzcNnXfB8l/poY+ao+DpT/vZw+TGR0h6mvNzWEwgtxxbvlJiirmJSqv4BvJMVwBOhWq0BiMcOqwtjxGPBs5HiP3iebRZRwOaQ7j/ORLq4OM0La8RHl8SXLWBCU0Ttjho5Q3luWHW5Wq8fY8HynpZadwdD1WBpDVz+VdqvVddTUySoyaoG5gO+YNXIvErAuTP/Sm6lk5Kr4qUOGZvflT5l8WF8un52AARmlDFwhrj5eQUbDy+7sDZ7yFmbJADrP4yu/yvVO93P2BTk+9Ku9kxRcOmDo5BPjh9uA0ZBtcXtXkLFf/UOogxbVZYWOi5rP0cz9B36YvwMTG23sbKppaPRr6uBZbdGiCsMtK4SFLOkdXDQf5MnQ2OstcS68Jj5Eg7SUvpZtc3tE5YX61PBDNmvEAzWJKubC5dubswAARPXuGC4NatIO6Wl8AZ4jC9CTYZPyTd1UsTX5UUeMk5Lnngfxfskf21XqalX8EXqvKZCuXsUIzInFrF98gjklwQJZhgW0IzuiX9qW6SY58QVuHLrdlWyVCSjarWArqurIJYZLThauCBa1yOLbl2N1T3IG8ZV39McYdEgpKR/zopxHT9rf1af4twgzUqSGMA2SU91GpHG/4hLFvsSuY6pYhgiIWD2b50RDn0J4CmwsNmLt0WROdihOeOdCB9rdf4RdIdIXgt59eszPQpu80hwM85QVTV3vnRwUlgH7Wq7wsiIG1qD8b/vS9KHkFzl5fqC/v6f7XiDfGXqVudsbJQZFMmKjSYTqblgpdpI3grfczdlPhpHiq5394OZSKxOJGcMcxBltQhqQvI41RsZISnZN2Wm0LMU2dS8wb9Fb9fVnzXsUt4KwSVybT63+F43ArBWMdMnWHfk30rTSyiVHJ67DpxRMxMhUC2ZvSYIaV0UmEqv3ftcmcgoFN+oUGfOCzqBdxfLrHkZXQxt1C/kQStLXWgL/DSghrR1LZV1g1mE4WGR9Ez10eY9mq9PD1xO/gOSP30/dB/cbRGefahyZJ4aJmZByO7xvc/d+z17rvLAw0pgAG4mfUKSrZ6dlVLUgCAFiLJyT819ISuJcoe+LyyLhNGbm1Xube2jMEIBgAIHpDXP/7og8emqJE1SgQI72rWFon6wT+im3RnwZZrmF/djgo/fyW6cR9uW0/TI6K7e9BLrBtMtEeJyMwETjR1iriPscfJj7/q3l9PMgwnVs+9T8oNtN92tbK2fx0g==,iv:yAOn75fpabKTBerH+rXWD3t0LhgamgFd6p/lOOInWFo=,tag:koKa+B8QcVZucXsL2sx8BA==,type:str] -MAILCHIMP_API_KEY=ENC[AES256_GCM,data:8vEFhiBFhWB91LeLalAuG2Vt7GoBswHIUr4nnd6EfyXy7GK1,iv:kCpmyFyWxwuWQeelc0axeCHOZ89qv/AcK4VBhURJjC8=,tag:8NHa3TUDNWWC+UAMstKpjw==,type:str] -MAILGUN_API_KEY=ENC[AES256_GCM,data:YoI2DxZpTJ95Wrlf8fR78nM04Yy839v2cGSInXvlh6APaDKG,iv:x7z8HHT+saUegym+Px1wNPmx7ug8VRxcv4XsuihqFyE=,tag:M/LnaXh/sJ60IOuP5tmZ7g==,type:str] -METABASE_SECRET_KEY=ENC[AES256_GCM,data:qfU32TvQB1Q8RHPMex6og8bN2j6JPNatsgzv22iFaIu83xAtshMvp8Q7BaNuSwfBwShxc1fIgIxLJllc2L2NSg==,iv:r1ZB4HV1xiLcLF7wMCbN3BQ8yncMyaMQuobhpV/ruug=,tag:Gj97jl+P1iyEb3utG6ovDQ==,type:str] -NODE_ENV=ENC[AES256_GCM,data:x5s7Dz+7SjlKog==,iv:Deo1oawK6hF6qhCANL73lJR+e72Yoth8exoNw/DoObM=,tag:AewO8DutFe795MdMCFufFg==,type:str] -S3_BACKUP_ACCESS_KEY=ENC[AES256_GCM,data:zvA3GfI2nCVT9fBRSLUPRZkf5/8=,iv:jHnN4we/rOGiBSXS8BsrvifWcUdlrgJqMmCjUnseQ5U=,tag:O0TNFcxCoGajXujEDnLcrQ==,type:str] -S3_BACKUP_BUCKET=ENC[AES256_GCM,data:Lj3dJBNZRSeHqUQ=,iv:RGfg+GIG8wnL2O5eRxChKGmZJSTVXSHYWwZ+Zn0EDdc=,tag:jVbfqi3xuj9i6bLqla2LIw==,type:str] -S3_BACKUP_ENDPOINT=ENC[AES256_GCM,data:1OrAI1iKlYgehYedsVEmaY0h0xIKIBlyQiATFdHR7wmM2Ac=,iv:RVLgTyh9yS6flHbwyLt84VMos3XVE69bnqAmkOaUDo4=,tag:bbTLu0hfzWa5ozB2ojzuSQ==,type:str] -S3_BACKUP_SECRET_KEY=ENC[AES256_GCM,data:TrEnluwtYjnpUUs3owZagtUyU8mB+pvaYfSTg+HHk2aOPADRLimhqQ==,iv:/sO28OOD8sHXCHSXCcSSiC+stN80ZRKGTh4TSINCYbs=,tag:alryeRrfX3GjomuGnQVT8Q==,type:str] -SENTRY_AUTH_TOKEN=ENC[AES256_GCM,data:KfuH9pQ3b0Ojfx3wwFR0nj9wrlKc2OiFTiZG+ooanyxljSheCWMTgXpnhspF/wggc4+D1QE9c5H9al4LHG1SI25/u6/zprzFIifMhw9Fy0j4grJDRJLZ6ugk7Lvvxzh9jWCj2Nx7wnUGgorIjFY9wb1yn38NZYF+qnbLkte+U4yc8ujgZWVGwh1WpAW+9YCPcTNTRAfbGGYVcCaytroeRGfZURygv9y0L7wxhZiGzv1DIRgpfko0OPgb1g==,iv:OH3Xq3NiRHtI+sPf39SV8JBIWhBZPAvpL2aLu8ao0PE=,tag:htgiLoAy/C/96+KteX96wg==,type:str] -SENTRY_ORG=ENC[AES256_GCM,data:2MWR,iv:l22j8tSl0rPPq9+fZUjoU9eBykNMlsUM8zNBimA6bAc=,tag:0JBd/P3mVFQM1xsJ3Qp3GA==,type:str] -SEQUELIZE_MAX_CONNECTIONS=ENC[AES256_GCM,data:ono=,iv:weVapfSe7tLG0D/c/iHKHBKt8asLOTe3h4j07yp2PgA=,tag:ltuz7Qzpa+r9dw8dJdOSPA==,type:str] -SLACK_WEBHOOK_URL=ENC[AES256_GCM,data:twqzbW2Aq0DwwBsy7ccprRigsYFoJMQClh8qGozdHR5DiiIh/t0fS59qepp2uioLwt6F2SRBBY7tfyTxTBbyFglgEC4l/EBLKO1dlLYmjg==,iv:ns0rz9G//qFgDYclf0RypoXSc4w67j7hllght9JTqBk=,tag:Afev8kxv80trQl9bI9bK1Q==,type:str] -SMTP_HOST=ENC[AES256_GCM,data:i9JaAqu42uWPjO+PhqvHSUzvii/7X0xEjyPpvtXpJGN4Lg==,iv:b3OhtgljEsYQfeQUD4a5K7/IFP7aw3HkCWU6ecnU5fg=,tag:+BkFjOJxuEM2Bj+UKHyzrg==,type:str] -SMTP_PASS=ENC[AES256_GCM,data:pIalNdOXcThauiS7VjOXBhgh48XVN0t/WdQuzezqQV/GsUBDqw1IIKtTOK8=,iv:t/NQPiEkIRt8haHYtEd738agqK7TT3Y3kS7+ISNtAN8=,tag:b3EUgJ4dqMGFOnkMi/NfgQ==,type:str] -SMTP_USER=ENC[AES256_GCM,data:wjIZY8In97sSOoXL15RFmRndekk=,iv:2WivLoRaMYCVor6Yb6J6ttHOBuJdM9HfOEWQPNc/uTY=,tag:nMH1aF7seRXJlIUlvZA8Wg==,type:str] -STITCH_WEBHOOK_URL=ENC[AES256_GCM,data:oE4ZuuVMgAeKcRfo028sOiasF3bsAGirLc+yUbxJySYwUHVXuKO8t9cOVaIGJmEsqpPz4JUdJCR7rVPiQzqLFNNSb2FCtNsYfuykg8z1zLuVeaNtylDM+ZDoD1+S7GmFyNGi0Kgu2sxzp4cZ9rduom9lbVDL,iv:htNYpPakrOK3PuOtz5UHukwcLTWBOTx8BM3xNrGo80w=,tag:vu9G0ItpNlaMDLvj/CPtcg==,type:str] -ZOTERO_CLIENT_KEY=ENC[AES256_GCM,data:j1a+M1C25ICgDPUBjDQTOR4ki3s=,iv:6fEZGn06BUMWB5pkfY+vSJRB+5AeRgZph5oAForEGPw=,tag:BbzVz67rpGbEfa29ZCXcIA==,type:str] -ZOTERO_CLIENT_SECRET=ENC[AES256_GCM,data:/Lh6A+47J2BXPtY+t215Jqv78pM=,iv:LTHXNsQkiS+bSldQCR8zewXeuy0+8C7QR6gtwyDcJaQ=,tag:EZQb764ROd4g68NFyZrjOQ==,type:str] -#ENC[AES256_GCM,data:shmP594h1c5CnReR0m1U3gpHDXUGM2V2AI6fDCHoFnoT4loL3LwZGVVa9UMKsBPF4nTiauIf8skWq/CBKIOn9C4D,iv:O3UN8uCG/5UiYMAHsC7HC+s/OOp2H6re0O0ctqUFhvY=,tag:exMjR/xbxO2ME3jPZsRu/w==,type:comment] -AM_REDSHIFT_PATH=ENC[AES256_GCM,data:dWUPc2D2bJO3nYJDyurckEfW2BWZj55OWt4ulYG0QzQIfOfmg6BvcALI4TVsOTnW9Q/xgnLbZIHrUY1702E=,iv:7arLiemNsZwqivC3WMiJJ40iXNqDVsh/RwbBsW0NdKw=,tag:ZooblXyzzbS2EyGH7bu26A==,type:str] -AM_REDSHIFT_BACKUP_ACCESS_KEY=ENC[AES256_GCM,data:ZSqqr/nG3JjIdU4VUmr2H3xhXB8=,iv:FJfbLg0luUYazTqCFdPd9nKOOcKtTczrkesEc8UguLo=,tag:ACkaN7f7cCZ68v9Ck/MQ9A==,type:str] -AM_REDSHIFT_BACKUP_SECRET_KEY=ENC[AES256_GCM,data:VOi9wl4sQBibExS9p7dGn4xDWoZhuN+lXKbiP1+whubywgYCgYnqPw==,iv:qr+gBQjYzFs8+rQ/o26joLJHvTppNQA1o8trefHjAKU=,tag:NIcPShYCuyIIv7LemMu6pw==,type:str] -AM_REDSHIFT_BACKUP_ROLE_ARN=ENC[AES256_GCM,data:6eWGzdyhr+7SyWN67LTyJRWt6/XCVEF1W6+PDda1pX1ezHwkG1O/7gy45E62ptJLrv3+,iv:C+WZbmJZKM9KMdg+r2gZLNYVPuqWKxakEhuPgPXWrU0=,tag:KnR0iy67GTj/UtYfnbS6lg==,type:str] -#ENC[AES256_GCM,data:OPzgcUPQDarEoyZc9rfCgp8bOH7dr8dJapJWWw+6Yw1LzYU3bm6HGaAZGHou/uzXpSaE0tFNimaCLe9UB2VgLJM365uUZBNk3dbCH02JJGIFj4cJf/LHZ2ireg/jX0QgTSc68Kny+6lQn5jggEG7NeSbdybpUDbn,iv:590Qf8RimA3o0AvL6T4ztVsxgL1YEe2cto7hLHJzTDY=,tag:8Q1MQkUlt03ZfmStiJRliQ==,type:comment] -#ENC[AES256_GCM,data:IsDe8Ca2kQF7CbNgdIzh4BZWv7JPSnqyQHqjRt+mgj8g3zx7kMBc81xMSMzMlBHJLWs=,iv:gczcuNyKDDk8FS2llCdFm7PJxdeA6CcXXANnjx5a6p4=,tag:hJAOY8lSQoB3e51OGG/MwA==,type:comment] -#ENC[AES256_GCM,data:vtcU22QZkdqkoC7mCgJMmHSrB6MclkvPT6Rx1jBZ2W6Dfpz3nQlmPnqajHeL,iv:NofVcQTZBaky/TeeY7YK0rl3m85pQC5JTM534CocC9o=,tag:qicq6/4A3gHZnrIjjKvTuQ==,type:comment] -#ENC[AES256_GCM,data:wWvrc4R3qrOhzyaMZZdub4lnh73FydM=,iv:F5M0afhjSt4RcMVa5k6VaaWXGH+yXKwhUEbhH+L3gAw=,tag:2Y63Xh/pd999lZrip9Xvaw==,type:comment] -#ENC[AES256_GCM,data:zN4hrA34ka8RoFJu3XbOBJ6B+Vod33c=,iv:HCxBKJOoQOii1bA4xu2TvJWt+QLuZq408tWTO9mle+0=,tag:IVKxYJGxx3BeASs/98SR6g==,type:comment] -#ENC[AES256_GCM,data:gMZWS4Ozc9306qBc6gpCeIrCibDKa8SiyvxD9gB2bUj16Mn0y+EPuk+jBtJzyaiW0T6YZ+WODrgR0g7V77Ecqq6xQ1P+hsMi57GvQg==,iv:Cyp3g4Q5tkxgJaMZioDJyI2yBp3IHZrj3HVsfR8Tjfo=,tag:ljPs0RKe7ZRtOLI1lHMNpg==,type:comment] -#ENC[AES256_GCM,data:ZeDp9r07+f9pVpzAQgMHwgEIqho=,iv:Qt5gxxRToOU8oh0nkXGYpIDJMKe/aqIajchH+f/2P7Y=,tag:VzAC6h6/1d0hjRH/if6quA==,type:comment] -#ENC[AES256_GCM,data:akUbOuJaQDHLdwOBxNonQ1yOiQ90urU=,iv:op5jFbAZJHRuIM3u9oD/T/ovxemhDvWKUiOO0FvW4zY=,tag:8GexIZMmjDFj0arh+qdj6A==,type:comment] -#ENC[AES256_GCM,data:hedGehtv6rJvRtoSvrO+qg==,iv:CEp6sQXiczQboIEzoJg0xb3yqIy6oylVfYOPNmYhYU0=,tag:lGSmt3p4e3j56cmQ00V5eg==,type:comment] -sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyRUZNbU1YLytkdWhWdXB1\nUTRkTktERnM4VmRyRGZDMENuT0NXSDVjZWdRCnlvVk1HNHlFdjM5UE9uK1AzRGJV\nZEp0L2hDRTJpLzJSOWcvVmRwZm1tZzQKLS0tIGZuZXdRdVNoaFh4ZU13ZHRrdEZp\nL09KS3c2eGJqallsRUF2RHVXY0F3WEUKliSIrRYs0MKqiYR6PlFvy9H6cPQB4J5F\ngMSnG0h3/uFdd07KE7aB3OXepS/wYANYuQ5FDsvsAFaENJxNinttVg==\n-----END AGE ENCRYPTED FILE-----\n +AES_ENCRYPTION_KEY=ENC[AES256_GCM,data:5eyDi/Myfa7q19on2U+U8XNbF6eloh0rbZMtfwYRu/fUHzBRfunpIFyAJjqKEe443XCv7ANlAQVaUrGtqKbPfg==,iv:mbuM3PXrDJk9jYYVZ82yyt4VfOJjkIr4TjGVenMZ1IQ=,tag:4qDbbmzXHm7IpvZE7QgZbw==,type:str] +ALTCHA_HMAC_KEY=ENC[AES256_GCM,data:gpvshJLtqnvmnZNd1HgN7kGgzdo9b1yrBybewm/CYlD7/54ByT9ARllz8iIad2PXqd7nCDYJ1TFKoGsU42Pllg==,iv:02i8cUEPqligZHGGhdAew1epHQc9FhGH02GxN0x+i/Y=,tag:P0keAQ2kNQScpVNb/9bVTw==,type:str] +AWS_ACCESS_KEY_ID=ENC[AES256_GCM,data:v6AmSYarSSJ/QwoB9Cq+OrwGWUM=,iv:1ru9JLuSFSk/6SFMF5/3PhLIDt0LsRbA6HhnYIykiac=,tag:grAfeyoz8dywKKv84dgzLA==,type:str] +AWS_BACKUP_ACCESS_KEY_ID=ENC[AES256_GCM,data:xGk4iFeO4KorkxsP674ZWourvwI=,iv:8IiCFw8EdsHk7/1TFETKn2IinJ5RA099s9CJzARolmU=,tag:1OU1CdFXDH8cp0tvLxqWZQ==,type:str] +AWS_BACKUP_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:qt/N7sKLAIE8KRR29vlm7Mi0qmOW2DQWch8QyPqQrIYtrxxT9flfFA==,iv:go5VTEqCmPFUVgDRXM1cGp/9kz1uSkDAq8yS7xqo6JE=,tag:+DJ3HZtDYMA2qQK/7Juy9w==,type:str] +AWS_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:9pFbGNBVUQiG7bTZCNT/Xd7e9+RASXy79UOnRDBo6/xZQVuNVlmGYg==,iv:auiG0V2rbbu1nFYfxsaYZDPtILwIdbdQzeLLv7q9k28=,tag:r+PmpYOaRkyScHp18jrz9g==,type:str] +BACKUPS_SECRET=ENC[AES256_GCM,data:BcHyxdLNVppp2Vc4HDsXxII1ZdcgEeg5oyWMHzX9u/g+7IzGSjITmGsSzNk=,iv:gr1GzIAdX6JX2RlK/7TCpVmod525y+Gi3TslZF+uBs8=,tag:CbLGAuiEUJEc16/9Uhc5GQ==,type:str] +CLOUDFLARE_ANALYTICS_API_TOKEN=ENC[AES256_GCM,data:KHANxAiDEE8BZbDnv1FNQ9lfI+AqMrYZJZJsFeMhU5bKjOs/zkoc9Ds2bNuE/BTZTfZSZd4=,iv:dSzStP9ZoRC8/SYY5m4AsrC/H0JjNMchSUcVJNTajwI=,tag:gwYFHRpWFWkXwC4y9a7g8Q==,type:str] +CLOUDFLARE_ZONE_TAG=ENC[AES256_GCM,data:qhYr8I36kKSraigQFuNQx0/VyOoPzmI/D36C3bXYZHU=,iv:qBimYbEs4zSGoWSpFyO5Y3wuzRUn3nm+9GSa8vBmGOM=,tag:e046eKwARv/VQzFl1BKC6A==,type:str] +DATABASE_URL=ENC[AES256_GCM,data:o9eOXVCal19zHU3V04s6gUVxn4C28mIsI7/ngXN4aTMOlcZjj6LC32xGfgA=,iv:UTGQQw53MippQgD8IJ8rf59NP0dTbjX2fFKbxkQRrq8=,tag:XXTm5asgQ3/JxDoAV3Ng5Q==,type:str] +DATACITE_DEPOSIT_URL=ENC[AES256_GCM,data:soJK4pbSvf3bzo7VykcvtuAS6oAYaK13ryCr2gfksv6/DQ==,iv:tJgM5UXWmSuUYCez90AFs3yzhu2dCe0IkIVr2CJgpEw=,tag:CeAQ4phfquSP30UCdfTY1Q==,type:str] +DOI_LOGIN_ID=ENC[AES256_GCM,data:rf3UyRir,iv:18x/f+alZIWOFsMXrLbSyX49jwKYONeIzPfTVjtcq5U=,tag:ROiKotBrg/lRYIfRkUSBSQ==,type:str] +DOI_LOGIN_PASSWORD=ENC[AES256_GCM,data:lwQQ/SMMjyMMezt0Uc7mti9olvc=,iv:L0ePwKGExDsrGPXZLFpIEVUTRhIT4E7qYs9G1mfB+c0=,tag:a3k5ob7ZemE2KgG3SsMewA==,type:str] +DOI_SUBMISSION_URL=ENC[AES256_GCM,data:s0+NigZlzcnDzK4GNuPRggKphb5Dt9WblDy556lmiXHTlsXrTWK7cbk=,iv:+VGyNAykFouk2TsR35Etji3ETRju9e73iUlBMVzzq7w=,tag:HwofrhPwuvqLqXuMLaTeqA==,type:str] +FASTLY_PURGE_TOKEN=ENC[AES256_GCM,data:NAaGAbYiLA0XeKlKR2Vb/HJodfyaesQ3yQW8FxVUUuc=,iv:KaPGM5ENfgm9u7cPyG7MHbOPihaAnqqC0bgOQAGyWsM=,tag:k4fVBPnmgIvH4RwHEJlMOA==,type:str] +FASTLY_SERVICE_ID=ENC[AES256_GCM,data:mjm3s0ddmoFnhg2sZH0CWfHBNcOMPw==,iv:h2bs8bvnwQ55TBVFtEQmezm96Xvud3bUtEEuX77mTOA=,tag:Ti70N7kpVvCUwgeJzRHvFw==,type:str] +FIREBASE_SERVICE_ACCOUNT_BASE64=ENC[AES256_GCM,data:PfsZ4nJsOb0EPmGdgFMwg03VRF33GRdMtHv8b1DINL0vztrD/kIm0f1jCU7CNmBhSssPgD4Q+bBXRetCEDTgsRNL6iJH3pFJdDCRgJrfx7FoU8ymRNMW1CL7vEXFSJBR4Asd0K6f8aZb8TRBGXnDnNIya4N1AoUzE55HUMdfE8J9PJ+RBQaNU7qStdYGFa+voHriX2M4IXwwCjwdNg1pKNn5UZbI6xinnBdn9nQahQzOlJOCigBr7TbSjXF/pmfnNoECVSpyxdFAbHdAID71ozU8faPQE35VhOvNwp63Srv6Gf1g+U5gxt1WHTKw/CQeiUCGjQY1ssa2Ba5aLCEdluCSy0w83bc1kZhKFtnFVH0eTEUPI26Lexi1IVlZEe2akqSPPruGmv355j3iotnEcq+khrQKfVBoGKWRKmg0QMjeU6Ji2Xbby16jbp/QgvntUwxk2jaJehnUql+9HE8bOEheW19tupN/ZaCVOTs1nurTdO3nR1b/TUV/ib0cEP/uTYOTWEGcLJ1Oms+DHsuRecaMafglKcFuTO+Hu69fjcWkZi2QpXq8orPMDy2kTVJrapT47z7E7VcyVUAW/XPfWQPnpKL4OWer+LJxNlH/E50AnKWzZJqjjLsHk2aVfpq3q1sE9Z1B9DG9IguhibhNjuCds77XJiemqjjlDXM4NPXAhKNbTZ20DRZLL/ydPcORuMlA2FYHUK1x34wpdlWoVBQsYon9e6AKg3xqTyG87mbaI8oA/v0NA2cmmGjF655JYnN+E805V89ZNDYscvByzuKd2JyJbSOwcydUCyJaiyRzKG+K8lCsXAM9JLB+Jqlu7ZPWxv6P4wJV1gEz8FC3Lb3M2CMxq0UWgZesBc0qOTmWj9VKpdJCd25KljuB1oKI16DkMfc7bSgYEk+nGbMcNC8f9Gj4YYnReImuEHFMCwpSKkqBPpIlYoFk6dJ/6kI1G6eu7lXxvWusjGBU4k6MxZpefRmswlEhTd2biyBmT1+7brzyseSpUgnUap/ogPKKxiHFGLMeVbAF05egwAE8wowuRja8piLIVRw7PWRqaKvpQS3ma++ibAyOJ9PsD1ddEqOAZH/Sd3pukuLjTj7OUY8jLa8jIyUgLrke58K1Z4O9+kyQHRjs8Sg0oYav7psGn3ov5rDaFvRJuPKOXGb1mnDmE/5gE2AfXOc5rs7Qf0kMG6uknDBhImobsHeJGWrGvtCRBUtOVspZuZBSxC/Svn9ZpdyjU9PMI86eR8t+4cNavIF4ITNSye9CSTr8DwGOU3CgHMvR+WbBwg7qNa+YBXH/nSwnuwT7TdTIMB9WlR2JiG8p0GAAbncJbS9fpbdbvt9ypeGACFL/stRuirySnCl4AfOCP+tPBUrDvZ7SIPSGHqgyUNML1MYBCzpBVS0syBWJE7MZ0rfXRZZ2F2/+U1K2ulH3IQg7LwOh6DoYtn14gk1Q1u/jT9u8bO6GRbQdVttuPBZiESebGIV8nOifWIbEgcVNrXeirtoVubTULg3R3/GrxBoxmLenPD/ypfXDivf+F6nC7VzA0u0ECJeTDD9inS1/sFC3OuWNq35KnBUDlbJo1nqZ6ZtuFKR5ob8bpBcLkDDhSx9RgOI17Tt4z/GwhZpqIhhTrJp4ppEY3PnRhDdu9EssHD3TE4reKtZnt9mIfAvKrfcJ/ewF8OvrpSc0nqT9UNkzk6J+F5qzTydwY671bVEBpeozUYkyHf3SPEySm8FYX/8SughYxL4B4mwX5nn9tlNMJl1fyjfcx8ZNHKg312hGli5adKfjocS8sKC7sc8WTbK6wMmvfGsMqd/yk/Y+3kudY/pgsQIIe3seAjN5QMLyMrMaXILldGfIgspSBWTODJfieHhUaE5Ag25N7wygv+atvEGuQOnvvoSUKadAUQvr0Q6HKocRBFcR4nN6GNaFxSlsewejMhS4xDgY1bamq7RhZG+wgeo1PiJpaEC3oFemxXgwt2X0sb5A3dGkf+k8X89Ux5BhHMTHRCVNSqSdqzg9NoWpQtAIPypGVfdGk7mVmH+r2/oKA3j8RUOHLjx82fjLRtL+HyXJwkXKlvvF+ON6fB3x4bYeMwXNzUIM/aWvvDlPDm5oRvO7vXD3OcFCyE5u5pku2Q5q5s5ckZgzb1VgxgbAeI+E/t4JUkmdOiIUAYQ/IRuJnrfZZY7UarxJdehk/K7nHaTXGg8cRWU7EY7XGYbrYiuck/XgqaAhjaVL1QIDd/IAmnOtVl+P1f6caO4F7JYkxfHk8zYFNg6YdNlc+7gHfTknRR83f2Wj1DuiWAbJpDjfkQaOHeKjLEIAnm5QlnInfElTeZ93O76QMxSSbzHuKA1oB2iyZcjc0xzKsPqMYCUMnCbmWiaU+fy9pXK7i2R2nomeZIKW8vFqYdsGcOzGufhmlI2vWm4v29qZ2tBlFHP0MhZoD//D9PDH7yRKLv/HHQsv9b0as+RBqBmdqcNhh1gWwh+EfQYqX9qnmNgehylqyd/Kw2CSyU7EpIICMCag223/m/N6iqFsBGZ6JvdNgTVOkfCY8g8UFw0ONcAawbVKJ4aVkZb+AxtNcqTfVDTDebD76wBIA8gdMJlN2fdAl/Ma8DHLwm8mH3ejQ8ELdIYCUJVLJz4ufMAMrmAWqQHYu+u7NkmE7dS3KiH6zrh6Xotp7GzDj8YSSbUbI1Tpn6nirv+0cQnisM+smBVR/XsTwk3LGy0VJuUGTLjMRTN8bZQQ3Bcdeo4nau1uBHdkSIjPMtQZ5Bjd7fKdoQX0KBt+EsELxHNokqh75OcKKRnoqxx+D/OKw+Cv/cL2yUhBNajjbk+pl+YoUV4dkrsa7aguFSDTMI54d3SeUQbIwsvLVuUhzyrA41IohKJgPPqL8vuYbSaGFI9cMCIxUKaazWA9EKKEETwsxWBVVzuLChKx+P/HldE8JWok1JRZtQFM49/4bv1gf6BlNsPtHQaJibTH4iFtXPq9sIbnaQbQXgAKF05FPiDca6+S9H22YpM5erd0/MydQU/85laFsUB/VviSg2jGS+8qz9iAuSS15uxSSDI46vTzoi3u8Pf8jXOYljvf0Fhm6CVYhDTXmhPklOVO3dABQQccda0RfTECPWLTg5Bjh/LMFQjN4OK2SrrzkQY5D4gPXAhHvk/nT87oLMUDuEJejN/WP9n9SpPtMy/9ZXek5AeBZdzDbUlBwt2gNzc4gNI/PRs75I6CPi5HYY6uuaKZfnItxQM1JKjvt8MX9Qvpee92NeXFkpRHjnc9kegEryrEHkwb06EUoEAnviUpdmI/xm/9OxPn98qaIK427KlPIo6fooreIx/zpJ3TRGIMZfcwiTS5n1PTjC/C6RgtpIaGaOxRvdZe+hh79RCquqYx4HxXOVsMud84oTsmaRc9BePVQXoxetxMcVZyDE8oRgNhtlefald/OSNlG00f6JeEo2q7beaZrH0X1OK0eDgwS9UiV48fEMUt3LN8g85aJH0I5R4BzjF33hriO1L/X0RKucDB5ljqe8hlJmfbw8M6gDZqHAsUErRsQtG6o5lDP+RQ5nkvLZIkx+/f9LeRp3YOAAMWgnAVlnIvL/Rfka53/Cz53fkJbgujdvntmZZalfL3GVsw+nVKNdscaj5BWI7QyahptIr2DyLgPqYaHm3bovY/fhfiAWZ7tNhRYMz12A5tK8hWt864tIp02FtjfXqW6OCbYDB4TeWLnG9tshqq5Lr/Q135k5wgbPvCx3PPN+purEMAN0V8nD2GBP1JkxVmta3JELeefiRNR5wEyfCw4hYJ+0NalXvLWoqd8spxQneQBbMAW056aTT/eqMfjnGYtQ5l0Tz7DKshMUHTsUVOGeJrpkQkVqX3yVckB9dxzeX/XV4J1muMy7PjDkh5okI0nLtqa/WnkrLzz2Hqp0HxBgHu9t9JzUGPapECFSOh31K3+c1yscg97MKsbOTt5rjGunqSqXRok80osnQhqNNrquNotLce0GJ6ktEU4GbrHYbPZ5zYhg/UwIvwUvIvE260lZ4H34F+mbR7Xu76q+sRKjgHlvNuiIxKdDfwW8ZS7FttuwuKzPJn1PF7y4/sYOXql7hXkJddnlJBJ2bkABV9lWR6IEg8eA28Tt3WW5jp9mzhP905iwV2+ozoqYRPMzvJbRk=,iv:ZMbFSIJMgsdS5ukJ0xH0M7+5vdIY126AiURYVbFwcxM=,tag:8F1wkY2LBWAXVvB8d+cp5w==,type:str] +IS_DUQDUQ=ENC[AES256_GCM,data:vrgaiA==,iv:/Q93Zmw+9OFLhKw80jpRW2k85HEbvwNW/jr307NzAIo=,tag:Ttfrqux2N2QkPL8LufgnCA==,type:str] +JWT_SIGNING_SECRET=ENC[AES256_GCM,data:HGFZGnfRwCp/FuPum2NvfdDevtv87lajLXzBhAZmR3iMZckiSt7wwo0qda6khb9kJr1Ex8H+kagKkFA/nbkm0WXYzuuwIZWO8goT36UeGQ/SAm50hwWospg94lA41mdrrBb3MVvgCBvMX+BIypO8Mwuzp/iM/GiOaBB62GUVPAFXMx+R/Pr+9e/M5hKRm9rlSEJD3fuIlsqTzoIFnYppe3pS2HAHgukzUdfp7VDvVJcGPyx3eZgRJv/nHiERxxn5SDZaUqIh8KlXYJ0t+wsVphGIIWLUlmIf8rFVi+lvwUDiL6r3eIvcUZht/k5LFCqEL9bw4ga9xIyqZqUxkU9qgV9YJpJf3i1MYOQcgKC35gV/zCxSdeuVzvB4bMpGuOMvFT3g7aFWJRWBB+pt/7iKmWVksTEcDniQag4/8AkICfxb76xoAalI5AX88Oem7RX3trl8QmY977ZpcwwVRoR4px00s2zYG+y1fVs/SctSH8JOPTLBgGG7gZmzN8J3Z5QcZpvQSCjcmBfbSnU2uBe2RstdAJflhtYu+UXfp1tSkpkVqOKqc7b8epE6HPpbJpQGKWcoczxnKIKtkb/PeM8/r9he7SiZP4Cu2bpHalj7u6ZT/pycr5XRE431pfoU5zMou/3HYsisKMxLHGnJ+g5s3mMCid7HaU9+iLODUMZyHZrH+TcWjYEFRR2SHDpDgQhdPrEuwofVv0x6FzhbVy0qZ2zxvcNP/XHnAnoPc1a/JJlbWsCWTqu6PoyWzqC1rlQRBf8fzcNkoj6K4GfqbzdCfkcuY50xlTGR7EhCdq5u9/JdxfHzKaGOuUKXcePZ3YFYiSFzHfYbpgmbFLPpotTXEP0HXm6cXixcwxT/OIzPDRk9UE5ZLBcQFmwwnu66sOQFZVdhWsv4HOJ7e6MLxqzqrOiToURSfRspK72rejnNBBQr/EmtriDDBcK7FrTU/4d58lUapbkGo2HkLN340oKB59NCN8VFxoe3CU6qRx/kLqTWP8kSdzo95b09XsWUs+n2YuQEtpFo0ISs4oflDazomKrZ1rERfXeBHhfyWIxN4kpdfGXR2rqQKTJFylJZXnjH0fewm85bpfAcXEmBUQnjJ3O7p+CPxN11aemfNfcb9AUo2okrz4X5ro7j2Y5yndz58bXWZJ+xCvJ7O/3dCU8VlFwNCvmiJvp0E9qWPrhhEdeKpWpq5UEZJ1hJ/BqMwFZFtU87mvh1Btc9dJWR2hx5uBljT5N1MsVrjfvNljfnG2dK5pI2uiGPltvkr9iOjoU/N/m2/IvNUnHcqhkaByPwS/SzWOwBdGihrtTVMjRuS5K9RYGK8Wd1ai7RPb1tlm+2tx/S5P7K/99dkPjV39ugQw==,iv:H9O1joJI/c3pOY8aKj3zylpUMjGz948pfXEbOgVNzDY=,tag:Bp+zaMSpTHGQ/Omx1oXW9A==,type:str] +MAILCHIMP_API_KEY=ENC[AES256_GCM,data:bIdbCaFRlvf4BJRl5gWl5VuUmf8PE9RE8/VrTS33XJYtg/pW,iv:JKTWK1r4J3tmIclmloGQ5/xQtOL+///2XUrBLzh/ybg=,tag:cw1VFAJJ3AmoqrnC5S54WQ==,type:str] +MAILGUN_API_KEY=ENC[AES256_GCM,data:gSW36ExFYP3C8TvgGCDo48NKpqfXL99YPdX1MzQdPZ/m8z59,iv:YyAWThMx9cE1Bq6RnjPVRjDTRc+nwNo/EuEMCZBUs0w=,tag:NCh7HFyv3VHo65GYl1zZ3Q==,type:str] +NODE_ENV=ENC[AES256_GCM,data:wL3jvGP846k5zg==,iv:hI5mwOA9wJdVxfoOhS9TOn96rltiUOl+NcxVmNHHxLs=,tag:HrDutSjXn7BSGHhVGNqUzw==,type:str] +S3_BACKUP_ACCESS_KEY=ENC[AES256_GCM,data:7PMX/J9BOfT1dD02nJ/tnwT3FwM=,iv:tBfqJrcxsrqxXZK27Nx04xeywuhzjT8ghFbAMFBeb7o=,tag:GqJp7po3qiBVAWkDpVy92Q==,type:str] +S3_BACKUP_BUCKET=ENC[AES256_GCM,data:azA2VCVLOZ6/vT0=,iv:IrawaI8heTtUgx8eIU+H+BO/IPP6WwvBdfJIlhoRn+U=,tag:kaBRbvFSTCcul6G5djojDA==,type:str] +S3_BACKUP_ENDPOINT=ENC[AES256_GCM,data:PJeAH6RB+OGij/y4PwdQMUd7Bvxn6mNUWlLC/7K4jNONWm8=,iv:UHISDvi0/q3hDXDmy+RGzNl4lroFYQEjfzirkgy4rhk=,tag:frYiSu9Pzxwd9YQ82VwYvg==,type:str] +S3_BACKUP_SECRET_KEY=ENC[AES256_GCM,data:tUBUJ8Muh1L5SSP483DTvMXlYdXUwj1w6sCzzBZFKAltcfFiSjYKIA==,iv:9LImxXcK3yO9H5d4rAgDfGRYuusQiz1NpYbp5K7gmxo=,tag:yM7qrdBV89ekH/Wr020rNA==,type:str] +SENTRY_AUTH_TOKEN=ENC[AES256_GCM,data:3lyXoS35uuiaqNl5ki7RgnWd+7sqfcZFMLTyz0FsV/MrVJg9tNc7xO3pct+kqE92VA2n48DPawkmht7MUXVdbQFoDf3f03YTXSdzS9zW/hmz4EsIIBYFXwCS8SMBBvN0AhUA1iY0mxxJwHI391XqhA/KlBWBgT9U+7p9o4e3CbFHnzr0HXCNWsSaj7q57jw5SZY6X834xDEj/W8I5Geh5h7rApLiGRfphyBmv2TPyYKSiUvqzHfWyAC9gA==,iv:nqE3Ec+KB6BZIDxZYxQXYaYRQIaBuLI/1+Pw/HQMY8k=,tag:wIEHbSIJw4tuJorQFJHYWw==,type:str] +SENTRY_ORG=ENC[AES256_GCM,data:9wae,iv:zAYBAOSV6YndL1fHTpnzJRSNd2smchXr2+X1TcfYT5U=,tag:8FunKMIkdkuM2FHClP0Agw==,type:str] +SEQUELIZE_MAX_CONNECTIONS=ENC[AES256_GCM,data:yqA=,iv:pDw3+RZFGplJ/BVI4g9rXVRHboJlWxa/XGgvkmjZHg8=,tag:8W1lDrM4vctCcyL4jvmxzQ==,type:str] +SLACK_WEBHOOK_URL=ENC[AES256_GCM,data:UWQeslv/I4Hta9gUcl/St5lfhJILn+upilU+R8ZzP54n3yeXpLbJ8rWZj3LCW7HGtAgBQ9gyI6g/JptObmaWQ5/JWC+HumqHoQP6KTXNMA==,iv:4j91dvuBS+ndLP7chfS3C8GoQGibVyCaWBtVOlwWcu8=,tag:aWBAEzgrW0gC8JkzVI6usQ==,type:str] +SMTP_HOST=ENC[AES256_GCM,data:gdJ9fLB+1ovDtEa7CC5uLUrDMmzjo3dOJJ7emBILUqvV7w==,iv:7PYaJFbdrmGD8IO21hAV9KonG9tmTBHwgWWmPLEcGTE=,tag:ZMN4DKDYrldnLi7oyL76xg==,type:str] +SMTP_PASS=ENC[AES256_GCM,data:0ckvTx5mvRdBNvrPK774ZIpiXasXhzsN4euIHgmEUle/ERjFF12QGBBHtEs=,iv:soK5R2DIqXafHAANvDaWckmVPmMROPF9OFba/h+cheQ=,tag:V5EW51tBbCH/ts8ydcCO4g==,type:str] +SMTP_USER=ENC[AES256_GCM,data:/SlUXJHs732VIhgQRyWkLjnOEjw=,iv:n+VtKReE/BNAdUuGshOox+2+Qg0COPaJNP2gDHqBP6Y=,tag:/EQVVWnDzLHNp3C88J3nPw==,type:str] +STITCH_WEBHOOK_URL=ENC[AES256_GCM,data:9X11hmd/AdpbV3DY9TkoAJ087JFeRwE5razX0YqSAEK1tEW0wCs4JtPTkZhe3tjY4CKfwd/ewe/mHN2pcQ70fJFa7KjxgXQAB7SXHNSEGu1hnMiUmRSC+/lotGLzM5dLVPHHFvOIFw23vGtsCLDOHnx0loeq,iv:AfiWQdMtNAjK/FOhw8PLEGuE706nLUd0LaPDqiaETl0=,tag:fYfW5BzakZjSMA86ia+qfw==,type:str] +ZOTERO_CLIENT_KEY=ENC[AES256_GCM,data:M0avz1s8XqLKhQ2MiEYJAAtVVvg=,iv:ciPVBZhrcYjmDPmeLmh7KwQw+KSKfm8GZSQb1e6ORl0=,tag:YjnxV6lL4iF1xCgRejFoQw==,type:str] +ZOTERO_CLIENT_SECRET=ENC[AES256_GCM,data:5vvLxZLmC16bIHgnDOXFE339swg=,iv:xf4Y8q31drAHw0rK7EKgOY92YNOOG2c6ghUCM2Yqy4M=,tag:/jzgaEyBSkQDIn7QooZy0g==,type:str] +#ENC[AES256_GCM,data:Glh8xJ9H0D1OCrkkt0cQeREs8SzI7ORTo4kYhw31ICKhQE6Xkck0mBbXTP3i+Avvt/GwKuS8fAocqc94YWHgaSWw,iv:/WdIwRK+XXzGh9qx9k+vk6aqakNbE0q3rhlwxZ6SOEk=,tag:H9MMsT/1BVLpio6BdN6kOw==,type:comment] +AM_REDSHIFT_PATH=ENC[AES256_GCM,data:GAzWQNHKLQKXLj/x6saEQ7tfXHrappf4bA8b64oThSlwi+N3jd54ueNOkJ13oF3fyeMSIFvrLvv5RmV3oXo=,iv:3VxuYeTUwmNNXdJtr19xagJ+D54D+SAKB8YqC2eLefU=,tag:c+vqhrBra1uYzV07KUcYKQ==,type:str] +AM_REDSHIFT_BACKUP_ACCESS_KEY=ENC[AES256_GCM,data:DhMQW2EHl+zD8QmnSMQep+8KjQU=,iv:rNtkP+5TuFvQIoIOZU7rp5f8PL7Jt9oeei/yGYkDGJo=,tag:pzwNTAq2Z+7lXQ1tVXPKZQ==,type:str] +AM_REDSHIFT_BACKUP_SECRET_KEY=ENC[AES256_GCM,data:G/jyICHIx0RoZn06GuvODQt1D8r3Xgidftir/AMsMhoZMp78u2h60A==,iv:e/euz9+TTuly+AHiNq/JtZtvoEHUATo+nNxYldefiHE=,tag:1oHX3QE77YGvQuz/gHJfxg==,type:str] +AM_REDSHIFT_BACKUP_ROLE_ARN=ENC[AES256_GCM,data:W0h1+pLy4fXSFWcjPsUYUObmS7Am+0TJtIB6JxsD7BM8V4D5BxjV7RjHV72a0pjh5TTJ,iv:xkg3NvGsLIkRJw4+QMkIbLEe0wyaXtBF5gNrI9JbDXk=,tag:UjdbwEjk8C4Rbgttyu2Y8g==,type:str] +#ENC[AES256_GCM,data:sfaBAeP+CIgDUR4Hs0Dl3lM7+f/yzju5JWM42OXDW+OU2kIqVdzRgsd3yofjUiE1dw40Xl0puDJ6c8P8QBUMdZtmRHHGXQeKUQAZmDnYwhkTA1dcr3pmcWzAB71jrcToYqCuhPCF8fxIDU+PPrOuXqvhK8PBNBIB,iv:nw1mZz82gSWsAACdBfmHkeCsDRn+vtNuAOUJp/iAHSE=,tag:yOw4mmmAQ+jXp6r2hsccHg==,type:comment] +#ENC[AES256_GCM,data:KefvM8Y+fot81+cYN5OSwLKhECwJdTG3iH0sQLWjyjGdBOFX7P0RNDYgo+AJCAOCp84=,iv:HHB7+6b3lgQq2YV8/85dk5xhld7Iv84z5qx4hsGlRNM=,tag:bey/jyK5TsnkY1SINM1r4A==,type:comment] +#ENC[AES256_GCM,data:en4ObJjD9e5ba2QcMpTYJaOuBLMcOWab/2vtPYome7xNOeaaxVFlR/H61AMn,iv:Awa/aXRVfrA1ge74NHSPlduP8jVJiPui7Y1GpKIrVvo=,tag:r8jSBOAswVZGnQMEgv6OOQ==,type:comment] +#ENC[AES256_GCM,data:Cdiy5gRAhNls86RZoW4hV21bvqE0P0Q=,iv:x7VexSF9xb14MJBWu3ah5qurj3gh8h8OyEirjztUGtc=,tag:SPKulsaQrkwPEjHTprmgig==,type:comment] +#ENC[AES256_GCM,data:GtzZ4jSBx2S2RLiZBtr14TiyELioDag=,iv:MilEdQbbQY7mQdkKgNqoj/A7pAGy48/OYNUpJspMCI4=,tag:YQWadmUDatH6VayeLMn5kA==,type:comment] +#ENC[AES256_GCM,data:qlzl8cjwFY3axBGE4vya5QGrJMiaeD/sdwF47FbwYs1yqLzAwYs3esVo+LHo0V7tjAAHWQK2LgZY8yVOPO20stfCgnjvaSkXlkYVyQ==,iv:+GMcTvMAh3ASjOHigj7vw19YPRb2NgHOJpXjrLaCc9k=,tag:g3mxy6rv/+x3ieubukRqbA==,type:comment] +#ENC[AES256_GCM,data:C/LO8xOZ3eejZGFFCl5fALbybeM=,iv:pVhD8QfL03zy7iobXdcpx8ZcGKMI2+n1P3LSQ3cMaTk=,tag:E9XNyeLHDzIOcZb4QB2wXQ==,type:comment] +#ENC[AES256_GCM,data:WslQs/JvWXvr2dw69G5BCKqCAEVXWP8=,iv:tXKIr44mVkgA2yXUw+M+gJwsxuVILOKVdeOLKAyEidE=,tag:v0WHCSSbP9am176zN55lNA==,type:comment] +#ENC[AES256_GCM,data:g8ztn1xnp1K28imygQQv0Q==,iv:VErIm779nOI60syvZQdEz/EyvrEGeIuFWL5v0EA+sH0=,tag:6LQwXoMjocZCietJmehUsQ==,type:comment] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuWDVXTi9ZMHhJVzU0ZjY4\nNE9FV2tML1J0bGUvZ1Fsb05rZFB1ZXlRZ0RZCmhJa3F0YWl2S1VMUTVMbVplcDl2\neWNUNUZHRk5TWVRYWFN0dkVnZ0IwUFkKLS0tIGh6MDVKMHA3ZGhiMTVzZy94RXFB\nT3FYa2lFbzdFWC94elViREVoTUxBRTQKpn2HKKdgsKochtJpDU9BaSDIm0HjdOsU\nvgWrdkuEL6Dmb5PfnzQSkQLaJDvqKElaexM7OiqHCa/iCSQO/FtyOA==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_0__map_recipient=age1wravpjmed26772xfjhawmnsnc4933htapg6y5xseqml0jdv8z9hqemzhcr -sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSeHdrWmlLbDAyL29DSHhu\nRnNSOEFNR0FyQ080dTE3enpBRVlVYkwrSkd3CkkvSHQzc1pJWFZUUXpyM053NVJX\nYWtnV2NUZHZpblU2Ym5rQTYrZkRBYlkKLS0tIGppSzhUT1lINi9nYzBnbERHSUVC\nQjJrYTVqUUwvRU44cDl0dWc3cFpOaTAKxgbIs7bawmyi1IUAWTo82nz6DiugL8Ht\nmrX9IAoM+oEcd50gaC99PKveUX6Hm9YFt01wYjcFmJlD0p60exsx8Q==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHZ3NYQ3dyMjhrQ001a1lQ\nZjFqRElidzJsY0JSVjlSQzA0RWtIdEZWVm5JCkphenMzYUdpcE5WTHU2MlRnaTJo\ncnNzSSt6V09JMUt1SFovUXdYU3FlQWMKLS0tIExOWHRscjR1NEhEamVpV3o1MmZI\nNHlyajkxWVBLZXdJaTh1LzVwTVovaDgKKOnEkJQsG0qVE+TSCoYaNmJieUS6Sm3q\njh82taba3Vt/C7JxGeEgfVKOThWGFFWcUiRFWOM4rzEqMVjhSd9wZg==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_1__map_recipient=age1vhftscteyrwphx0jpp0yl60xxrjs77jq05mkzv88js7ckc926vcqepp2cj -sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2eHhEYkpERTJKZWhqcWw2\nQnpheC9vR3daT1NxVWlnTXhwTTFWcDZaOW5RCmVxTUY2UnhUc3Q3K2hiWFZYemhw\nbUFTaDIrSjBkejVieGpIb2RlNkhMU1EKLS0tIFBLSVgrdTc3ZU9YMGZDRDJRbk1R\ncC9RWXl0SVRPQ1E1L2JNSmcxTmhhSDQKXyPOv47SGBPhvMJTFMw+mU95GwwQWl+1\n7tGbjWYk5swJuFqIzzOIkpPxKsusMo5DuZ601OBJjM2KNzjXa5R5Hw==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUWW1oVVFCcEtpRE1PTnVI\nMTFuTFFuYWxZTjlPd1BFZUs0YmRLZlViWXdJCkVWTW9KUjU5WWpLTkhOQzI1NG5C\nR2lGQ2xmY3V4dndEMVk1U2lDK1o5RG8KLS0tIHg4Rm9KRXU4L0FSVXBBQ1VyMjVu\nOTcwRE9mdFBCeHBTRzZSRDVPbjJ3U28KU29lj+CN/kkknWIfSW3RU2/S3QYZyjpT\nOAGms7zxeI8tyXtd6aD2JKTYQKItRuzeEymW7QqhnHzZ22xFQZE47A==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_2__map_recipient=age1slx6e48k7fre0ddyu7dtm2wwcqaywn5ke2mkngym3rpazxwvvuyq9qjknk -sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhRTc0THMyUENhWXl4bFo4\nLzRwS0RIUG43MG1OaXlrdkxKTXB6RG5kV3hzCnk1TDYxZEZjNE9mbVp5M0FEYU91\nRWQ4Rnh0WlhTeW1oVlJOQ2wwSDVFZDQKLS0tIG5MWHl4ZGJCQ2V1RDliaURpUDY1\nV1p6VjdkcXJOZkJxcUxWQiswVUFUTmcK/TAYHS0XclvIhwWZ4kKMOYhaC7JofcoR\nD6FRY8uxabHJ8jfpNqjh+MV7YUTGTZEpmFyThdWJrK2t9o4V68ikDQ==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5WklWeXNFOGxMTVVRcDRE\nOVFuQmQ1bzQwazNob0tXQVkzdXA3amVSSDNNCkV1ZjVPckFPQzJadzUzS1Fkc3JC\nMDVVajZXeHV6Sk9ZS1dyM2VmZm1UbGsKLS0tIEJReVQwazBDSEI5V1duNyt1a3h4\nTXpNOEpmM2h2MC9ybm5Xd2djUWFjRUkKga+s4Vg1W+CI4PY9aeDSCT5Ey0FekFtG\nQeGbzWa1z+6v92WhtAqI3GDzWb7cRztelIlCyF6kH7NF4xrd5Vi77w==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_3__map_recipient=age1vfhyk6wmt993dezz5wjf6n3ynkd6xptv2dr0qdl6kmttev9lh5dsfjfs3h -sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBMFNRSlFVckliT3I0blBy\nNy9lVjFxL2hMdUFZMWVXLzRlRXpUMGlPUjNNCmEwYlR1ekNBbGkyaDZVdFBhckZJ\nOVR3VjF1VTA3NU8rbzR6Zy9ZL1VkL1kKLS0tIEJ1ZW5LbUdVOStxUXUwb0gxZld5\nSHZoTkVLQkRzeE55YkRyMUVuYkJ0dlUKwHsI1pGDDIjTaF2tJ/anbuycq4IbNxEL\nmKkXkLjqPHI023iLrcdQejzKNYAbNxCyA5lyt5OeW92aDYXQJL5Rkg==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvb2E2ZG1WTExJQWN3djBR\nUVYrY3lOd2E5RDNTY090UWNMNnRLNFVzU1ZzCkJ6RGhWcGk5WkthUnZWQkRlQVZD\nWjdhaDZKVW1xZU5PNUJCTkU3bnliU0UKLS0tIFR1YXpkUDFKSWpGMmtiTkpiUDFI\nZWVtK2prTHBUTEdyTFNtZkkwSEw3bTAKCEXfEO8bDCCW8GvkpFpF9EYVXVX++MLb\n013tqasoCz5etZweZZ+Nzrb8MBSR26h5GkDJNwiJsF5KUz+fDDpDDQ==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_4__map_recipient=age1jwuvzyghfer7rx3qtqa4vs0gxyff0sg6pqgmqvp5zlhnpmvrkdlsdha4kx -sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0U2dkTTlPN212TmhUVVRp\nSWJ2L0tEU054MGcyYW8waWJ3OFpPeGdEVlFFCmI4NkpndSthTUVIOWlVNzYyaFZh\nSkhIbFdNNkw3MXU1YlVFelFpZSttVE0KLS0tIEcva2NoWUxYZ2o5bC9ONU1VeXlB\nU2dseXp1blZpNm1Kc0RyL3M3Yk1LN2sK9JqFOxQA5rDCtnyA8cFYb1VYJ8dWfrj6\nTwsiuNJDTfyNgoNYx9RkahjH/nthCm8IdtBM1+TI77Ru58U0hLPjSQ==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArL3hHNFhiTEdVSnJCZGNN\nUkM0T3B0YUtLSmRlcjIwdHpmUHFUNURqelFJCks1M3pWSFluTzFZVzg4cTZ4RWFz\nemx4bkZLT2ZGRVJVa3ptb3pJRTlJOHMKLS0tIFgwYTF2ZmNaRzRmQ3d1S0dzeHIv\nT3dBSjRKcmt1M1R5UnZHT1g1eHBPQnMKoJywqzsS0Jrx0V6LRQ4pI0G/PmeO+tUy\nmnnruvIi3TQtXKPks/+Itu1cz3ey3JHX07ZDFBsXMezXc5qI5xAwlg==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_5__map_recipient=age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy -sops_lastmodified=2026-04-15T18:32:53Z -sops_mac=ENC[AES256_GCM,data:Ts3o8hy0OP/+dc25PL0LkCH8corWa7MJMsw/LFsS5+YnaoIULVIYytXpR1zAvVtH92WOJ43wvMh/C/c5GcLVYi3xgvSW9OFGoHobgh0n9/0GMkIP1qLQxRj7qk3yRuSU6E6fQE3LheCi1BlgUvd22l1qhMOdbwWBRsNuYbGWWY8=,iv:BPHRHRw0gzPzh1MR6S/SQ+uptvoJyibjrKY0Y4GaitE=,tag:HCwPb+h0W5KjZYuVTiK2Cw==,type:str] +sops_lastmodified=2026-04-16T02:09:02Z +sops_mac=ENC[AES256_GCM,data:f1mYCyZcM7p7bLSAFTtG8odN5O0u9glKymTPFyOfbWuQzC5r1LOo0vEZ6sOdjX7xf22NHJT5gI+k75BntnLI6QZAx9HBbAqISVKNcWaxsAI6UNXXiTffkeR7ou3KW/An/u5tYMwKf34T/dUXG39+vH48cWSOLBoejkVZ90LH0F4=,iv:q3CsH+xPxpbhCqB/+5HbTB8bgKZ2tH2FpoSYHvyGWFI=,tag:Dnw/sXLsXNk26wwNoGCKtQ==,type:str] sops_unencrypted_suffix=_unencrypted sops_version=3.11.0 diff --git a/infra/.env.enc b/infra/.env.enc index 5a6c86d907..40f91f91c1 100644 --- a/infra/.env.enc +++ b/infra/.env.enc @@ -1,68 +1,67 @@ -AES_ENCRYPTION_KEY=ENC[AES256_GCM,data:Ui95vUqLY/G2f/1rcD/FdaxiF5Zmk9qBd9PQl/Ul4XZvr4M14oZTbbE8IB7FuF5ie8HWDWWIYcU1T7sdsz+RzQ==,iv:I8bUx6PzPr6IK3nugl3/CZGuNaljfWOqyt7+eEeNkck=,tag:S5p0lpHqA2LR8Bsp2M4ODA==,type:str] -ALTCHA_HMAC_KEY=ENC[AES256_GCM,data:oo1Hc1B+9RlRk4umcCSzlhflHB7hV3MDnlUhudMCuiEoV4+iNY3PPuNQHbsvevPnuIeAPHFaG7wA566Kdwc1gEoK+WK0KvvvK2byOw6IzJRWOyf7EEpV3YewVXmwQDSBp1YIidMDM66EAxR8AH6Cs1gayzyS3bPGqRNe15FywYc=,iv:yFZFP+rR6yTtrE9acWW4iiC/KAzcALsl+NdjxbPNNqU=,tag:4G0nPI2s9IBurcTqpUuReQ==,type:str] -AWS_ACCESS_KEY_ID=ENC[AES256_GCM,data:JfCeg/T0BeOzIQft7GPgexf4kOY=,iv:5y4euI2hkNIzzOJspRmdl6ew/P44eHqjDqMttySvwrQ=,tag:y43N8u9UkcwVfeiW4RnGaw==,type:str] -AWS_BACKUP_ACCESS_KEY_ID=ENC[AES256_GCM,data:gGWKKyerQwKptnjri6LB6jOKJwQ=,iv:hOvG3tct0RgDY/Omd2fx2+SVzylFYFxK9nCgzzjnMR8=,tag:moAleVDs90VggM5Ou+I09g==,type:str] -AWS_BACKUP_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:OiIY34wX+x6lYIiL+39eshFpeflU2di2dryO69ECPfvxkPhjWzntew==,iv:DuvHRWz+OJe6C8sRvy5Z0TJsN9pfD9MXkaFoNOXTvX8=,tag:QH8DIAk8zN5SjT2yZP56uA==,type:str] -AWS_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:0pnKqThCTK0mLQnh98l6VawCgA2KBD4UZ8YDJ4Nyuyxf+eBG2yOqkg==,iv:+ZNIas01EJMTzOU1Y3BtVNkdabd9SA+rt1t4UcKZp5o=,tag:h9vVf97l3v//+N0DovuJDA==,type:str] -BACKUPS_SECRET=ENC[AES256_GCM,data:iVWN5AhCYGseXzZQBvtRnp4aO9knsLc5Hz5tyfGLKpYED5QAsPcyrtk+R5o=,iv:FBOG6bvNAzDDgNoA795KtYv96TY9q9OJeztDHoNouEM=,tag:YOa+wj6s99OXgKPgt/1L6A==,type:str] -BLOCKLIST_IP_ADDRESSES=ENC[AES256_GCM,data:Pcy7hSfUwl+MBjcJV5T5z1+V70AG,iv:PsTMvFvcTLv2+rMgb912WeqRQGlBEZ2JOCfzzwZTOfM=,tag:uF6KjILqEkwLpj3ipBnbPg==,type:str] -CLOUDFLARE_ANALYTICS_API_TOKEN=ENC[AES256_GCM,data:i+g4KxXrgPxgMo+wZ0zTlSh72RTwdoLH61V90geRxpwwOf5u7Ue5HuS26rpuzO+u0q3/LKA=,iv:ZOvNALk+KmO2HPTi5A4vb7JhRUyLAdBVGn8/m0aME1c=,tag:6zXlB4n21/Pqaxipf0om/Q==,type:str] -CLOUDFLARE_ZONE_TAG=ENC[AES256_GCM,data:Xt3vEGsfOM1FrLt9crPvUCrDek+sXBJ9niQy1zMMuSg=,iv:WGEgKfDz7HvnO+iw14xvbltakAdOII7zq00u9lQwFjM=,tag:0gWEWH6g9u8B+b48WY1/hQ==,type:str] -DATABASE_URL=ENC[AES256_GCM,data:nwiAGeb/u6LQC7rfg1Ru4F4iIGGtJMR0NaT5YHHJzq4Tf3i5DkcIJIDUUBU=,iv:m/Tw5+hsz9AH9np4qijUB59dwpQ1N+3yTDb/tVkN40E=,tag:JTJWsjwzWvITb9NB0yR73w==,type:str] -DATACITE_DEPOSIT_URL=ENC[AES256_GCM,data:ZH5JC9vE1UAU2Epz7DzESByzQp57gyR+J9zdXQM=,iv:uziEOlB/TBG9raV3IsYDsryUywZN/yTJ0tCiZ3I4cSY=,tag:/PurtclnuVB9rxDvBp/5MA==,type:str] -DOI_LOGIN_ID=ENC[AES256_GCM,data:aatCuRDt,iv:9Fx/FciXouymlkRTMO8W+eI26XyaZvMQVmfejb0Tt2Q=,tag:HlhZfXGYTXEMnpLKrR44YA==,type:str] -DOI_LOGIN_PASSWORD=ENC[AES256_GCM,data:V5QYtcdXhS8ZSigFx2IPA1wzYNk=,iv:x/632bVVT4ZGACOspA+4lnZio8NY2GS7FEDqQhx58Nc=,tag:tYUgWfsMyQTkNCYHtjcD0A==,type:str] -DOI_SUBMISSION_URL=ENC[AES256_GCM,data:5aIe7Q6btQa7Ay9fxETATB9p6w2fMiv5icOkRtWtJLYW9GRxRWyabA==,iv:FvXboA+elgPY7+/PEaARtAXIZLnpDAR7SvzO2Azgnv4=,tag:U1b+t6/xaTalBkuN7XMD2Q==,type:str] -FASTLY_PURGE_TOKEN=ENC[AES256_GCM,data:S5X6eqK6aBKYvzPWF6+xKs9AA7VeLYwsU/ZWQ4GxF5A=,iv:JuhldtCNS1MV2RBR/sfL7NnKWeRI0+zoO8gOYbtdlbo=,tag:YjjchdYtu+WNp0G+2J8aNQ==,type:str] -FASTLY_SERVICE_ID=ENC[AES256_GCM,data:X5JHZ3poR0PSnv5XIc22KfFv9czwKg==,iv:Xc3xOZ4f96zddfC2vffhd7ATsk+Vo6NT7LjCDVxsYyw=,tag:rYAIMoxdoJWn7T2FZRE02w==,type:str] -FIREBASE_SERVICE_ACCOUNT_BASE64=ENC[AES256_GCM,data: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,iv:wA4rc6BK1skj2aQr9bXlAc0nP9bTXzaE6yHPKF1JjNo=,tag:H3LsnFJdFRplzfM9/5HtBw==,type:str] -JWT_SIGNING_SECRET=ENC[AES256_GCM,data:JxF48s+ihDRN6zl57XlBgiCUAymisZCqLpdBkzo6MYg0jaSP56SXVGGbYRV3qyV4Un9usu7e1Ltar8kLASs903ZhlDgBAdDGCq6OTNWelWVaXhdloUKiI19sTV6jTvLKfD7fT/3eEpTXaXNypYvJTFo9xukb4fm2bPH4h29H5qrv1nhKGEVExHIDf8LljjWIqoXleynuwcqeY410vpqRSZsQYrZzQmhc+VD3kOXKoTj73w7j9CTfJgE2sEx8nrHftEo2ez8pjgaiWFdXlt4Y4rE/0pTGUYy+azjLElyBY5FppaNjXc9W4mOhaact6BY4RRUjc1Dp61HRwPO28fJg+RI2Bo3phIH2wzDZtgRSBR8Q3R54rCnNQvG7r6sWaC03IY+okCaBILwR9IMqlFLePpfJb+asyrVuTm82uIik1wE6tKR4O4jmkp8zfMXyz5NSTUVo6/XP8I8Jv9nfpopHhP92FlSUa0lDsSOsAa50tLDxas8QZRI9J1T0Xb5jkdBbrWmNfmUz8TkOMxHlAnSPkVroRDfvQKWlTUjgSMlcEUKssWxYYq/awbguMwcAPFEObSEaEY7UhmbYlcKLmVcaSGBipcaJF1NGK6lJ8DiZx/Zaii6WddyDeTJtxJXnteaQPcS5wSN1qj+KYefXn5a19FKBeSRnkT0KKgNFvAHOc0g=,iv:sqENtnaEjUIYDq/9Q2557aIDFPhe0+N9bGf0aWboiM0=,tag:f9qK+cwExgIi1sn/6eJ9Mg==,type:str] -MAILCHIMP_API_KEY=ENC[AES256_GCM,data:6NRaF25eBYrqsG8wWof+msHo9Msxf9CLR7/VEBvGBb2cJ2PS,iv:+4+iVWHW81SVBnMW1asJ9T1Or4hnPD50HoytVOROTq0=,tag:HkutAebm2bKRLVaCsxrbpw==,type:str] -METABASE_SECRET_KEY=ENC[AES256_GCM,data:o4sxmTU7TqaE4xcF5ujJZUDcTky9ughd4uw/OTvNMz70Tixhx6Mo8NGaZsOOO1HbDS7QJ4aPWv0qBaUbPQAapw==,iv:ONPeQ75IPNzLcn3ZIcUmMuFiK9eTOFwAg6XmNFGEFSk=,tag:fqYLhkLCVhwucj27KU1sXw==,type:str] -NEW_ACCOUNT_LINK_COMMENT_WINDOW_MINUTES=ENC[AES256_GCM,data:keI=,iv:WOoVf4nBZDMEdt6fuY8jxIi+x13qmLEWfrPXULJ4Qbk=,tag:sOEL37Lhpmf+WyE6zEp8vA==,type:str] -NODE_ENV=ENC[AES256_GCM,data:EnQ15KJ5Jf4DeQ==,iv:7aXPVshnuOuJohlf+hNGPBJXKnzJ9+9XoRZa4JAQx0M=,tag:KoodR3g/7doSORc4c68cXg==,type:str] -PUBPUB_PRODUCTION=ENC[AES256_GCM,data:nUrdaA==,iv:72bfWPP1saPw31EkmmBquhtbi95rJOr2g0jgUo9xGSM=,tag:H1zDiUzpn/X+smxyaW3dew==,type:str] -S3_BACKUP_ACCESS_KEY=ENC[AES256_GCM,data:4jQaVGHL/fy19Jo76aH+bX7vUKQ=,iv:nXP539AJbdSgkXzUuA41jsKh0Gl/8f8YSsqtBhu4xII=,tag:BtjURukoqejdzP5eG6dPjg==,type:str] -S3_BACKUP_BUCKET=ENC[AES256_GCM,data:wxn3e9/czomF1OI=,iv:jAZyv7oChFc2nY2l8echaMEDQTG+rNo4d+H4HhUwZx8=,tag:bG1i6pbFXpdCbw+F4QAoaQ==,type:str] -S3_BACKUP_ENDPOINT=ENC[AES256_GCM,data:ySpn9Bms+6YaSBOAqQsM2mzCp0lWm6YmpAWBEraP7Xz6vHA=,iv:q7qan8OqUtn20RlOZI8/jr6iS8SMcQYani3/xXmwSGg=,tag:go/bW9hBdwYeMQXxeZlU+w==,type:str] -S3_BACKUP_SECRET_KEY=ENC[AES256_GCM,data:waqthMMpZHZqbVyWx3HqeuW2eAVZoiRx52ORgO7SFP6zEgfDjm1Irw==,iv:jQOuO3/kmEiZI6KYFZ6eAYtgOuWLD+/HmjboBc8G3ZE=,tag:aXcPkdDy2I9YBKGZiwJV6w==,type:str] -SENTRY_AUTH_TOKEN=ENC[AES256_GCM,data:wETPjQQEUAy3dCm8oPYLqeKwDH9wGO4sOsvEJTu0+QxOpiQRWltGWNkHAsPXXdbPUD2R4OiE82sbr32y5TdmCKaE7R9cXLQl9NQGsR0+jIRhiSf8JlOH4ooEvaSCaH3FcOSFE2TtNaXf8pl76Rh2+naSvgIOIWHbNKysFaSTxdZpr6T/5ekLZeNO1b1C+m+5HPYTFCBzW+fLYmfebbXNSIrG3A+D8CL38BiGqcyncmlj6lipyJTumpLCWg==,iv:euflMK8i4pdbdj6G8EjXp+H7S6TzV1m1mDFPr3jIKHU=,tag:aTF7mNb7K7Q11gKyajSXGg==,type:str] -SENTRY_ORG=ENC[AES256_GCM,data:WN1g,iv:C9sbcs49bITQ/i5EuxUsCXgSCL8k3iNzacoDEII1p68=,tag:qgPpCqgmepqnriXifDCv4w==,type:str] -SEQUELIZE_MAX_CONNECTIONS=ENC[AES256_GCM,data:zjo=,iv:2SrKZRhXKNdQTyKRQCzGSRQ9ATwl91N8VCEbaMvF4Rs=,tag:VPQdStR20kkIuK/P8fRfdw==,type:str] -SLACK_WEBHOOK_URL=ENC[AES256_GCM,data:2rIsrHikodKhA2l0omEeuAKcQY/1k3VLFr9WgsxpCFGaZz5O+VddEYfXE+DrxcGU+/OnBi6Ssq4Sn2aYu/XkvhTU8nyzGkVlVLJwaeZ1ng==,iv:+ccSwsH7T8zqnZVyRy2UF3LLnl4Sn5OpeqtIP5vyyaU=,tag:v820AKoH6IyVC920451nXA==,type:str] -SMTP_HOST=ENC[AES256_GCM,data:zPSKG3aKDZ9DIrXl3b0EOvgw+JoDVS7TSGvFm6PYbIH98A==,iv:lfpbDHqvVH7YOuQeCR+IeJ22LiVqvk6vYkPxJdh90as=,tag:HDwJrIk8XoP/CL25HGx8dw==,type:str] -SMTP_PASS=ENC[AES256_GCM,data:hwxxJbvxow5hEVfLCNQ6kdXwsfp8BTKiqyBX52Jdha8mqFcyeqHuDGTLDEs=,iv:0OHh+bzoBiRoheBQmX57IhOJjA0ASh916nOAIiZL+28=,tag:86+pC+ibCdU/ex9ByyWxcQ==,type:str] -SMTP_USER=ENC[AES256_GCM,data:640gXFa+ySFr12kDxGEz7d72QgY=,iv:L5hzg9g9dA56jG+7Xko/qgXGNMz4yZOHhhUHzvA7oGo=,tag:GwHUmR/JP9nsa21ZCZphXg==,type:str] -STITCH_WEBHOOK_URL=ENC[AES256_GCM,data:Lwg99AwYPgGEuc2aiO3ayFeMT16icSfOTzuSoSctYbIOEG651T/2YYBIVXT37yAaP8gArDEiM19DjXKGuZK+hUuRM9gjkNMYgbj5rHsnAAh4FWqRzZJjgrGRDW79HwQ5fo2bA+af9lF08pEdOrPNdKYH6EXg,iv:cpRi/MVVp20icPKjHyTb3Jr/jI3u9x73gtSFRKc2Sz8=,tag:Cz3gfvNdr5FAIhT7d+TgbQ==,type:str] -ZOTERO_CLIENT_KEY=ENC[AES256_GCM,data:fui6RVlZOlSsUNBq86dYOqTexjY=,iv:BKhff03EAN1pF1+933YYVf+19FvftPzad8jbUZIM5qs=,tag:ngdAsFyK47pJUd/4Xvezuw==,type:str] -ZOTERO_CLIENT_SECRET=ENC[AES256_GCM,data:9jU5zCYTzBMaTjaT3Rb0DIXaRR4=,iv:DqVGEgiYd0Pdq0geN1GLaowiLrXE1AP1WAXZLp/nmv4=,tag:vRAkHobs9ve0xomIkCEc8Q==,type:str] -#ENC[AES256_GCM,data:OP+ZLOvH2R52GBI80RcU1eYWfcb2W4ly64OnJ1BfYgmJTx9VkRaqyWGB7as3d2W7p+ky7lkFnAN7iLswExbwiItJ,iv:3rXPj9fIKdE5tjsgDIr08J+Gw8tCXYT9Pu9CX8GCmwg=,tag:4WZ8fvwFyRHvt3olQKjc9Q==,type:comment] -AM_REDSHIFT_PATH=ENC[AES256_GCM,data:igLvrNR/FpflwvkSlRnfSIInWrfCLxmPJupSEDPwbmQA+l0ev81/Sj3PvHOfHvZEQjmwfTvYc2BFMz7C6ok=,iv:fNjdzWbt6kfav8xf52/UHQ8ri8YJzWH7HaQSkR0Q+To=,tag:mb57ik4m9zN+iIiBYwMj1w==,type:str] -AM_REDSHIFT_BACKUP_ACCESS_KEY=ENC[AES256_GCM,data:NpJj5NXkdeaJJGtWhIUv/k7TU34=,iv:/yt9D6JBsfl/fvqg+E+MkkiL3a9FNKbstFzMSym1ypM=,tag:P7WsaMZxT5StqTDYbHvyKA==,type:str] -AM_REDSHIFT_BACKUP_SECRET_KEY=ENC[AES256_GCM,data:7+xvjVagQ6hIJs/vtCfGnnuYokquAvH6rfq5Ez86wBlwIQp74Jvbrw==,iv:E5dFxn0Ra1SeAQGHHLdzb7j27mDI08dvRS9X/Y5KUpE=,tag:H8EOanX1mGXRiwOxkftsBA==,type:str] -AM_REDSHIFT_BACKUP_ROLE_ARN=ENC[AES256_GCM,data:tItV2uNaN5bj+jY1ghsGtL0nInranzwTh+KEoa5NwDepD3akYfs+wDDXUytM5JCrDTIY,iv:qjs2EmBy0s6qCLM8IIO+4BJpp+8RP90WfmhecWKRuY8=,tag:FEw/9d1sExXMK2+DxOLTTQ==,type:str] -#ENC[AES256_GCM,data:6WyNJDMVgvdDK6oeHpP50WokRB4YYzLJdWbXXGyrWygo3matYIQzO8gVGgYfVXszc7DidELAbHTyrEwL6OAmUlVQq6Fo8fwLEwGv96dqlkKULqN6UFcw1/jYYppx0C9rc64RAa7agEK8cqDSwIRBMmu1QSLwRRF1,iv:A37qc5SisamZ1rXF3QZbRMGYHqReEVMFKQDt2hbR/kI=,tag:xDMWNXmEnqyJEQHmJy1gGA==,type:comment] -#ENC[AES256_GCM,data:iTi2tYUq0gY/CWanYwHxv8O+Ga8IE5aRZXEBfb1WN4NUyE3RnvY6fvepclFOilhtBcQ=,iv:RFi4O6WLG4abQ6U+BZbLtPT7Bl+bthDwA72EgeeNnmw=,tag:vbaWF5cIoEj41v96bJ9bmw==,type:comment] -#ENC[AES256_GCM,data:uTs7pKOJdLJ+2F7ZW0TOJqxkfhsGoZNVzFGZ77XMNqRBUkneiyi4gDXsauEL,iv:yVqJOalHuik9X1zq06SgjsicUK/2f+ufrukItqKZM1w=,tag:yV9eiHtuTXofMfxU5D3fEQ==,type:comment] -#ENC[AES256_GCM,data:NxIpgvISjxk4Jt716yJa/OlopqO3t20=,iv:GEr2LCyJGzLmI0WLJDaKeUYRT5LYiDyw5k/TLptqdV0=,tag:5AHbYLkohLR3MMrIX216fw==,type:comment] -#ENC[AES256_GCM,data:5UFfTdn4NxUXDDCg0F8Fb0s6R3vdUqM=,iv:7bq8eZVHew47EOaKdA46QixSdnOSEvphCqaYhjAtdU4=,tag:1zgeP7JCmbyYS+9pCcMuLQ==,type:comment] -#ENC[AES256_GCM,data:6BC06vYVVaIKuFHb/XuN0jnZzDX4pbcQlID7vyv+yzL0tRIQ+86dvMoi62y+43G+ymOBzVOvu6oslp4cZZqKskUwhPVACQMWwrfkjQ==,iv:1Xcqz1K9BWdv13cXq/PUfyR47VssV7iyaTxmvy6AfhY=,tag:gfOahTzuZ7IkKS3WFXx36Q==,type:comment] -#ENC[AES256_GCM,data:EZegD+uzbGMrn3s609Q1OMvM/Tc=,iv:s06BJmwwYaxEs/Qi0bax26+3rCNMKtQ5sk8/G3WCjAA=,tag:KRmnUgjl4f0SP9pYL3g9Lw==,type:comment] -#ENC[AES256_GCM,data:2N/dLT88ZMjTo90fN5CPiOHlV6Gg6rk=,iv:ojS+I5eA+xrsbl3qNqNv4sPoiOYiR+janW4XCzdB52Q=,tag:GcCfQMSXWgjO7eW74XI0+Q==,type:comment] -#ENC[AES256_GCM,data:ZLka4KlfWYZ5CaGvenshNA==,iv:1AXeIICI+CHq/Z21dY0DSxhrNONPrmKMvjnx24Xr2nQ=,tag:R9u5PTqzB6/CSXocdGI3rA==,type:comment] -sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6cTdCL2d0SFBKTzhUdk5B\naEhudm9aU3YvNXVlQm95NVN2UmptajcrTjJFCkk5cGF4YnllQ1N2YlhoOHhnSnNw\ncWo4dUZoUDljbDJCRTV1UnVmMlJMZ2sKLS0tIFJBekVDOXNQMnVwNU9kTjYzdmk5\na1ZWaFdEdmY2eGttajc1SEgrS0k4YUUKmPFm4Y1t6bm+bCd94s7a/NR2g89l3VEN\n4NrPJ2gB4TaQ+wThajUu+S3MG5Qj+4p1gRgcA6SOSSp9/hJwNK0//g==\n-----END AGE ENCRYPTED FILE-----\n +AES_ENCRYPTION_KEY=ENC[AES256_GCM,data:k32ZCst0YEgC3gvP0Koc8f/CopI5GcNKyrYGkxCkDqfasmO5QSIz/PY1ci/SuyZvD/qAcnb53q1GNXMqR67ppg==,iv:H0XHfTN7tYQECeHb9XLh2bJY2sdA57IoqwtNxBbHhfY=,tag:nYerayazolJziQuMof4LhQ==,type:str] +ALTCHA_HMAC_KEY=ENC[AES256_GCM,data:YfYmB4BVygwrQzVmaJOFg02y18woR1KXx3hrFeTu89rqeHFXORiBK4GOr14r9iTwj/4wMhFdO0OzYf1AwWvk5AEFYjChEilb2OAL3NexJ0ZTwagOyqhYL9mrIfsycvBrkiJe5sjKFg1fD88AwHbVQ/udR008upoigBUDmxFVAXM=,iv:Xw61phMMYeWbwDI86DHrTTKcFJ+/wZdneSseOt7lYtE=,tag:Rwpv2ifLtkVYxgKSOJSFkg==,type:str] +AWS_ACCESS_KEY_ID=ENC[AES256_GCM,data:JX7lhNcmSyAacP1vDuQIXhaoA8w=,iv:B1AFt05utLa3t9vpr1n9I7XK3WfGSGaGdeWfsV3ypNE=,tag:iUI0jhfu+Cfq0CWwagJcGA==,type:str] +AWS_BACKUP_ACCESS_KEY_ID=ENC[AES256_GCM,data:s33OElLTL3kuU1LpDAjQIEcJu9Q=,iv:xqpg8c1/WMT4xEuTq7f9AGGshPMCtgkUbS54Sy+/EiU=,tag:vVk2EKgCuLBD/hy3qZHTiQ==,type:str] +AWS_BACKUP_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:PlZTdyJRRczCyfitpFjPK9rXNCHL57EOSWIU6yEgDt8xQA6RmU7NaA==,iv:seOaGxG76Jq28n5iRCFjcHqQIzcLy7rMiHrRK9BQrPE=,tag:a2lsNWRhyTVEKLy7hbPqNA==,type:str] +AWS_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:+5yTd+bDgtqhCNmLruqN2skMVqWMV2v4mBM6TGAxLyRgTT4R0OwwDg==,iv:efvzb/Yh3l5dn27Z0wUzXvbwDvIy98ADtPZV/rYDfY0=,tag:/ihjDcifNjoCRN9q5RQlKg==,type:str] +BACKUPS_SECRET=ENC[AES256_GCM,data:VbidUbUW0wqotioSvFiHM3bPl5lPSnfd4bzbTlFu5DlCbJYQXzgNE6MuVqo=,iv:PmxZ1FCNlEKniTnqJEJtJDpQaN4BCecggD7lxQmjn0Y=,tag:ld04d+KS/u6zA4qKhg/m8Q==,type:str] +BLOCKLIST_IP_ADDRESSES=ENC[AES256_GCM,data:o1e0Sb47NxpoLpSsRIj0qTpZG1z2,iv:zvAd5OQhKWkWJS1Qvc+sSezQdEPg1fVdMHi1dJXsjz8=,tag:KKs9qINowsTQA6SZz6kJjQ==,type:str] +CLOUDFLARE_ANALYTICS_API_TOKEN=ENC[AES256_GCM,data:fjA6EoxLmHE9XESy9xM2OUZkFs330n3/spbnukM9yBjEnRVHzYIsxo2lNKk06W1t8+U+8VQ=,iv:v5OFvJpTxSr+T4ZpWygE1cG/gobuC6h67wL/DMRCogE=,tag:6z0GJveXYhHCt1B7WebZeg==,type:str] +CLOUDFLARE_ZONE_TAG=ENC[AES256_GCM,data:b98V+mQRbFjiC7f7SADOHrdbeyA+3IaLpY0sRnfUgHo=,iv:BJAC7H12q8c2gbqKvivVznEpQFG56datA0g/X7KwB1Y=,tag:7zq1tfU0d1TZpSKCtN3IsQ==,type:str] +DATABASE_URL=ENC[AES256_GCM,data:frRrTmAvWgApYggDLXaQCXHusHrxRDvg9M3nOU7MuMxJCXyzEs2x5LzsbNo=,iv:2bWi9sm2YFTnen7rI6G0gPtjH416Qa4rUr0JNtXy560=,tag:JjxLsvbPFccsppyAB2RDCQ==,type:str] +DATACITE_DEPOSIT_URL=ENC[AES256_GCM,data:jubE816ObsEcY3pvrLpwMFW7ww5y918WVty3rsg=,iv:lcCm+1kd1sKX82jO9FHD3M34icJ2aFZsIvrGOLlW+6g=,tag:2KOo7JoqF9XRRC+OWpvEIQ==,type:str] +DOI_LOGIN_ID=ENC[AES256_GCM,data:P1uoOqKD,iv:5AI/6cTzsEygKtpP+mi/zIC1uX9t/jGIa3Fliv2/APA=,tag:yDdHASwGRvMA3bUTTP+LLQ==,type:str] +DOI_LOGIN_PASSWORD=ENC[AES256_GCM,data:DRAd0DTz/JyvBWwEixGHc/zIVak=,iv:TOLZau+ceyXY7OBIzFUL0uz7Vst4LqMLtRo1yMy7StU=,tag:Rr6MeI6g5yhhbUSSlsdOqg==,type:str] +DOI_SUBMISSION_URL=ENC[AES256_GCM,data:naKjv8ZxYeucJzB+5Tj/mRpMsJx48rqQFRgOq+P8eXSTI4q3LnsjqQ==,iv:s1C+ptMYcHxx9i7s24OB8Kvx/M9Fjnro3EfOXzpWJuU=,tag:WZvSG1V9h+DNaDot/9tU4A==,type:str] +FASTLY_PURGE_TOKEN=ENC[AES256_GCM,data:eWJsQZqyb6tLiJd5ZfpVTo782ouS8DrZ4BgITGlxE8U=,iv:eQRUqv4aQwhuwZNW/PSkt8ksSr0tTm9TbmXmGefCDkU=,tag:HsuBmUg4YFauZeHNWVXUeg==,type:str] +FASTLY_SERVICE_ID=ENC[AES256_GCM,data:2dlQuCq/3wfUg8upiTN3JhOc8hBFJw==,iv:e0ii8TtTNOXteBlmb6s/pnd7ecIFFLRqC29MadByeVM=,tag:JCljylCxFnQcXxXzhhZptw==,type:str] +FIREBASE_SERVICE_ACCOUNT_BASE64=ENC[AES256_GCM,data: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,iv:BzGgm+kUAgnErUqMwJ7xSELfHRDZ4w9jecQjBL1KT/4=,tag:wf2h5x8RP1/nlBfEDvn+fg==,type:str] +JWT_SIGNING_SECRET=ENC[AES256_GCM,data:osI0+FyWHOrWRrDQOEUxqYIu1QRDYHWWBBEnnw4BBO2mvjhkstiXKtWA6mn8W939DxU0UqbJz91riQxZ6NWIIoahh2kbpTnCOm6tI9cJ/1cYVnyhCrCUahwL0gtiA6kfK2XB9KjUgFbF0aJ7pC3hOz6rlofzTb2CjyII5EDHf1UGi0lX1yz50kUCGPmbCOnh6Lkv0hKci06nzsWnBa3pgPLZaCz0Y9OGAQ1fhTgFLvaBPXJh+DmC7ZFsDZ0BzntNtxKTx4gJPSargbCTI7lUCR930hBTOs95w8LdQsCDJsJcLebtDwZYCdE+cHouTSvIB+wU67vc8aI0/Oy5i03jt9wEzgoQm4w9RLCXfIFZOBUa8O3FMpjqDoQ/OBMbDoPDM0a+2DsXW6fdF+AyLnKmoBpjp8NmzT+onggQNVtkmHFb+SszxPUWt7JVo8me6kvOjClb1K9L34SaNkjfVmtR7MplY7vuQayV5Wed7yHfL855C1NQDa0rNYOtyx9dYRzTG8QdS3gsfJmkZMG+4hqWbCPtymwTLbX9k0OsWaCvNKZThCBSfSZHPu6S5SdO3QvBrIW33JU6+Kq38VV2ZjYeDII84e5v+OTx1DPB3trzJTLN2DbbF6Vty2RnZoyz+YZivzvZpOHknkM7nYphiNfO8vpd6Vzn+qzUWJ6epAcp9K8=,iv:fMAbX7v9pcO/ULK6A7Mv9bhikC7ayhhgEtQ3tV5cdFI=,tag:nqIWtrSjFnJCE7zhSAIIeA==,type:str] +MAILCHIMP_API_KEY=ENC[AES256_GCM,data:ErZmWEjEO9YiuQqX2tLWS3EaTrOQBSmJ6eZ4oFoK73jeySgX,iv:kuth2DVt6pNbvblBizDkfcg020swMl+MgKOeNaajM9A=,tag:i1xwFio6ox3eLEoDhmcGDw==,type:str] +NEW_ACCOUNT_LINK_COMMENT_WINDOW_MINUTES=ENC[AES256_GCM,data:QY0=,iv:9ME5oU1GtBlD0Um3tqEHkWxhXPDOT42HTehqjhK5020=,tag:zTTGGQhuDG6Y6LuS6wt5zg==,type:str] +NODE_ENV=ENC[AES256_GCM,data:NSDv2jB2fdK4ug==,iv:y96kYR9khx5F2mYeuQ1DUGx5QwAYbrZWjPzJe5M6dRk=,tag:Uyh+O2kbYPHWHjZ4Msb7AQ==,type:str] +PUBPUB_PRODUCTION=ENC[AES256_GCM,data:XDAGHw==,iv:5LChII61pqgQjFuNI01C4+orI4cSLQGz0ez/2szSt5Q=,tag:43CY0QWbj70aXES9PFV/zw==,type:str] +S3_BACKUP_ACCESS_KEY=ENC[AES256_GCM,data:Czq5r/jQKMKNmxWpM2iRjw8adUk=,iv:F+zxSFlA1zUk3BJj/VQJHjLFYyOwzKrdwwBtbv0BMDs=,tag:UGR3tlywD0l2wlUusT5cyQ==,type:str] +S3_BACKUP_BUCKET=ENC[AES256_GCM,data:npTW3O9fPcZQYCE=,iv:RxD2kKidt3AFfafml1heRm3IQXXnaBFgHbCR/bsHO0E=,tag:Q6pPsAUUN1zWLn5B1wjukQ==,type:str] +S3_BACKUP_ENDPOINT=ENC[AES256_GCM,data:4B/MPb9uM0vSBuVO4G93XFoA+6nrlRq/DOs6OZqzxDuNP94=,iv:t/KXATs+dH1gg+4c6n0QytmE3NpOfMg+g5zSdR/+wtk=,tag:CI1wF2tR22L8Lahf8cnQJg==,type:str] +S3_BACKUP_SECRET_KEY=ENC[AES256_GCM,data:xc2Tvr9wzg6tYGhZwIhd5DywZ9cheM3QkWyiP6JZBJWsWrYsBPF3+w==,iv:TsKJ7dYTVtAzFdSG7wo9tGZzRl+j9Ib4bIWtB215nFw=,tag:wXl/8wsSdZS/becxSHYwVw==,type:str] +SENTRY_AUTH_TOKEN=ENC[AES256_GCM,data:7UB6/o1ShAPI7mg+zwrJJKUEin6Z/RPF6jNQh5oYljqWei6b2Lpur60/6L04ZkQxANUIKx36Aop07Y8n7/I/QwKfL2rlgtvREB9aRUICzVzqHOhS+jgY8LGdSnwnC3euZn/hLuEkY7IP410YQytt4y3r1k1jRJZfj31+br9LjV06XlK/xJVrK9/izzNk/e8EQfZdsvsIRYBKbj75J5gl0g3ZIE17jtxdh2sC1FF9x9fWr2JhxNnTuSIC0A==,iv:sLn/OuAy2GhzCqNexYYLtTCRMQ9F2gZEvDKu5GpaRm8=,tag:wL0vMcVa5E/hvPZ+CVsoRQ==,type:str] +SENTRY_ORG=ENC[AES256_GCM,data:G1So,iv:5tuwimX3r0supEjw8dHbNrqdtl+o56+D9ibIIFMzHx8=,tag:76xUM7NgHOeAJFpJ6Cyv4Q==,type:str] +SEQUELIZE_MAX_CONNECTIONS=ENC[AES256_GCM,data:Yw0=,iv:0ZkgJ98uesklesUZSVEa0Vd63ybUZ9mcew9m8RO6G4A=,tag:RudcxE3A3kTVo2/ATT5SqQ==,type:str] +SLACK_WEBHOOK_URL=ENC[AES256_GCM,data:gah0wRUzopnZeWS2UeDZBTdadolGco0diPwMnkEd7fce7NZPlWUWb9ogm0KPefGtKufPJ9YeS29wq38by1SBqaF9lGELm2gQAulSP4HM8Q==,iv:LJ5vFqpapV4kP60MpoLbmZHflj1H7igfVl5ggtsLiYU=,tag:GTRsEW2zzaVUsogbN6h4hA==,type:str] +SMTP_HOST=ENC[AES256_GCM,data:+j67YClmBZtWVS9qDLvnYzS8OHOY2KlUb9nDnW5kpZI0HA==,iv:HnRVu8YARX5xxJ5JTXYIWz0ow7W8rc4wKQVBVAauxfw=,tag:kLDdjUr8hpN0l7EV+d4r2Q==,type:str] +SMTP_PASS=ENC[AES256_GCM,data:mT2i229O9bBsvw7SJFdvPjIrLlKUVcWfbXeztv/eXP60NwJtVspWWlDjlpk=,iv:bIFUUjj4SecLRvRdK41jyGFDh2aEk+p2xiwRYZrXz8g=,tag:ufsKdy4SlIpvblyR57uY6Q==,type:str] +SMTP_USER=ENC[AES256_GCM,data:bTwrBSMFVxxGn1a/Cyrz+31guYs=,iv:hCsP2cpMmRcNh1joNFu+xMhuux0FcPsM8EysRTaNrhE=,tag:p84wDCE6MCJUgdAFlEqWwg==,type:str] +STITCH_WEBHOOK_URL=ENC[AES256_GCM,data:33tptTudNy6C0bm3bjG9P7Orjrdw8E6nhTxZ56FKHSXSGzHt2rB8QVC6pQNT4/4SyVDkyoFiSUZhxscJZr124J6BCb9WZnXK+dWOpRO7QDldz7JmVKNs7nogs21qknP0ulx881qSV6tSItng3FDuwoDDzDCz,iv:NvElZiaswOjSER19OUAf321BlBYJJD66yN2jOEzTzY0=,tag:R/jtI7N2rOy9AcJVlI+MEg==,type:str] +ZOTERO_CLIENT_KEY=ENC[AES256_GCM,data:As3yJtrx9I8WdocdnDdgFvrJgys=,iv:DnESLy0ajHBzcxOZ0hQD5mcdtxK0hP7tdoWEq6TPsP8=,tag:rDqqObXHrgZH/T8zBBpiPQ==,type:str] +ZOTERO_CLIENT_SECRET=ENC[AES256_GCM,data:5xaGPyuq/ti/sNQUADq+9TVn3UQ=,iv:QU10agR5ntE4tHcMrYhL5Ddazd8LMIFtNXuP37WiN+g=,tag:uvw+1ftE2Mz2tec7OdfiXQ==,type:str] +#ENC[AES256_GCM,data:lFhMVq92QDNc6GX2eCAdYGZfV2Iq7jl3gY7KTHcaDt1sUPyhW8BrBOaCCBzqXwXcecdku5PNw1mjsBEsXzo68kPq,iv:qOLJlzebNn/fmVjimyjHL3iCb59Noe1b3NVnWI9bGiM=,tag:JygNLV7UAJkm5IhgKf9SRQ==,type:comment] +AM_REDSHIFT_PATH=ENC[AES256_GCM,data:d9WgymSsEIIa+IKIIZyuI2l7NjrALDLChg8u9M94vzOvY56mUeRpBUOKLh8e/Z4W9VuR8j7w/FxIlRcF0wE=,iv:5uqU1ziqJa51ULiOrZ9FooiMI+1WHzkeSqKYqDfJTGs=,tag:Ndf2ozqb1WIJWwu+KsefPg==,type:str] +AM_REDSHIFT_BACKUP_ACCESS_KEY=ENC[AES256_GCM,data:B39njjrLnag6RITSk/7raUDduA8=,iv:qKCRZVkv+TBzhIUG4f+4IVA9ic72lr8RaBZnMUWiwfg=,tag:YkUkgL77bEp3JeWBd/Vqqg==,type:str] +AM_REDSHIFT_BACKUP_SECRET_KEY=ENC[AES256_GCM,data:TwYfRFYOicHhhUQfwGXil17i+Q5q3Xz+/MDyOqcm/seKsS9PqzQsoA==,iv:iC+wNqnQ/XY+AIq13A/Tcp0VBS/bU+smYKsrwO8u4mk=,tag:fOKS5kWs18iGQtOMiKSaow==,type:str] +AM_REDSHIFT_BACKUP_ROLE_ARN=ENC[AES256_GCM,data:mqra1K0ZZ8S+YDD0qQv05ulcxw0tvlTE2MINiYecOyk/fK11FmDG6L9+kbKnxaalyUEa,iv:HooP/DTIzUg7fjWyWGT0Dz+1H4bOtLBQ9hFVy2UGXJ4=,tag:k3WkvDUbzxoCoJN3TjeVEQ==,type:str] +#ENC[AES256_GCM,data:4xykLh0rGkjC/1WzqlCmM9MPapXckH/UM3+3pGq/bIBC2mC1s5K7Xw0DRpdRuBbAEhbdiruxyfChVHPrjnnwCkGTU6T+Swe3v1T91pKe9whODg7lztlEYAaYuJadpUyqAI8RL+X5zl+qDXwJHRL3UbnH5+KL8BKB,iv:W4MFzAZeZ57SHuLR2IcEIdGQt/4JCyRjfUH65c99A2o=,tag:pxgdiKCmyyRTRoqAaAWYDA==,type:comment] +#ENC[AES256_GCM,data:QluCN9lsfdwEEwulbTVjS1mek/vBdduF8dlCbKjzWDPTUT++nGxsP6Wm9cEo3h8pNLE=,iv:2fb/gwgH0aRZZjdWe1EQrcTDkFuR9CqQf27GWDWfPHI=,tag:TA61SQlC9UwSQcvLgeDeCQ==,type:comment] +#ENC[AES256_GCM,data:7GbwRvShiPuKtJQ6+2SAhy8CvVv4LgJl3/w+fXTrdLqXArigWODlI5tlKK6A,iv:0OMz0porMe0h+X30mh0ct4HIxxVGVtjGlwMeZpBD1Fc=,tag:2Zd/8+/wS+7xfnfwMt6DRA==,type:comment] +#ENC[AES256_GCM,data:0/1f9ROR9WPkcBVjwB/YrdaF7nd8Iew=,iv:Oa85fXG1ei67dkDY6q4XpeKmAGSa9XcVgFXoWYBq2BI=,tag:6saYtbO9kga43J3bNcbbBA==,type:comment] +#ENC[AES256_GCM,data:OnYrFus3Cw8GI/REojAYNqwpD3hhVuc=,iv:91YsLmZKsbU2XIFlSvGCFQD1nZqfqFWM35VJKlBP1iY=,tag:261FIhaQv21JLPP53EW0Lw==,type:comment] +#ENC[AES256_GCM,data:kM+6D8a5fuzxpP/wdIxQ6HFL5DwmLZHkMa9BIHQtmG/mzjwKKuuyvF2l2U7Xy1e/7+VRyzlHmQ1Fz4dM/YuWIoJcj8pi3NGA1SHh0A==,iv:oRu4iTaBC5/XPyTrxtIVHoMoNPdp/SfEdTS9mfUaSE0=,tag:TlncDhlR/8TGT7V7L1geZA==,type:comment] +#ENC[AES256_GCM,data:omlkUCwsNPTdeYZlzyBqdKF8ozk=,iv:NF10hgIYNtpCoiQsYVllvkqnGlp2pWL4TB07WdF6TRw=,tag:Xypmc7kAYpo0HhQBTjjJlA==,type:comment] +#ENC[AES256_GCM,data:QaqBWtOlm2TVwzId2vRi9Faqn7nxUi8=,iv:HhYXRgceDEgTDXT277zTYpHht9TnayrvJwhOuaBZ5KA=,tag:QYTmq6k56xD397TZMUHudg==,type:comment] +#ENC[AES256_GCM,data:Adi60JZ5bDyTepYhwYrdNQ==,iv:/fuoMaeNki6tNbQRkPVumXwGlA/1hKQS+Y9tX+i/KYU=,tag:H15s1/1GFTXraogqvPFbmA==,type:comment] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiYXpUcG1ZR2F2UXBHYW85\na1RGdTQ0dHZPaFF3R05qK0g0MkJxNmd4RFdRClVYcDRKalR0ZmtoY0pKdkdvT1FS\nUjVzUjRETnlvQlZwOGxxcmtSRS9WdHcKLS0tIHl3UlNDUE9tNFdFT0w1T0h4REJo\nQVlkdnpRQ2gxUVQxS3pzZGNQaGRBdnMKMNuePXzvj26MFe3AKGNJElztaZxfZmKY\nZptUYVnQMd69ikAnpGoZkPfh3CRQ7VSsoKoN2fD+Rjz6E86rS/WXrg==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_0__map_recipient=age1wravpjmed26772xfjhawmnsnc4933htapg6y5xseqml0jdv8z9hqemzhcr -sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBcC9Zek9VaDBkT3BoVThi\nMHBRTHVUWjh0aHJwc0d3UkN4blVGZkhuRmdRCkFUdEIxZ2NRYnJ3WkJibEQ4aVBR\naFBoQ08xRS9vbHh1Tys2bjU3M01KRlkKLS0tIG8ybTFWN2Y4VzVoWlNNVzhBUjVS\nd3RKSUxBSnZRdGI1T3RkMEJpU1JwY3cKH1+fap5aRx4Pytg9qT+jM9eY/2WyinT8\n9nrmFlBT6As4+J9WiJQPbD6I9GLjjdHBkxBfevvm4j0GbD3gU1fXxw==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6SHZ1dlRIOVB3SEZBTmpS\nY1NRYXhicDdXd2J2QkdvZDJIWWsra2tscVJJCm9HejdJMDEzaEdtQ3BPWCtkWWtW\naUp3czhyZmxSYllWQnhvVExnYlpDcjAKLS0tIGhaNjhGYWJWNm1MUkJhTTgvSXhk\nU2Y0eEhwZDJ0MGpweDJRRUQvVzlQTzQKeA0VrZCIItEYVX5H60U1LUI8ZXpYZGiS\nBMGaoGaXwa6vF/9IUzJbYG3z1GCelhNsHTvd+ENc5z+mwy9OMY1taw==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_1__map_recipient=age1vhftscteyrwphx0jpp0yl60xxrjs77jq05mkzv88js7ckc926vcqepp2cj -sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJOG1ieERCbDExSHNvaHQ3\na0RHK1B0N1loT01BUXYrZmt6MnBRZDFZNGtvCk1HakxaM3J6MFNsVHkrTjRBZnZN\naGllbTRpbDJUWjg2VTJRQ01janNLOHcKLS0tIGdIcFVXV2dnYlhWYWhtQmlyalUv\nUVl2dFVwMFVTaWJTK1g5MFUvbkhmeGsKs6q4o9gsWu7EhTDimF0GOMGId3txOmYf\nKJBHia/S4nb4FJkYC8xwJA9oRoli5/v39TAnTCzihLmwPOWDEmKsnw==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5TnJyTy9EdHkxS0FSUm1h\nUndTalQwcnJSc3U0TEplZlZZL2pIOTFaSXlrCmk5elVlaSt1UmhNWWVYdHIxSlNG\nUExPT3g3TnFjNEpncmxDSys1NU5QRm8KLS0tIDdoMjl6ekE4U3hKNHJleWZqZjNy\nUTdqdE80elo1VEZmSUhnS20vTTVIZ0EKt9JWZ8JdofgnX0J20thMtOrQjt4grs44\n46sW8dYZLhKF/2gbddLvyOza6/VrGiwE46gimP9eON/hDMnlqDmIxA==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_2__map_recipient=age1slx6e48k7fre0ddyu7dtm2wwcqaywn5ke2mkngym3rpazxwvvuyq9qjknk -sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlWnpkQmZyL0VOejkzVFhZ\nVFhsNGpvTEFpR3lKQmRncEZSVXdUS1hlcHpJCjdWSVFTd3AwK1lOSWY1aHN6RkY4\nMFQyTmdmeDI1Wk9qZ3lnZmc4d2tmTjgKLS0tIDA0eEpCZzJ0T2lXaXY0bWc1UFM3\nL3k2Vi9vbFh5eWxnUm10eDN6bHp2cUUKFx9NstG3M+zWeu1PKjmE90iOLhPfDm9c\n0onKUXaBH5HZugiXj5TXsL/zsHRUdCsQkHv6LODSzqsnAIrc53P6jw==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSUGxjanp3SGw5SWdIUTFV\nclpUM2lDMFA1aVVSdzlJQjdYSkx1ZjBVZlhvCnFSekhRZGYrSjNyWjR2aStHdy9V\ncVhXRC9kRXZQY2Q2TE9jYm44MXVYQ3MKLS0tICs2cGZxbk9vVUg0cUJpT0dKSkxM\nbUtvNmJUYngyRVUzeGZwUG9jQnBMS3cKuUUGbnVjIpLh/xttjVKuNScdpVoXMj2V\nIj2UWXRytJQ7l28b++iR8jNIPhZ1dYjYdBcC+Ov/qL441P+4z0J5jw==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_3__map_recipient=age1vfhyk6wmt993dezz5wjf6n3ynkd6xptv2dr0qdl6kmttev9lh5dsfjfs3h -sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2VGNTa0ExbHRNQjYrNDd4\ndlgrMEdJVExIUzFnUTFOY21iTHIwOWVuUzFBCjEwQm12TnJKdzA0QjNpSWxtSTM4\nSDh3ZmdDandubVlETUVoWi96VEtDbXcKLS0tIDZESHMxMkVsZllteFlQN2sxRDFH\nOGV0bXVna0Exdk9ScFdvVTY5SVA1djAKdQZNKiezS5Lx83Pkw2zFE8VpBn6ImuGU\nvd4uYrtSe8flMh14hoCO8zVnIJW/J4jKQtrLryydOn/3JdGU7I0S0g==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLZXg3YXZCS0R2TFJiQlo4\nRDV3cnF4MndxaGhoYjY4TGtRL3hkZkxpZ2o4Ck1KeVJiL1pDUWoyekhLZjA5L2JH\nQkZXYzBZM0hOTndJY0d3RWgxUUIzWE0KLS0tIENseVVqVEkyK05WbEgzNFNEZ0c5\nWXhva2hvK3E0MG84b1EvZGtmd2JPOG8KJ4ZbZk8+YvZuwFqGl9HyJ+OL+RZELw8q\nlXEOhQQSVFroOXfCrvdKqlzh9pPJB1KYhF8Cg3I37xo8w8V0lKe1Tw==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_4__map_recipient=age1jwuvzyghfer7rx3qtqa4vs0gxyff0sg6pqgmqvp5zlhnpmvrkdlsdha4kx -sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5dmdadXl3RE1BZkZ3amlw\nRUQ0dVBGYnp3TGZHa3o5d0FneFFZampiaTFnCmd6Z2x0alJwODJMaDlYR08vbkFa\nRnJKNTNWUHpPWk1kWW8yUHNxbFJhWlkKLS0tIFNzcVJWcUVqbTZRb2E3UzNoZlow\nYi9sa1lBSmhLMjdRQXRtQUlBTlN4Z0UK29CesxlXkkXVXGvfg4sTwDrljeSrDxxq\nblF7jGpIPtMjuTYuhVfEX9vIjIq9Fv6HEI8EVOc4BpwBEDp7wY9PvQ==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTbUxrSmxMWFBuM004Rlp6\ncFNiMXRUYS9zL2ZXbk5FU2s3bHZQRTE4NUhRCmVsY0JFcXdvVThxQVBxSUl1TEpD\nazdhOUR0TWFwcWxEUXIvVEREVnRXV3MKLS0tIHFLVklZbUlnTVR3d010QnJyc2J3\nTmZQTVdLSy9GUzZHbmFOeHM1OVl3d0EKEiMWU2NfMjJUJnR0IHCUUbfkJJQ4lhZN\nbSNEIzQc489/Px5OxLmejRR5u9y7+Q244WUyfBp4z5pCU2HPWRIepw==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_5__map_recipient=age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy -sops_lastmodified=2026-04-15T17:44:42Z -sops_mac=ENC[AES256_GCM,data:0oQJu5w+tn6pDTAaRSYs9qbu9QrQzw/xsOYCWVRrkzCOizJuDWWgV1f0GOW8751NH0WqHW36PZ1fXuHWX1PB71egN25RW5twjhl6ExEkOg98jqWtZpkhtnp5pVDe4Tz0SA/ZRyPuGp5tkknZgVI+n2PAz24NFpKxOZ68ceE6tlo=,iv:JNT5xJeufeda/ZufmATtvo8xbnNeZIpHckfnDp3jkP0=,tag:U9ckKGYMWRZfGH8AJfcAQQ==,type:str] +sops_lastmodified=2026-04-16T02:09:06Z +sops_mac=ENC[AES256_GCM,data:Kj0EPA2ce2M75u/YpfyXy6sHcUhm1apYe5lXDPwJNYS5m8DpGZL6jrr+egrFx2+egQfFHBu+wihCQ5QO7ygbKyEcY6he26vJvs4Ej1p+//EtoTSQDUp8cpO1xl+rCrmwRVUVHq6ky/S9vU0KuPIAekmyzh02D/glRMEsWg9N880=,iv:ollJafWW4vpPeg5H0kE/osULdT/h1DayERIEKns8+lQ=,tag:4I0JUYwEfxVr6fVVGh43hw==,type:str] sops_unencrypted_suffix=_unencrypted sops_version=3.11.0 diff --git a/infra/.env.test b/infra/.env.test index 6664540dd5..e4a730bf24 100644 --- a/infra/.env.test +++ b/infra/.env.test @@ -35,7 +35,6 @@ DATACITE_DEPOSIT_URL=https://deposit.com SLACK_WEBHOOK_URL=https://slack.com SENTRY_AUTH_TOKEN=xxx SENTRY_ORG=xxx -METABASE_SECRET_KEY=xxx STITCH_WEBHOOK_URL=https://xxxxxxxxxxxxxxxxxxxx.com # AWS_ACCESS_KEY_ID: Required # AWS_SECRET_ACCESS_KEY: Required @@ -62,5 +61,4 @@ STITCH_WEBHOOK_URL=https://xxxxxxxxxxxxxxxxxxxx.com # SLACK_WEBHOOK_URL: Required # SENTRY_AUTH_TOKEN: Required # SENTRY_ORG: Required -# METABASE_SECRET_KEY: Required # STITCH_WEBHOOK_URL: Required \ No newline at end of file diff --git a/infra/ENV.md b/infra/ENV.md index 070e3b68e8..78cbf85134 100644 --- a/infra/ENV.md +++ b/infra/ENV.md @@ -59,7 +59,6 @@ All environment variables used by PubPub, with types, defaults, and descriptions | `PURGE_TOKEN` | string | No | — | Legacy Fastly purge token | | `SLACK_WEBHOOK_URL` | string | No | — | Slack incoming webhook URL for notifications | | `STITCH_WEBHOOK_URL` | string | No | — | MongoDB Stitch webhook URL for analytics | -| `METABASE_SECRET_KEY` | string | No | — | Metabase embedding secret key | | `SENTRY_AUTH_TOKEN` | string | No | — | Sentry auth token (build-time only) | | `SENTRY_ORG` | string | No | — | Sentry organization slug | | `BLOCKLIST_IP_ADDRESSES` | string | No | — | Comma-separated list of IP addresses to block | diff --git a/package.json b/package.json index 3d86f1e2ff..e3d22c1336 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,6 @@ "google-auth-library": "^9.7.0", "graphlib": "^2.1.8", "html-minifier": "^4.0.0", - "iframe-resizer-react": "1.1.0", "install": "^0.12.2", "is-url": "^1.2.4", "js-beautify": "^1.11.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e96111a5f..69191cd279 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,7 @@ settings: patchedDependencies: reakit: - hash: 31488077229fe3d19a71c66458b888c58eab18010632a29e3b5c485df77242a8 + hash: hpw5k5ors3jxzufoxbjqeo4iee path: patches/reakit.patch importers: @@ -525,7 +525,7 @@ importers: version: 1.0.9(react@16.14.0) reakit: specifier: 1.0.0-beta.14 - version: 1.0.0-beta.14(patch_hash=31488077229fe3d19a71c66458b888c58eab18010632a29e3b5c485df77242a8)(react-dom@16.14.0(react@16.14.0))(react@16.14.0) + version: 1.0.0-beta.14(patch_hash=hpw5k5ors3jxzufoxbjqeo4iee)(react-dom@16.14.0(react@16.14.0))(react@16.14.0) rebound: specifier: ^0.1.0 version: 0.1.0 @@ -23469,11 +23469,11 @@ snapshots: dependencies: picomatch: 2.3.1 - reakit-system@0.7.2(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(reakit-utils@0.7.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(reakit@1.0.0-beta.14(patch_hash=31488077229fe3d19a71c66458b888c58eab18010632a29e3b5c485df77242a8)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)): + reakit-system@0.7.2(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(reakit-utils@0.7.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(reakit@1.0.0-beta.14(patch_hash=hpw5k5ors3jxzufoxbjqeo4iee)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)): dependencies: react: 16.14.0 react-dom: 16.14.0(react@16.14.0) - reakit: 1.0.0-beta.14(patch_hash=31488077229fe3d19a71c66458b888c58eab18010632a29e3b5c485df77242a8)(react-dom@16.14.0(react@16.14.0))(react@16.14.0) + reakit: 1.0.0-beta.14(patch_hash=hpw5k5ors3jxzufoxbjqeo4iee)(react-dom@16.14.0(react@16.14.0))(react@16.14.0) reakit-utils: 0.7.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) reakit-utils@0.7.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0): @@ -23481,13 +23481,13 @@ snapshots: react: 16.14.0 react-dom: 16.14.0(react@16.14.0) - reakit@1.0.0-beta.14(patch_hash=31488077229fe3d19a71c66458b888c58eab18010632a29e3b5c485df77242a8)(react-dom@16.14.0(react@16.14.0))(react@16.14.0): + reakit@1.0.0-beta.14(patch_hash=hpw5k5ors3jxzufoxbjqeo4iee)(react-dom@16.14.0(react@16.14.0))(react@16.14.0): dependencies: body-scroll-lock: 2.7.1 popper.js: 1.16.1 react: 16.14.0 react-dom: 16.14.0(react@16.14.0) - reakit-system: 0.7.2(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(reakit-utils@0.7.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(reakit@1.0.0-beta.14(patch_hash=31488077229fe3d19a71c66458b888c58eab18010632a29e3b5c485df77242a8)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) + reakit-system: 0.7.2(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(reakit-utils@0.7.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(reakit@1.0.0-beta.14(patch_hash=hpw5k5ors3jxzufoxbjqeo4iee)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) reakit-utils: 0.7.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) rebound@0.1.0: {} diff --git a/server/apiRoutes.ts b/server/apiRoutes.ts index 4aff0caabf..dbc26ba3c6 100644 --- a/server/apiRoutes.ts +++ b/server/apiRoutes.ts @@ -17,6 +17,7 @@ import { router as draftCheckpointRouter } from './draftCheckpoint/api'; import { router as editorRouter } from './editor/api'; import { env } from './env'; import { router as impact2Router } from './impact2/api'; +import { router as platformAnalyticsRouter } from './platformAnalytics/api'; import { router as integrationDataOAuth1Router } from './integrationDataOAuth1/api'; import { router as landingPageFeatureRouter } from './landingPageFeature/api'; import { router as layoutRouter } from './layout/api'; @@ -79,6 +80,7 @@ const apiRouter = Router() .use(zoteroIntegrationRouter) .use(analyticsImpactRouter) .use(impact2Router) + .use(platformAnalyticsRouter) .use(apiDocsRouter); if (!isProd() && env.NODE_ENV !== 'test') { diff --git a/server/envSchema.ts b/server/envSchema.ts index 692beaea06..2626678cbb 100644 --- a/server/envSchema.ts +++ b/server/envSchema.ts @@ -150,7 +150,6 @@ export const envSchema = z.object({ SENTRY_ORG: z.string().describe('Sentry organization slug'), // ── Analytics ─────────────────────────────────────────────────────── - METABASE_SECRET_KEY: z.string().describe('Metabase embedding secret key'), STITCH_WEBHOOK_URL: z.string().describe('MongoDB Stitch webhook URL for analytics'), CLOUDFLARE_ANALYTICS_API_TOKEN: z .string() diff --git a/server/platformAnalytics/api.ts b/server/platformAnalytics/api.ts new file mode 100644 index 0000000000..e767114f87 --- /dev/null +++ b/server/platformAnalytics/api.ts @@ -0,0 +1,217 @@ +import { Router } from 'express'; +import { QueryTypes } from 'sequelize'; + +import { sequelize } from 'server/sequelize'; +import { ForbiddenError, handleErrors } from 'server/utils/errors'; +import { getInitialData } from 'server/utils/initData'; + +export const router = Router(); + +// ── In-memory cache (1 hour TTL) ──────────────────────────────────── +const CACHE_TTL_MS = 60 * 60 * 1000; +let cachedResult: { data: any; expiresAt: number } | null = null; + +async function fetchAnalytics() { + const [ + totalCommunities, + totalUsers, + totalPubs, + totalPageviews, + communitiesByMonth, + usersByMonth, + pubsByMonth, + pageviewsByMonth, + activeCommunityTrendActivity, + activeCommunityTrendPubs, + ] = await Promise.all([ + // Total counts — use pg_class estimate for the huge AnalyticsEvents + // table; Communities / Users / Pubs are small enough for exact counts. + sequelize.query<{ count: string }>( + `SELECT COUNT(*) AS count FROM "Communities"`, + { type: QueryTypes.SELECT }, + ), + sequelize.query<{ count: string }>( + `SELECT COUNT(*) AS count FROM "Users"`, + { type: QueryTypes.SELECT }, + ), + sequelize.query<{ count: string }>( + `SELECT COUNT(*) AS count FROM "Pubs"`, + { type: QueryTypes.SELECT }, + ), + sequelize.query<{ count: string }>( + `SELECT COALESCE(SUM(page_views), 0)::bigint AS count + FROM analytics_daily_summary`, + { type: QueryTypes.SELECT }, + ), + // Monthly growth series + sequelize.query<{ month: string; count: string }>( + `SELECT DATE_TRUNC('month', "createdAt") AS month, COUNT(*) AS count + FROM "Communities" + GROUP BY DATE_TRUNC('month', "createdAt") + ORDER BY month`, + { type: QueryTypes.SELECT }, + ), + sequelize.query<{ month: string; count: string }>( + `SELECT DATE_TRUNC('month', "createdAt") AS month, COUNT(*) AS count + FROM "Users" + GROUP BY DATE_TRUNC('month', "createdAt") + ORDER BY month`, + { type: QueryTypes.SELECT }, + ), + sequelize.query<{ month: string; count: string }>( + `SELECT DATE_TRUNC('month', "createdAt") AS month, COUNT(*) AS count + FROM "Pubs" + GROUP BY DATE_TRUNC('month', "createdAt") + ORDER BY month`, + { type: QueryTypes.SELECT }, + ), + // Pageviews by month — read from the pre-aggregated materialized view + sequelize.query<{ month: string; count: string }>( + `SELECT DATE_TRUNC('month', date) AS month, + SUM(page_views)::bigint AS count + FROM analytics_daily_summary + GROUP BY DATE_TRUNC('month', date) + ORDER BY month`, + { type: QueryTypes.SELECT }, + ), + // Active communities by activity items per month + sequelize.query<{ month: string; count: string }>( + `SELECT DATE_TRUNC('month', "timestamp") AS month, + COUNT(DISTINCT "communityId") AS count + FROM "ActivityItems" + GROUP BY DATE_TRUNC('month', "timestamp") + ORDER BY month`, + { type: QueryTypes.SELECT }, + ), + // Active communities by pub creation per month + sequelize.query<{ month: string; count: string }>( + `SELECT DATE_TRUNC('month', p."createdAt") AS month, + COUNT(DISTINCT p."communityId") AS count + FROM "Pubs" p + GROUP BY DATE_TRUNC('month', p."createdAt") + ORDER BY month`, + { type: QueryTypes.SELECT }, + ), + ]); + + const toSeries = (rows: { month: string; count: string }[]) => + rows.map((r) => ({ month: r.month, count: Number(r.count) })); + + return { + totals: { + communities: Number(totalCommunities[0].count), + users: Number(totalUsers[0].count), + pubs: Number(totalPubs[0].count), + pageviews: Number(totalPageviews[0]?.count ?? 0), + }, + communitiesByMonth: toSeries(communitiesByMonth), + usersByMonth: toSeries(usersByMonth), + pubsByMonth: toSeries(pubsByMonth), + pageviewsByMonth: toSeries(pageviewsByMonth), + activeCommunityTrendActivity: toSeries(activeCommunityTrendActivity), + activeCommunityTrendPubs: toSeries(activeCommunityTrendPubs), + }; +} + +router.get('/api/platformAnalytics', async (req, res, next) => { + try { + const initialData = await getInitialData(req); + if (!initialData.loginData.isSuperAdmin) { + throw new ForbiddenError(); + } + + const now = Date.now(); + if (cachedResult && cachedResult.expiresAt > now) { + return res.json(cachedResult.data); + } + + const data = await fetchAnalytics(); + cachedResult = { data, expiresAt: now + CACHE_TTL_MS }; + return res.json(data); + } catch (err) { + return handleErrors(req, res, next)(err); + } +}); + +// ── Period drill-down (date-range filtered counts + paginated communities) ── + +const PERIOD_PAGE_SIZE = 25; + +router.get('/api/platformAnalytics/period', async (req, res, next) => { + try { + const initialData = await getInitialData(req); + if (!initialData.loginData.isSuperAdmin) { + throw new ForbiddenError(); + } + + const startDate = String(req.query.startDate ?? ''); + const endDate = String(req.query.endDate ?? ''); + if (!/^\d{4}-\d{2}-\d{2}$/.test(startDate) || !/^\d{4}-\d{2}-\d{2}$/.test(endDate)) { + return res.status(400).json({ error: 'startDate and endDate are required (YYYY-MM-DD)' }); + } + + const page = Math.max(0, parseInt(String(req.query.page ?? '0'), 10) || 0); + const offset = page * PERIOD_PAGE_SIZE; + + const [communities, users, pubs, pageviews, newCommunitiesRows, totalNewCommunities] = + await Promise.all([ + sequelize.query<{ count: string }>( + `SELECT COUNT(*) AS count FROM "Communities" + WHERE "createdAt" >= :startDate::date AND "createdAt" < :endDate::date + INTERVAL '1 day'`, + { type: QueryTypes.SELECT, replacements: { startDate, endDate } }, + ), + sequelize.query<{ count: string }>( + `SELECT COUNT(*) AS count FROM "Users" + WHERE "createdAt" >= :startDate::date AND "createdAt" < :endDate::date + INTERVAL '1 day'`, + { type: QueryTypes.SELECT, replacements: { startDate, endDate } }, + ), + sequelize.query<{ count: string }>( + `SELECT COUNT(*) AS count FROM "Pubs" + WHERE "createdAt" >= :startDate::date AND "createdAt" < :endDate::date + INTERVAL '1 day'`, + { type: QueryTypes.SELECT, replacements: { startDate, endDate } }, + ), + sequelize.query<{ count: string }>( + `SELECT COALESCE(SUM(page_views), 0)::bigint AS count + FROM analytics_daily_summary + WHERE date >= :startDate::date AND date <= :endDate::date`, + { type: QueryTypes.SELECT, replacements: { startDate, endDate } }, + ), + sequelize.query<{ + title: string; + subdomain: string; + createdAt: string; + description: string; + }>( + `SELECT "title", "subdomain", "createdAt", COALESCE("description", '') AS description + FROM "Communities" + WHERE "createdAt" >= :startDate::date AND "createdAt" < :endDate::date + INTERVAL '1 day' + ORDER BY "createdAt" DESC + LIMIT :limit OFFSET :offset`, + { + type: QueryTypes.SELECT, + replacements: { startDate, endDate, limit: PERIOD_PAGE_SIZE, offset }, + }, + ), + sequelize.query<{ count: string }>( + `SELECT COUNT(*) AS count FROM "Communities" + WHERE "createdAt" >= :startDate::date AND "createdAt" < :endDate::date + INTERVAL '1 day'`, + { type: QueryTypes.SELECT, replacements: { startDate, endDate } }, + ), + ]); + + return res.json({ + counts: { + communities: Number(communities[0].count), + users: Number(users[0].count), + pubs: Number(pubs[0].count), + pageviews: Number(pageviews[0]?.count ?? 0), + }, + newCommunities: newCommunitiesRows, + totalNewCommunities: Number(totalNewCommunities[0].count), + page, + pageSize: PERIOD_PAGE_SIZE, + }); + } catch (err) { + return handleErrors(req, res, next)(err); + } +}); diff --git a/server/routes/adminDashboard.tsx b/server/routes/adminDashboard.tsx index 575edf08c0..c3976ea104 100644 --- a/server/routes/adminDashboard.tsx +++ b/server/routes/adminDashboard.tsx @@ -1,58 +1,9 @@ -import type { User } from 'types'; - -import React from 'react'; - import { Router } from 'express'; -import Html from 'server/Html'; -import { handleErrors } from 'server/utils/errors'; -import { getInitialData } from 'server/utils/initData'; -import { generateMetabaseToken } from 'server/utils/metabase'; -import { hostIsValid } from 'server/utils/routes'; -import { generateMetaComponents, renderToNodeStream } from 'server/utils/ssr'; - export const router = Router(); -router.get('/admin', (req, res, next) => { - if (!hostIsValid(req, 'pubpub')) { - return next(); - } - - return getInitialData(req) - .then((initialData) => { - const user = req.user || {}; - const users = [ - 'b242f616-7aaa-479c-8ee5-3933dcf70859', - '5d9d63b3-6990-407c-81fb-5f87b9d3e360', - '807f3604-4223-4495-b576-861d04d2f39e', - '237fe275-0618-4a8f-bd40-ea9065836e67', - '06f7d120-391e-4d71-8725-dcd3e9b9af44', - '408952a0-58a6-42df-86b8-d7fe3ab6ba43', - 'c575d640-4d72-4197-bc32-0cf383a34a4f', - 'af262062-bb64-402c-8cfc-165c27c2788a', - '4d506b24-80b6-462c-85f0-f237e08010d5', - '6ddd7a7b-c542-4127-b403-33a67eb40ff3', - 'd29eed4d-d754-4f8c-975e-b34826e6b8d2', - ]; - if (!users.includes((user as User).id)) { - throw new Error('Page Not Found'); - } - const impactData = { - baseToken: generateMetabaseToken('pubpub', null, 'base'), - }; - return renderToNodeStream( - res, - , - ); - }) - .catch(handleErrors(req, res, next)); +// Redirect legacy /admin to the superadmin analytics tab +router.get('/admin', (_, res) => { + return res.redirect(301, '/superadmin/analytics'); }); + diff --git a/server/routes/index.ts b/server/routes/index.ts index 458b5f9c9d..6c57f4bd64 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -34,7 +34,8 @@ import { router as submitRouter } from './submit'; /* import { router as picingRouter} from './picing'); // Route: '/pricing' */ -import { router as adminDashboardRouter } from './adminDashboard'; // Route: '/admin' + +import { router as adminDashboardRouter } from './adminDashboard'; // Route: '/admin' (redirect to superadmin) import { router as authenticateRouter } from './authenticate'; // Route: '/auth' import { router as landingRouter } from './landing'; // Route: '/' import { router as legalRouter } from './legal'; // Route: '/legal' diff --git a/server/utils/metabase.ts b/server/utils/metabase.ts deleted file mode 100644 index 2bc41b79f4..0000000000 --- a/server/utils/metabase.ts +++ /dev/null @@ -1,72 +0,0 @@ -import jwt from 'jsonwebtoken'; - -import { env } from 'server/env'; - -const dashboardNums = { - community: { - type: 'community', - base: { id: 2 }, - new: { id: 28 }, - benchmark: { id: 8 }, - }, - collection: { - type: 'collection', - base: { id: 7 }, - new: { - id: 32, - options: { - event: 'collection', - }, - }, - }, - pub: { - base: { id: 3 }, - new: { id: 30 }, - benchmark: { id: 9 }, - type: 'pub', - }, - pubpub: { - base: { id: 6 }, - benchmark: { id: 10 }, - type: 'pubpub', - }, -} as const; - -type DashboardNums = typeof dashboardNums; - -export const generateMetabaseToken = ( - scopeType: T, - scopeId: string | null, - dashboardType: T extends T ? keyof DashboardNums[T] : never, - isProd?: boolean, -) => { - const dashboardNum = dashboardNums[scopeType][dashboardType]; - - if (!dashboardNum) { - return null; - } - - const payload = { - // @ts-expect-error - resource: { dashboard: dashboardNum?.id }, - params: { - [scopeType]: scopeId, - ...((scopeType === 'collection' && - dashboardType === 'new' && - // @ts-expect-error - 'options' in dashboardNum && - dashboardNum.options) || - {}), - ...(isProd !== undefined && { is_prod: isProd }), - }, - exp: Math.round(Date.now() / 1000) + 10 * 60, // 10 minute expiration - }; - - const metabaseSecretKey = env.METABASE_SECRET_KEY; - - if (!metabaseSecretKey) { - throw new Error('METABASE_SECRET_KEY environment variable not set'); - } - - return jwt.sign(payload, metabaseSecretKey); -}; diff --git a/utils/analytics/usePageOnce.ts b/utils/analytics/usePageOnce.ts index e7c12b7aeb..e1781bfcaa 100644 --- a/utils/analytics/usePageOnce.ts +++ b/utils/analytics/usePageOnce.ts @@ -46,7 +46,7 @@ const determinePayload = ( // UUID in the array, which creates a ton of events for a single page view if the pub // is in a lot of collections // much easier to just have a single event with a string of UUIDs and then do some - // processing in Metabase + // processing in analytics queries const collectionIds = uniqueCollectionIds.join(',') || undefined; const collection = chooseCollectionForPub(pubData, locationData); diff --git a/utils/superAdmin.ts b/utils/superAdmin.ts index 88a9b86e44..864ae52c08 100644 --- a/utils/superAdmin.ts +++ b/utils/superAdmin.ts @@ -1,4 +1,4 @@ -export const superAdminTabKinds = ['landingPageFeatures', 'spam', 'spamUsers'] as const; +export const superAdminTabKinds = ['analytics', 'landingPageFeatures', 'spam', 'spamUsers'] as const; export const getSuperAdminTabUrl = (tabKind: SuperAdminTabKind) => { return `/superadmin/${tabKind}` as const; From 1cf7c50a5145501f1e495a069764504a244aa6b2 Mon Sep 17 00:00:00 2001 From: Travis Rich Date: Wed, 15 Apr 2026 22:10:26 -0400 Subject: [PATCH 2/4] lint --- .../SuperAdminDashboard/PlatformAnalytics.tsx | 81 +++++++++++++------ server/apiRoutes.ts | 2 +- server/platformAnalytics/api.ts | 25 +++--- server/routes/adminDashboard.tsx | 1 - server/routes/index.ts | 1 - utils/superAdmin.ts | 7 +- 6 files changed, 76 insertions(+), 41 deletions(-) diff --git a/client/containers/SuperAdminDashboard/PlatformAnalytics.tsx b/client/containers/SuperAdminDashboard/PlatformAnalytics.tsx index 153c2a041f..8ab905184c 100644 --- a/client/containers/SuperAdminDashboard/PlatformAnalytics.tsx +++ b/client/containers/SuperAdminDashboard/PlatformAnalytics.tsx @@ -74,7 +74,9 @@ const COLORS = { }; /** Convert raw monthly counts into a cumulative series. */ -function toCumulative(series: MonthlySeries): { month: string; count: number; cumulative: number }[] { +function toCumulative( + series: MonthlySeries, +): { month: string; count: number; cumulative: number }[] { let total = 0; return series.map((d) => { total += d.count; @@ -155,7 +157,10 @@ const ActiveCommunityChart = ({ pubData: MonthlySeries; }) => { // Merge the two series by month - const monthMap = new Map(); + const monthMap = new Map< + string, + { month: string; byActivity: number; byPubCreation: number } + >(); for (const d of activityData) { monthMap.set(d.month, { month: d.month, byActivity: d.count, byPubCreation: 0 }); } @@ -232,20 +237,17 @@ const PeriodExplorer = () => { const [loading, setLoading] = useState(false); const [page, setPage] = useState(0); - const fetchPeriod = useCallback( - async (sd: string, ed: string, pg: number) => { - setLoading(true); - try { - const res = await apiFetch.get( - `/api/platformAnalytics/period?startDate=${sd}&endDate=${ed}&page=${pg}`, - ); - setPeriod(res as PeriodData); - } finally { - setLoading(false); - } - }, - [], - ); + const fetchPeriod = useCallback(async (sd: string, ed: string, pg: number) => { + setLoading(true); + try { + const res = await apiFetch.get( + `/api/platformAnalytics/period?startDate=${sd}&endDate=${ed}&page=${pg}`, + ); + setPeriod(res as PeriodData); + } finally { + setLoading(false); + } + }, []); // Fetch on mount and whenever dates or page change useEffect(() => { @@ -430,7 +432,10 @@ const PlatformAnalytics = () => { const [error, setError] = useState(null); useEffect(() => { - apiFetch.get('/api/platformAnalytics').then(setData).catch((err) => setError(err.message)); + apiFetch + .get('/api/platformAnalytics') + .then(setData) + .catch((err) => setError(err.message)); }, []); if (error) { @@ -512,26 +517,54 @@ const PlatformAnalytics = () => {
All-Time Totals
- - + + - +
Cumulative Growth
- + - +
Monthly Growth
- + - +
Active Communities
diff --git a/server/apiRoutes.ts b/server/apiRoutes.ts index dbc26ba3c6..eea8124ceb 100644 --- a/server/apiRoutes.ts +++ b/server/apiRoutes.ts @@ -17,12 +17,12 @@ import { router as draftCheckpointRouter } from './draftCheckpoint/api'; import { router as editorRouter } from './editor/api'; import { env } from './env'; import { router as impact2Router } from './impact2/api'; -import { router as platformAnalyticsRouter } from './platformAnalytics/api'; import { router as integrationDataOAuth1Router } from './integrationDataOAuth1/api'; import { router as landingPageFeatureRouter } from './landingPageFeature/api'; import { router as layoutRouter } from './layout/api'; import { router as openSearchRouter } from './openSearch/api'; import { router as passwordResetRouter } from './passwordReset/api'; +import { router as platformAnalyticsRouter } from './platformAnalytics/api'; import { router as pubEdgeProposalRouter } from './pubEdgeProposal/api'; import { router as pubHistoryRouter } from './pubHistory/api'; import { router as reviewRouter } from './review/api'; diff --git a/server/platformAnalytics/api.ts b/server/platformAnalytics/api.ts index e767114f87..bf4ae353db 100644 --- a/server/platformAnalytics/api.ts +++ b/server/platformAnalytics/api.ts @@ -26,18 +26,15 @@ async function fetchAnalytics() { ] = await Promise.all([ // Total counts — use pg_class estimate for the huge AnalyticsEvents // table; Communities / Users / Pubs are small enough for exact counts. - sequelize.query<{ count: string }>( - `SELECT COUNT(*) AS count FROM "Communities"`, - { type: QueryTypes.SELECT }, - ), - sequelize.query<{ count: string }>( - `SELECT COUNT(*) AS count FROM "Users"`, - { type: QueryTypes.SELECT }, - ), - sequelize.query<{ count: string }>( - `SELECT COUNT(*) AS count FROM "Pubs"`, - { type: QueryTypes.SELECT }, - ), + sequelize.query<{ count: string }>(`SELECT COUNT(*) AS count FROM "Communities"`, { + type: QueryTypes.SELECT, + }), + sequelize.query<{ count: string }>(`SELECT COUNT(*) AS count FROM "Users"`, { + type: QueryTypes.SELECT, + }), + sequelize.query<{ count: string }>(`SELECT COUNT(*) AS count FROM "Pubs"`, { + type: QueryTypes.SELECT, + }), sequelize.query<{ count: string }>( `SELECT COALESCE(SUM(page_views), 0)::bigint AS count FROM analytics_daily_summary`, @@ -147,7 +144,9 @@ router.get('/api/platformAnalytics/period', async (req, res, next) => { const startDate = String(req.query.startDate ?? ''); const endDate = String(req.query.endDate ?? ''); if (!/^\d{4}-\d{2}-\d{2}$/.test(startDate) || !/^\d{4}-\d{2}-\d{2}$/.test(endDate)) { - return res.status(400).json({ error: 'startDate and endDate are required (YYYY-MM-DD)' }); + return res + .status(400) + .json({ error: 'startDate and endDate are required (YYYY-MM-DD)' }); } const page = Math.max(0, parseInt(String(req.query.page ?? '0'), 10) || 0); diff --git a/server/routes/adminDashboard.tsx b/server/routes/adminDashboard.tsx index c3976ea104..e475f63e71 100644 --- a/server/routes/adminDashboard.tsx +++ b/server/routes/adminDashboard.tsx @@ -6,4 +6,3 @@ export const router = Router(); router.get('/admin', (_, res) => { return res.redirect(301, '/superadmin/analytics'); }); - diff --git a/server/routes/index.ts b/server/routes/index.ts index 6c57f4bd64..4d7501bb21 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -34,7 +34,6 @@ import { router as submitRouter } from './submit'; /* import { router as picingRouter} from './picing'); // Route: '/pricing' */ - import { router as adminDashboardRouter } from './adminDashboard'; // Route: '/admin' (redirect to superadmin) import { router as authenticateRouter } from './authenticate'; // Route: '/auth' import { router as landingRouter } from './landing'; // Route: '/' diff --git a/utils/superAdmin.ts b/utils/superAdmin.ts index 864ae52c08..9768ed4df0 100644 --- a/utils/superAdmin.ts +++ b/utils/superAdmin.ts @@ -1,4 +1,9 @@ -export const superAdminTabKinds = ['analytics', 'landingPageFeatures', 'spam', 'spamUsers'] as const; +export const superAdminTabKinds = [ + 'analytics', + 'landingPageFeatures', + 'spam', + 'spamUsers', +] as const; export const getSuperAdminTabUrl = (tabKind: SuperAdminTabKind) => { return `/superadmin/${tabKind}` as const; From be7833d47a9e979067bb80263535c04eb3bf5b80 Mon Sep 17 00:00:00 2001 From: Travis Rich Date: Wed, 15 Apr 2026 22:50:40 -0400 Subject: [PATCH 3/4] Handle spam data --- .../SuperAdminDashboard/PlatformAnalytics.tsx | 182 +++++++++++++---- .../platformAnalytics.scss | 22 ++ server/platformAnalytics/api.ts | 188 +++++++++++------- 3 files changed, 284 insertions(+), 108 deletions(-) diff --git a/client/containers/SuperAdminDashboard/PlatformAnalytics.tsx b/client/containers/SuperAdminDashboard/PlatformAnalytics.tsx index 8ab905184c..4276675a7b 100644 --- a/client/containers/SuperAdminDashboard/PlatformAnalytics.tsx +++ b/client/containers/SuperAdminDashboard/PlatformAnalytics.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { Button, ButtonGroup, NonIdealState } from '@blueprintjs/core'; +import { Button, ButtonGroup, Checkbox, NonIdealState, Tag } from '@blueprintjs/core'; import { Area, AreaChart, @@ -18,35 +18,32 @@ import { apiFetch } from 'client/utils/apiFetch'; import './platformAnalytics.scss'; -type MonthlySeries = { month: string; count: number }[]; +type MonthlySeriesRow = { month: string; count: number; spam: number }; +type MonthlySeries = MonthlySeriesRow[]; +type SimpleSeriesRow = { month: string; count: number }; + +type Totals = { communities: number; users: number; pubs: number; pageviews: number }; type AnalyticsData = { - totals: { - communities: number; - users: number; - pubs: number; - pageviews: number; - }; + totals: Totals; + spam: Totals; communitiesByMonth: MonthlySeries; usersByMonth: MonthlySeries; pubsByMonth: MonthlySeries; pageviewsByMonth: MonthlySeries; - activeCommunityTrendActivity: MonthlySeries; - activeCommunityTrendPubs: MonthlySeries; + activeCommunityTrendActivity: SimpleSeriesRow[]; + activeCommunityTrendPubs: SimpleSeriesRow[]; }; type PeriodData = { - counts: { - communities: number; - users: number; - pubs: number; - pageviews: number; - }; + counts: Totals; + spam: Totals; newCommunities: { title: string; subdomain: string; createdAt: string; description: string; + isSpam: boolean; }[]; totalNewCommunities: number; page: number; @@ -74,19 +71,40 @@ const COLORS = { }; /** Convert raw monthly counts into a cumulative series. */ -function toCumulative( - series: MonthlySeries, -): { month: string; count: number; cumulative: number }[] { +function toCumulative(series: MonthlySeries) { let total = 0; + let spamTotal = 0; return series.map((d) => { total += d.count; - return { month: d.month, count: d.count, cumulative: total }; + spamTotal += d.spam; + return { + month: d.month, + count: d.count, + spam: d.spam, + cumulative: total, + cumulativeSpam: spamTotal, + }; }); } -const ScalarCard = ({ label, value, color }: { label: string; value: string; color: string }) => ( +const ScalarCard = ({ + label, + value, + color, + spamValue, + showSpam, +}: { + label: string; + value: string; + color: string; + spamValue?: number; + showSpam?: boolean; +}) => (
{value}
+ {showSpam && spamValue !== undefined && ( +
(Spam: {fmt(spamValue)})
+ )}
{label}
); @@ -95,10 +113,12 @@ const GrowthChart = ({ title, data, color, + showSpam, }: { title: string; data: MonthlySeries; color: string; + showSpam?: boolean; }) => { const cumData = toCumulative(data); return ( @@ -111,7 +131,10 @@ const GrowthChart = ({ [fmt(v), 'Cumulative']} + formatter={(v: number, name: string) => [ + fmt(v), + name === 'cumulativeSpam' ? 'Spam' : 'Cumulative', + ]} /> + {showSpam && ( + + )}
@@ -130,10 +163,12 @@ const MonthlyChart = ({ title, data, color, + showSpam, }: { title: string; data: MonthlySeries; color: string; + showSpam?: boolean; }) => (

{title}

@@ -142,8 +177,24 @@ const MonthlyChart = ({ - [fmt(v), 'Count']} /> + [ + fmt(v), + name === 'spam' ? 'Spam' : 'Count', + ]} + /> + {showSpam && ( + + )}
@@ -153,8 +204,8 @@ const ActiveCommunityChart = ({ activityData, pubData, }: { - activityData: MonthlySeries; - pubData: MonthlySeries; + activityData: SimpleSeriesRow[]; + pubData: SimpleSeriesRow[]; }) => { // Merge the two series by month const monthMap = new Map< @@ -229,7 +280,7 @@ function presetRange(days: number) { // ── Period Explorer sub-component ─────────────────────────────────────────── -const PeriodExplorer = () => { +const PeriodExplorer = ({ showSpam }: { showSpam: boolean }) => { const [preset, setPreset] = useState('1y'); const [startDate, setStartDate] = useState(() => presetRange(365).startDate); const [endDate, setEndDate] = useState(() => presetRange(365).endDate); @@ -249,7 +300,6 @@ const PeriodExplorer = () => { } }, []); - // Fetch on mount and whenever dates or page change useEffect(() => { fetchPeriod(startDate, endDate, page); }, [startDate, endDate, page, fetchPeriod]); @@ -333,21 +383,29 @@ const PeriodExplorer = () => { label="New Communities" value={fmt(period.counts.communities)} color={COLORS.communities} + spamValue={period.spam.communities} + showSpam={showSpam} />
@@ -362,6 +420,7 @@ const PeriodExplorer = () => { Title Subdomain Created + {showSpam && Spam} Description @@ -388,6 +447,15 @@ const PeriodExplorer = () => { }, )} + {showSpam && ( + + {c.isSpam && ( + + Spam + + )} + + )} {c.description} ))} @@ -430,6 +498,7 @@ const PeriodExplorer = () => { const PlatformAnalytics = () => { const [data, setData] = useState(null); const [error, setError] = useState(null); + const [showSpam, setShowSpam] = useState(false); useEffect(() => { apiFetch @@ -509,11 +578,18 @@ const PlatformAnalytics = () => { ); } - const { totals } = data; + const { totals, spam } = data; return (
-

PubPub Platform Analytics

+
+

PubPub Platform Analytics

+ setShowSpam((e.target as HTMLInputElement).checked)} + label="Show spam" + /> +
All-Time Totals
@@ -521,17 +597,29 @@ const PlatformAnalytics = () => { label="Communities" value={fmt(totals.communities)} color={COLORS.communities} + spamValue={spam.communities} + showSpam={showSpam} /> + -
@@ -541,13 +629,25 @@ const PlatformAnalytics = () => { title="Communities" data={data.communitiesByMonth} color={COLORS.communities} + showSpam={showSpam} + /> + + - -
@@ -557,13 +657,25 @@ const PlatformAnalytics = () => { title="Community Growth" data={data.communitiesByMonth} color={COLORS.communities} + showSpam={showSpam} + /> + + - -
@@ -576,7 +688,7 @@ const PlatformAnalytics = () => {
Period Explorer
- +
); }; diff --git a/client/containers/SuperAdminDashboard/platformAnalytics.scss b/client/containers/SuperAdminDashboard/platformAnalytics.scss index e4b3fd7d87..53a5aa7de5 100644 --- a/client/containers/SuperAdminDashboard/platformAnalytics.scss +++ b/client/containers/SuperAdminDashboard/platformAnalytics.scss @@ -63,6 +63,22 @@ } // ── Layout ────────────────────────────────────────────────────────── + .analytics-header { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 1em; + + h3 { + margin: 0; + } + + .bp3-control { + margin: 0; + font-size: 13px; + } + } + h3 { margin-bottom: 1em; } @@ -110,6 +126,12 @@ color: #5c7080; margin-top: 2px; } + + .scalar-spam { + font-size: 12px; + color: #c23030; + margin-top: 2px; + } } .chart-row { diff --git a/server/platformAnalytics/api.ts b/server/platformAnalytics/api.ts index bf4ae353db..c58bca2082 100644 --- a/server/platformAnalytics/api.ts +++ b/server/platformAnalytics/api.ts @@ -11,12 +11,24 @@ export const router = Router(); const CACHE_TTL_MS = 60 * 60 * 1000; let cachedResult: { data: any; expiresAt: number } | null = null; +// ── Spam SQL fragments ────────────────────────────────────────────── +// "confirmed-spam" via spamTagId on Communities / Users directly. +const SPAM_TAG_IDS = `(SELECT id FROM "SpamTags" WHERE status = 'confirmed-spam')`; +const SPAM_COMMUNITY_IDS = `(SELECT id FROM "Communities" WHERE "spamTagId" IN ${SPAM_TAG_IDS})`; + +const IS_SPAM_COMMUNITY = `"spamTagId" IN ${SPAM_TAG_IDS}`; +const NOT_SPAM_COMMUNITY = `("spamTagId" IS NULL OR "spamTagId" NOT IN ${SPAM_TAG_IDS})`; +const IS_SPAM_USER = `"spamTagId" IN ${SPAM_TAG_IDS}`; +const NOT_SPAM_USER = `("spamTagId" IS NULL OR "spamTagId" NOT IN ${SPAM_TAG_IDS})`; +const IS_SPAM_VIA_COMMUNITY = `"communityId" IN ${SPAM_COMMUNITY_IDS}`; +const NOT_SPAM_VIA_COMMUNITY = `"communityId" NOT IN ${SPAM_COMMUNITY_IDS}`; + async function fetchAnalytics() { const [ - totalCommunities, - totalUsers, - totalPubs, - totalPageviews, + communityTotals, + userTotals, + pubTotals, + pageviewTotals, communitiesByMonth, usersByMonth, pubsByMonth, @@ -24,89 +36,105 @@ async function fetchAnalytics() { activeCommunityTrendActivity, activeCommunityTrendPubs, ] = await Promise.all([ - // Total counts — use pg_class estimate for the huge AnalyticsEvents - // table; Communities / Users / Pubs are small enough for exact counts. - sequelize.query<{ count: string }>(`SELECT COUNT(*) AS count FROM "Communities"`, { - type: QueryTypes.SELECT, - }), - sequelize.query<{ count: string }>(`SELECT COUNT(*) AS count FROM "Users"`, { - type: QueryTypes.SELECT, - }), - sequelize.query<{ count: string }>(`SELECT COUNT(*) AS count FROM "Pubs"`, { - type: QueryTypes.SELECT, - }), - sequelize.query<{ count: string }>( - `SELECT COALESCE(SUM(page_views), 0)::bigint AS count + // ── Total counts (clean + spam in one query each) ─────────────── + sequelize.query<{ clean: string; spam: string }>( + `SELECT COUNT(*) FILTER (WHERE ${NOT_SPAM_COMMUNITY}) AS clean, + COUNT(*) FILTER (WHERE ${IS_SPAM_COMMUNITY}) AS spam + FROM "Communities"`, + { type: QueryTypes.SELECT }, + ), + sequelize.query<{ clean: string; spam: string }>( + `SELECT COUNT(*) FILTER (WHERE ${NOT_SPAM_USER}) AS clean, + COUNT(*) FILTER (WHERE ${IS_SPAM_USER}) AS spam + FROM "Users"`, + { type: QueryTypes.SELECT }, + ), + sequelize.query<{ clean: string; spam: string }>( + `SELECT COUNT(*) FILTER (WHERE ${NOT_SPAM_VIA_COMMUNITY}) AS clean, + COUNT(*) FILTER (WHERE ${IS_SPAM_VIA_COMMUNITY}) AS spam + FROM "Pubs"`, + { type: QueryTypes.SELECT }, + ), + sequelize.query<{ clean: string; spam: string }>( + `SELECT COALESCE(SUM(page_views) FILTER (WHERE ${NOT_SPAM_VIA_COMMUNITY}), 0)::bigint AS clean, + COALESCE(SUM(page_views) FILTER (WHERE ${IS_SPAM_VIA_COMMUNITY}), 0)::bigint AS spam FROM analytics_daily_summary`, { type: QueryTypes.SELECT }, ), - // Monthly growth series - sequelize.query<{ month: string; count: string }>( - `SELECT DATE_TRUNC('month', "createdAt") AS month, COUNT(*) AS count + // ── Monthly series (clean + spam per month) ───────────────────── + sequelize.query<{ month: string; clean: string; spam: string }>( + `SELECT DATE_TRUNC('month', "createdAt") AS month, + COUNT(*) FILTER (WHERE ${NOT_SPAM_COMMUNITY}) AS clean, + COUNT(*) FILTER (WHERE ${IS_SPAM_COMMUNITY}) AS spam FROM "Communities" - GROUP BY DATE_TRUNC('month', "createdAt") - ORDER BY month`, + GROUP BY month ORDER BY month`, { type: QueryTypes.SELECT }, ), - sequelize.query<{ month: string; count: string }>( - `SELECT DATE_TRUNC('month', "createdAt") AS month, COUNT(*) AS count + sequelize.query<{ month: string; clean: string; spam: string }>( + `SELECT DATE_TRUNC('month', "createdAt") AS month, + COUNT(*) FILTER (WHERE ${NOT_SPAM_USER}) AS clean, + COUNT(*) FILTER (WHERE ${IS_SPAM_USER}) AS spam FROM "Users" - GROUP BY DATE_TRUNC('month', "createdAt") - ORDER BY month`, + GROUP BY month ORDER BY month`, { type: QueryTypes.SELECT }, ), - sequelize.query<{ month: string; count: string }>( - `SELECT DATE_TRUNC('month', "createdAt") AS month, COUNT(*) AS count + sequelize.query<{ month: string; clean: string; spam: string }>( + `SELECT DATE_TRUNC('month', "createdAt") AS month, + COUNT(*) FILTER (WHERE ${NOT_SPAM_VIA_COMMUNITY}) AS clean, + COUNT(*) FILTER (WHERE ${IS_SPAM_VIA_COMMUNITY}) AS spam FROM "Pubs" - GROUP BY DATE_TRUNC('month', "createdAt") - ORDER BY month`, + GROUP BY month ORDER BY month`, { type: QueryTypes.SELECT }, ), - // Pageviews by month — read from the pre-aggregated materialized view - sequelize.query<{ month: string; count: string }>( + sequelize.query<{ month: string; clean: string; spam: string }>( `SELECT DATE_TRUNC('month', date) AS month, - SUM(page_views)::bigint AS count + COALESCE(SUM(page_views) FILTER (WHERE ${NOT_SPAM_VIA_COMMUNITY}), 0)::bigint AS clean, + COALESCE(SUM(page_views) FILTER (WHERE ${IS_SPAM_VIA_COMMUNITY}), 0)::bigint AS spam FROM analytics_daily_summary - GROUP BY DATE_TRUNC('month', date) - ORDER BY month`, + GROUP BY month ORDER BY month`, { type: QueryTypes.SELECT }, ), - // Active communities by activity items per month + // ── Active communities (clean only — spam overlay not meaningful) ─ sequelize.query<{ month: string; count: string }>( `SELECT DATE_TRUNC('month', "timestamp") AS month, COUNT(DISTINCT "communityId") AS count - FROM "ActivityItems" - GROUP BY DATE_TRUNC('month', "timestamp") - ORDER BY month`, + FROM "ActivityItems" WHERE ${NOT_SPAM_VIA_COMMUNITY} + GROUP BY month ORDER BY month`, { type: QueryTypes.SELECT }, ), - // Active communities by pub creation per month sequelize.query<{ month: string; count: string }>( - `SELECT DATE_TRUNC('month', p."createdAt") AS month, - COUNT(DISTINCT p."communityId") AS count - FROM "Pubs" p - GROUP BY DATE_TRUNC('month', p."createdAt") - ORDER BY month`, + `SELECT DATE_TRUNC('month', "createdAt") AS month, + COUNT(DISTINCT "communityId") AS count + FROM "Pubs" WHERE ${NOT_SPAM_VIA_COMMUNITY} + GROUP BY month ORDER BY month`, { type: QueryTypes.SELECT }, ), ]); - const toSeries = (rows: { month: string; count: string }[]) => + const toSeries = (rows: { month: string; clean: string; spam: string }[]) => + rows.map((r) => ({ month: r.month, count: Number(r.clean), spam: Number(r.spam) })); + const toSimpleSeries = (rows: { month: string; count: string }[]) => rows.map((r) => ({ month: r.month, count: Number(r.count) })); return { totals: { - communities: Number(totalCommunities[0].count), - users: Number(totalUsers[0].count), - pubs: Number(totalPubs[0].count), - pageviews: Number(totalPageviews[0]?.count ?? 0), + communities: Number(communityTotals[0].clean), + users: Number(userTotals[0].clean), + pubs: Number(pubTotals[0].clean), + pageviews: Number(pageviewTotals[0]?.clean ?? 0), + }, + spam: { + communities: Number(communityTotals[0].spam), + users: Number(userTotals[0].spam), + pubs: Number(pubTotals[0].spam), + pageviews: Number(pageviewTotals[0]?.spam ?? 0), }, communitiesByMonth: toSeries(communitiesByMonth), usersByMonth: toSeries(usersByMonth), pubsByMonth: toSeries(pubsByMonth), pageviewsByMonth: toSeries(pageviewsByMonth), - activeCommunityTrendActivity: toSeries(activeCommunityTrendActivity), - activeCommunityTrendPubs: toSeries(activeCommunityTrendPubs), + activeCommunityTrendActivity: toSimpleSeries(activeCommunityTrendActivity), + activeCommunityTrendPubs: toSimpleSeries(activeCommunityTrendPubs), }; } @@ -152,27 +180,33 @@ router.get('/api/platformAnalytics/period', async (req, res, next) => { const page = Math.max(0, parseInt(String(req.query.page ?? '0'), 10) || 0); const offset = page * PERIOD_PAGE_SIZE; + const dateRange = `"createdAt" >= :startDate::date AND "createdAt" < :endDate::date + INTERVAL '1 day'`; + const pvDateRange = `date >= :startDate::date AND date <= :endDate::date`; + const [communities, users, pubs, pageviews, newCommunitiesRows, totalNewCommunities] = await Promise.all([ - sequelize.query<{ count: string }>( - `SELECT COUNT(*) AS count FROM "Communities" - WHERE "createdAt" >= :startDate::date AND "createdAt" < :endDate::date + INTERVAL '1 day'`, + sequelize.query<{ clean: string; spam: string }>( + `SELECT COUNT(*) FILTER (WHERE ${NOT_SPAM_COMMUNITY}) AS clean, + COUNT(*) FILTER (WHERE ${IS_SPAM_COMMUNITY}) AS spam + FROM "Communities" WHERE ${dateRange}`, { type: QueryTypes.SELECT, replacements: { startDate, endDate } }, ), - sequelize.query<{ count: string }>( - `SELECT COUNT(*) AS count FROM "Users" - WHERE "createdAt" >= :startDate::date AND "createdAt" < :endDate::date + INTERVAL '1 day'`, + sequelize.query<{ clean: string; spam: string }>( + `SELECT COUNT(*) FILTER (WHERE ${NOT_SPAM_USER}) AS clean, + COUNT(*) FILTER (WHERE ${IS_SPAM_USER}) AS spam + FROM "Users" WHERE ${dateRange}`, { type: QueryTypes.SELECT, replacements: { startDate, endDate } }, ), - sequelize.query<{ count: string }>( - `SELECT COUNT(*) AS count FROM "Pubs" - WHERE "createdAt" >= :startDate::date AND "createdAt" < :endDate::date + INTERVAL '1 day'`, + sequelize.query<{ clean: string; spam: string }>( + `SELECT COUNT(*) FILTER (WHERE ${NOT_SPAM_VIA_COMMUNITY}) AS clean, + COUNT(*) FILTER (WHERE ${IS_SPAM_VIA_COMMUNITY}) AS spam + FROM "Pubs" WHERE ${dateRange}`, { type: QueryTypes.SELECT, replacements: { startDate, endDate } }, ), - sequelize.query<{ count: string }>( - `SELECT COALESCE(SUM(page_views), 0)::bigint AS count - FROM analytics_daily_summary - WHERE date >= :startDate::date AND date <= :endDate::date`, + sequelize.query<{ clean: string; spam: string }>( + `SELECT COALESCE(SUM(page_views) FILTER (WHERE ${NOT_SPAM_VIA_COMMUNITY}), 0)::bigint AS clean, + COALESCE(SUM(page_views) FILTER (WHERE ${IS_SPAM_VIA_COMMUNITY}), 0)::bigint AS spam + FROM analytics_daily_summary WHERE ${pvDateRange}`, { type: QueryTypes.SELECT, replacements: { startDate, endDate } }, ), sequelize.query<{ @@ -180,10 +214,13 @@ router.get('/api/platformAnalytics/period', async (req, res, next) => { subdomain: string; createdAt: string; description: string; + isSpam: boolean; }>( - `SELECT "title", "subdomain", "createdAt", COALESCE("description", '') AS description + `SELECT "title", "subdomain", "createdAt", + COALESCE("description", '') AS description, + (${IS_SPAM_COMMUNITY}) AS "isSpam" FROM "Communities" - WHERE "createdAt" >= :startDate::date AND "createdAt" < :endDate::date + INTERVAL '1 day' + WHERE ${dateRange} ORDER BY "createdAt" DESC LIMIT :limit OFFSET :offset`, { @@ -192,18 +229,23 @@ router.get('/api/platformAnalytics/period', async (req, res, next) => { }, ), sequelize.query<{ count: string }>( - `SELECT COUNT(*) AS count FROM "Communities" - WHERE "createdAt" >= :startDate::date AND "createdAt" < :endDate::date + INTERVAL '1 day'`, + `SELECT COUNT(*) AS count FROM "Communities" WHERE ${dateRange}`, { type: QueryTypes.SELECT, replacements: { startDate, endDate } }, ), ]); return res.json({ counts: { - communities: Number(communities[0].count), - users: Number(users[0].count), - pubs: Number(pubs[0].count), - pageviews: Number(pageviews[0]?.count ?? 0), + communities: Number(communities[0].clean), + users: Number(users[0].clean), + pubs: Number(pubs[0].clean), + pageviews: Number(pageviews[0]?.clean ?? 0), + }, + spam: { + communities: Number(communities[0].spam), + users: Number(users[0].spam), + pubs: Number(pubs[0].spam), + pageviews: Number(pageviews[0]?.spam ?? 0), }, newCommunities: newCommunitiesRows, totalNewCommunities: Number(totalNewCommunities[0].count), From e5e2e4dadb2474553ef141bf6b71288ae3af2d52 Mon Sep 17 00:00:00 2001 From: Travis Rich Date: Wed, 15 Apr 2026 22:59:39 -0400 Subject: [PATCH 4/4] fix pnpm lock --- pnpm-lock.yaml | 37 ++++++------------------------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 69191cd279..a1192af7aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,7 @@ settings: patchedDependencies: reakit: - hash: hpw5k5ors3jxzufoxbjqeo4iee + hash: 31488077229fe3d19a71c66458b888c58eab18010632a29e3b5c485df77242a8 path: patches/reakit.patch importers: @@ -316,9 +316,6 @@ importers: html-minifier: specifier: ^4.0.0 version: 4.0.0 - iframe-resizer-react: - specifier: 1.1.0 - version: 1.1.0(prop-types@15.8.1)(react-dom@16.14.0(react@16.14.0))(react@16.14.0) install: specifier: ^0.12.2 version: 0.12.2 @@ -525,7 +522,7 @@ importers: version: 1.0.9(react@16.14.0) reakit: specifier: 1.0.0-beta.14 - version: 1.0.0-beta.14(patch_hash=hpw5k5ors3jxzufoxbjqeo4iee)(react-dom@16.14.0(react@16.14.0))(react@16.14.0) + version: 1.0.0-beta.14(patch_hash=31488077229fe3d19a71c66458b888c58eab18010632a29e3b5c485df77242a8)(react-dom@16.14.0(react@16.14.0))(react@16.14.0) rebound: specifier: ^0.1.0 version: 0.1.0 @@ -7175,18 +7172,6 @@ packages: iferr@0.1.5: resolution: {integrity: sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA==} - iframe-resizer-react@1.1.0: - resolution: {integrity: sha512-FrytSq91AIJaDgE+6uK/Vdd6IR8CrwLoZ6eGmL2qQMPTzF0xlSV2jaSzRRUh5V2fttD7vzl21jvBl97bV40eBw==} - engines: {node: '>=8', npm: '>=5'} - peerDependencies: - prop-types: '>=15.7.2' - react: '>=16.8.0' - react-dom: '>=16.8.0' - - iframe-resizer@4.4.5: - resolution: {integrity: sha512-U8bCywf/Gh07O69RXo6dXAzTtODQrxaHGHRI7Nt4ipXsuq6EMxVsOP/jjaP43YtXz/ibESS0uSVDN3sOGCzSmw==} - engines: {node: '>=0.8.0'} - ignore-by-default@1.0.1: resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} @@ -20554,16 +20539,6 @@ snapshots: iferr@0.1.5: {} - iframe-resizer-react@1.1.0(prop-types@15.8.1)(react-dom@16.14.0(react@16.14.0))(react@16.14.0): - dependencies: - iframe-resizer: 4.4.5 - prop-types: 15.8.1 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - warning: 4.0.3 - - iframe-resizer@4.4.5: {} - ignore-by-default@1.0.1: {} ignore@4.0.6: {} @@ -23469,11 +23444,11 @@ snapshots: dependencies: picomatch: 2.3.1 - reakit-system@0.7.2(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(reakit-utils@0.7.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(reakit@1.0.0-beta.14(patch_hash=hpw5k5ors3jxzufoxbjqeo4iee)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)): + reakit-system@0.7.2(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(reakit-utils@0.7.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(reakit@1.0.0-beta.14(patch_hash=31488077229fe3d19a71c66458b888c58eab18010632a29e3b5c485df77242a8)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)): dependencies: react: 16.14.0 react-dom: 16.14.0(react@16.14.0) - reakit: 1.0.0-beta.14(patch_hash=hpw5k5ors3jxzufoxbjqeo4iee)(react-dom@16.14.0(react@16.14.0))(react@16.14.0) + reakit: 1.0.0-beta.14(patch_hash=31488077229fe3d19a71c66458b888c58eab18010632a29e3b5c485df77242a8)(react-dom@16.14.0(react@16.14.0))(react@16.14.0) reakit-utils: 0.7.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) reakit-utils@0.7.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0): @@ -23481,13 +23456,13 @@ snapshots: react: 16.14.0 react-dom: 16.14.0(react@16.14.0) - reakit@1.0.0-beta.14(patch_hash=hpw5k5ors3jxzufoxbjqeo4iee)(react-dom@16.14.0(react@16.14.0))(react@16.14.0): + reakit@1.0.0-beta.14(patch_hash=31488077229fe3d19a71c66458b888c58eab18010632a29e3b5c485df77242a8)(react-dom@16.14.0(react@16.14.0))(react@16.14.0): dependencies: body-scroll-lock: 2.7.1 popper.js: 1.16.1 react: 16.14.0 react-dom: 16.14.0(react@16.14.0) - reakit-system: 0.7.2(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(reakit-utils@0.7.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(reakit@1.0.0-beta.14(patch_hash=hpw5k5ors3jxzufoxbjqeo4iee)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) + reakit-system: 0.7.2(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(reakit-utils@0.7.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(reakit@1.0.0-beta.14(patch_hash=31488077229fe3d19a71c66458b888c58eab18010632a29e3b5c485df77242a8)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) reakit-utils: 0.7.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) rebound@0.1.0: {}