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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
import { processPendingWebhookDeliveries } from "./services/webhook-processor.js";
import { getIsRedisAvailable } from "./services/redis.js";
import { enqueueWebhookDelivery } from "./services/webhook-queue.js";
import { addMonths } from "./services/drip-scheduler.js";
import { sanitizeBody, sanitizeQuery } from "./middleware/sanitize.js";
import { CircuitBreaker, type CircuitBreakerStorage, type State } from "./services/circuit-breaker.js";
import {
Expand Down Expand Up @@ -4272,11 +4273,11 @@ All errors return JSON with an \`error\` field and optional \`code\`:
const user = await prisma.user.findFirst({ where: { email: req.auth!.walletAddress } });
if (!user) return sendError(res, 401, "User not found");

const nextRunAt = new Date();
let nextRunAt: Date;
if (frequency === "weekly") {
nextRunAt.setDate(nextRunAt.getDate() + 7);
nextRunAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
} else {
nextRunAt.setDate(nextRunAt.getDate() + 30);
nextRunAt = addMonths(new Date(), 1);
}

await prisma.recurringSupport.create({
Expand Down Expand Up @@ -4379,11 +4380,10 @@ All errors return JSON with an \`error\` field and optional \`code\`:
// Recalculate nextRunAt when frequency changes
let nextRunAt: Date | undefined;
if (frequency) {
nextRunAt = new Date();
if (frequency === "weekly") {
nextRunAt.setDate(nextRunAt.getDate() + 7);
nextRunAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
} else {
nextRunAt.setDate(nextRunAt.getDate() + 30);
nextRunAt = addMonths(new Date(), 1);
}
}

Expand Down
6 changes: 6 additions & 0 deletions backend/src/metrics.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
// Prometheus-compatible metrics registry for background service health monitoring.
// Exposes counters, gauges, and histograms in OpenMetrics text format.
//
// ⚠️ LIMITATION: This in-memory registry resets on process restart and is not
// suitable for multi-instance deployments. Metric values reflect only the
// current instance's state since last restart. For production deployments with
// multiple instances or persistent monitoring, use a proper Prometheus client
// library (e.g., prom-client) with persistent storage and cluster-aware aggregation.

type MetricValue = number;

Expand Down
2 changes: 1 addition & 1 deletion backend/src/services/drip-scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Metrics } from "../metrics.js";

const DRIP_BATCH_SIZE = 100;

function addMonths(date: Date, months: number): Date {
export function addMonths(date: Date, months: number): Date {
const result = new Date(date);
const targetMonth = result.getMonth() + months;
result.setMonth(targetMonth);
Expand Down
7 changes: 4 additions & 3 deletions backend/src/services/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ export async function sendSupportReceivedEmail({
txHash,
}: SendSupportReceivedEmailParams): Promise<void> {
const subject = `You received ${amount} ${assetCode} on NovaSupport`;
const truncatedAddress = fromAddress.length > 8

const truncatedAddress = fromAddress.length > 8
? `${fromAddress.slice(0, 4)}...${fromAddress.slice(-4)}`
: fromAddress;

const stellarExpertLink = `https://stellar.expert/explorer/testnet/tx/${txHash}`;
const network = process.env.STELLAR_NETWORK === "PUBLIC" ? "public" : "testnet";
const stellarExpertLink = `https://stellar.expert/explorer/${network}/tx/${txHash}`;

const html = `
<h2>You've received support!</h2>
Expand Down
16 changes: 12 additions & 4 deletions frontend/src/app/explore/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type Profile = {
};

type SortOption = "newest" | "most_supported" | "most_transactions";
type AssetFilter = "all" | "XLM" | "USDC";
type AssetFilter = string;

export default function ExplorePage() {
const [profiles, setProfiles] = useState<Profile[]>([]);
Expand All @@ -26,6 +26,7 @@ export default function ExplorePage() {
const [sort, setSort] = useState<SortOption>("newest");
const [asset, setAsset] = useState<AssetFilter>("all");
const [assetIssuer, setAssetIssuer] = useState<string>("");
const [availableAssets, setAvailableAssets] = useState<string[]>([]);
const [availableIssuers, setAvailableIssuers] = useState<
Array<{ code: string; issuer: string }>
>([]);
Expand All @@ -49,14 +50,18 @@ export default function ExplorePage() {
}, [sort, asset, assetIssuer]);

useEffect(() => {
const assetCodes = new Set<string>();
const issuers = new Set<string>();
profiles.forEach((p) => {
p.acceptedAssets.forEach((a) => {
assetCodes.add(a.code);
if (a.issuer) {
issuers.add(`${a.code}:${a.issuer}`);
}
});
});
const uniqueAssets = Array.from(assetCodes).sort();
setAvailableAssets(uniqueAssets);
const unique = Array.from(issuers)
.map((str) => {
const [code, issuer] = str.split(":");
Expand Down Expand Up @@ -158,14 +163,17 @@ export default function ExplorePage() {
<select
value={asset}
onChange={(e) => {
setAsset(e.target.value as AssetFilter);
setAsset(e.target.value);
setAssetIssuer("");
}}
className="rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-sm text-white focus:border-mint/50 focus:outline-none"
>
<option value="all">All</option>
<option value="XLM">XLM</option>
<option value="USDC">USDC</option>
{availableAssets.map((code) => (
<option key={code} value={code}>
{code}
</option>
))}
</select>
</div>

Expand Down
Loading