From 36ff227e8e66500860dbd61a1859663d7fa534c8 Mon Sep 17 00:00:00 2001 From: Paul Atkin Date: Mon, 13 Apr 2026 00:56:15 +0000 Subject: [PATCH] Upload files to "/" --- base.html | 35 ++++ dashboard.html | 36 ++++ main.py | 180 +++++++++++++++++++ styles.css | 167 +++++++++++++++++ swagger-dark.css | 460 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 878 insertions(+) create mode 100644 base.html create mode 100644 dashboard.html create mode 100644 main.py create mode 100644 styles.css create mode 100644 swagger-dark.css diff --git a/base.html b/base.html new file mode 100644 index 0000000..2fa5bef --- /dev/null +++ b/base.html @@ -0,0 +1,35 @@ + + + + + + + Recon Ranger + + + + + +
+ +
+
+ {% block content %}{% endblock %} +
+ + + + \ No newline at end of file diff --git a/dashboard.html b/dashboard.html new file mode 100644 index 0000000..2d862fc --- /dev/null +++ b/dashboard.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} + +{% block content %} +
+

{{ title }}

+ + {% if results | length > 0 %} + + + + {% for key in results[0].keys() %} + + {% endfor %} + + + + {% for row in results %} + + {% for key, value in row.items() %} + + {% endfor %} + + {% endfor %} + +
{{ key }}
+ {% if key == 'Status' %} + {{ value }} + {% elif key == 'Flag' %} + {{ value }} + {% else %} + {{ value }} + {% endif %} +
+ {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..d813d94 --- /dev/null +++ b/main.py @@ -0,0 +1,180 @@ +import uvicorn +from fastapi import FastAPI, HTTPException, Query +from fastapi.openapi.docs import get_swagger_ui_html +from fastapi.staticfiles import StaticFiles +from fastapi.responses import HTMLResponse +from pydantic import BaseModel +from typing import Literal +from datetime import date + + +# ── Models ──────────────────────────────────────────────────────────────────── + +class Transaction(BaseModel): + transaction_id: str + date: date + account: str + amount: float + status: Literal["Matched", "Unmatched", "Pending"] + flag: Literal["None", "Duplicate", "Threshold Breach", "Manual Review"] + + +class ReconSummary(BaseModel): + total: int + matched: int + unmatched: int + pending: int + flagged: int + + +# ── Sample data ─────────────────────────────────────────────────────────────── + +TRANSACTIONS: list[Transaction] = [ + Transaction(transaction_id="TXN-001", date=date(2026, 4, 10), account="ACC-9821", amount=12400.00, status="Matched", flag="None"), + Transaction(transaction_id="TXN-002", date=date(2026, 4, 11), account="ACC-4473", amount=3750.50, status="Unmatched", flag="Duplicate"), + Transaction(transaction_id="TXN-003", date=date(2026, 4, 11), account="ACC-1190", amount=88200.00, status="Unmatched", flag="Threshold Breach"), + Transaction(transaction_id="TXN-004", date=date(2026, 4, 12), account="ACC-6654", amount=540.00, status="Matched", flag="None"), + Transaction(transaction_id="TXN-005", date=date(2026, 4, 13), account="ACC-3312", amount=21000.00, status="Pending", flag="Manual Review"), +] + + +# ── App ─────────────────────────────────────────────────────────────────────── + +app = FastAPI( + title="Recon Ranger", + description="Financial crime reconciliation API — patrol your data landscape for inconsistencies.", + version="0.1.0", + docs_url=None, +) +app.mount("/static", StaticFiles(directory="static"), name="static") + + +# ── Endpoints ───────────────────────────────────────────────────────────────── + +@app.get( + "/api/transactions", + response_model=list[Transaction], + summary="List transactions", + tags=["Transactions"], +) +async def list_transactions( + status: Literal["Matched", "Unmatched", "Pending"] | None = Query(None, description="Filter by status"), + flag: Literal["None", "Duplicate", "Threshold Breach", "Manual Review"] | None = Query(None, description="Filter by flag"), +) -> list[Transaction]: + """Return all transactions, optionally filtered by status and/or flag.""" + results = TRANSACTIONS + if status: + results = [t for t in results if t.status == status] + if flag: + results = [t for t in results if t.flag == flag] + return results + + +@app.get( + "/api/transactions/{transaction_id}", + response_model=Transaction, + summary="Get a transaction", + tags=["Transactions"], +) +async def get_transaction(transaction_id: str) -> Transaction: + """Retrieve a single transaction by ID.""" + for t in TRANSACTIONS: + if t.transaction_id == transaction_id: + return t + raise HTTPException(status_code=404, detail=f"{transaction_id} not found") + + +@app.post( + "/api/transactions", + response_model=Transaction, + status_code=201, + summary="Submit a transaction", + tags=["Transactions"], +) +async def create_transaction(transaction: Transaction) -> Transaction: + """Submit a new transaction for reconciliation.""" + for t in TRANSACTIONS: + if t.transaction_id == transaction.transaction_id: + raise HTTPException(status_code=409, detail=f"{transaction.transaction_id} already exists") + TRANSACTIONS.append(transaction) + return transaction + + +@app.get( + "/api/summary", + response_model=ReconSummary, + summary="Reconciliation summary", + tags=["Reporting"], +) +async def get_summary() -> ReconSummary: + """Return aggregate counts across all transactions.""" + return ReconSummary( + total=len(TRANSACTIONS), + matched=sum(1 for t in TRANSACTIONS if t.status == "Matched"), + unmatched=sum(1 for t in TRANSACTIONS if t.status == "Unmatched"), + pending=sum(1 for t in TRANSACTIONS if t.status == "Pending"), + flagged=sum(1 for t in TRANSACTIONS if t.flag != "None"), + ) + + +@app.delete( + "/api/transactions/{transaction_id}", + status_code=204, + summary="Delete a transaction", + tags=["Transactions"], +) +async def delete_transaction(transaction_id: str) -> None: + """Remove a transaction by ID.""" + for i, t in enumerate(TRANSACTIONS): + if t.transaction_id == transaction_id: + TRANSACTIONS.pop(i) + return + raise HTTPException(status_code=404, detail=f"{transaction_id} not found") + + + +@app.get("/api/docs", include_in_schema=False) +async def swagger_ui_dark() -> HTMLResponse: + base = get_swagger_ui_html( + openapi_url=app.openapi_url, + title="Recon Ranger — API Docs", + swagger_css_url="/static/api/swagger-ui.css", + ) + html = base.body.decode() + + # Inject dark theme + main site styles + html = html.replace( + "", + ( + '\n' + '\n' + "\n" + "" + ), + ) + + # Inject the site nav bar before Swagger's root div + nav_html = """ +
+ +
+
+""" + html = html.replace("", "" + nav_html) + html = html.replace("", "
") + + return HTMLResponse(html) + + +if __name__ == "__main__": + uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) + diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..fc31649 --- /dev/null +++ b/styles.css @@ -0,0 +1,167 @@ +*{ + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} +::selection{ + color: #000; + background: #fff; +} +nav{ + position: fixed; + background: #1b1b1b; + width: 100%; + padding: 10px 0; + z-index: 12; +} +nav .menu{ + max-width: 1250px; + margin: auto; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 20px; +} +.menu .logo a{ + text-decoration: none; + color: #fff; + font-size: 35px; + font-weight: 600; +} +.menu ul{ + display: flex; +} +.menu ul li{ + list-style: none; + margin-left: 7px; +} +.menu ul li:first-child{ + margin-left: 0px; +} +.menu ul li a{ + text-decoration: none; + color: #fff; + font-size: 18px; + font-weight: 500; + padding: 0px 15px; + border-radius: 5px; + transition: all 0.3s ease; +} +.menu ul li a:hover{ + background-color: #fff; + color: #000; + padding: 10px 15px; +} +/* ── Body & layout ─────────────────────────────────────────── */ +body { + background: url(../images/recon_ranger_logo.png) no-repeat center center fixed; + background-size: cover; + color: #e2e8f0; + min-height: 100vh; + display: flex; + flex-direction: column; +} +body::before { + content: ''; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.55); + z-index: 0; + pointer-events: none; +} +main { + flex: 1; + padding-top: 70px; /* clear fixed nav */ + position: relative; + z-index: 1; +} +footer { + position: relative; + z-index: 1; + background: rgba(27, 27, 27, 0.85); + color: #888; + text-align: center; + padding: 18px 20px; + font-size: 14px; + letter-spacing: 0.03em; + border-top: 1px solid #2d2d2d; +} + +/* ── Dashboard content container ────────────────────────── */ +.dashboard-container { + max-width: 1250px; + margin: 32px auto; + padding: 40px 24px 60px; + background: rgba(15, 17, 23, 0.45); + backdrop-filter: blur(6px); + border-radius: 12px; +} +.dashboard-container h1 { + font-size: 28px; + font-weight: 600; + color: #f1f5f9; + margin-bottom: 24px; + padding-bottom: 12px; + border-bottom: 2px solid #2d2d3a; + letter-spacing: 0.02em; +} + +/* ── Data table ─────────────────────────────────────────── */ +.data-table { + width: 100%; + border-collapse: collapse; + font-size: 14px; + border-radius: 10px; + overflow: hidden; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4); +} +.data-table thead { + background: #1b1b2e; +} +.data-table thead th { + padding: 14px 18px; + text-align: left; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #94a3b8; + border-bottom: 1px solid #2d2d3a; + white-space: nowrap; +} +.data-table tbody tr { + background: #161622; + transition: background 0.15s ease; +} +.data-table tbody tr:nth-child(even) { + background: #1a1a28; +} +.data-table tbody tr:hover { + background: #252540; +} +.data-table tbody td { + padding: 13px 18px; + border-bottom: 1px solid #22222e; + color: #cbd5e1; + vertical-align: middle; +} +.data-table tbody tr:last-child td { + border-bottom: none; +} + +/* ── Status badges ──────────────────────────────────────── */ +.badge { + display: inline-block; + padding: 3px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.04em; + white-space: nowrap; +} +.badge-matched { background: rgba(34,197,94,0.15); color: #4ade80; } +.badge-unmatched { background: rgba(239,68,68,0.15); color: #f87171; } +.badge-pending { background: rgba(234,179,8,0.15); color: #facc15; } +.badge-none { background: rgba(100,116,139,0.15); color: #94a3b8; } +.badge-flag { background: rgba(249,115,22,0.15); color: #fb923c; } diff --git a/swagger-dark.css b/swagger-dark.css new file mode 100644 index 0000000..5a07858 --- /dev/null +++ b/swagger-dark.css @@ -0,0 +1,460 @@ +/* ══════════════════════════════════════════════════════════════ + Recon Ranger — Swagger UI dark theme override + Matches the dashboard palette. Load AFTER swagger-ui.css. + ══════════════════════════════════════════════════════════════ */ + +/* ── Page shell ──────────────────────────────────────────────── */ +body { + background: #0f1117; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'Helvetica Neue', Arial, sans-serif; +} + +/* Lift all Swagger content above the body::before dark overlay (z-index: 0). + Without this, the fixed overlay covers everything since Swagger's divs + have no stacking context of their own. */ +.swagger-ui, +.swagger-ui-wrap { + position: relative; + z-index: 1; + color: #e2e8f0; +} + +/* ── Global text fallback — catch any element Swagger sets to near-black ── */ +.swagger-ui p, +.swagger-ui span, +.swagger-ui div, +.swagger-ui h1, +.swagger-ui h2, +.swagger-ui h3, +.swagger-ui h4, +.swagger-ui h5, +.swagger-ui label, +.swagger-ui li, +.swagger-ui td, +.swagger-ui th { + color: #e2e8f0; +} + +/* ── Compound selectors with higher specificity in swagger-ui.css ──────── + These all set dark text and need explicit overrides to win by load order. */ +.swagger-ui section h3, +.swagger-ui section.models h4, +.swagger-ui section.models h5, +.swagger-ui .responses-inner h4, +.swagger-ui .responses-inner h5, +.swagger-ui .opblock-title_normal h4, +.swagger-ui .opblock-title_normal p, +.swagger-ui .opblock-description-wrapper h4, +.swagger-ui .opblock-description-wrapper p, +.swagger-ui .opblock-external-docs-wrapper h4, +.swagger-ui .opblock-external-docs-wrapper p, +.swagger-ui .opblock .opblock-section-header h4, +.swagger-ui .opblock .opblock-section-header > label, +.swagger-ui .dialog-ux .modal-ux-header h3, +.swagger-ui .dialog-ux .modal-ux-content h4, +.swagger-ui .dialog-ux .modal-ux-content p, +.swagger-ui .errors-wrapper hgroup h4, +.swagger-ui .errors-wrapper .errors h4, +.swagger-ui .errors-wrapper .errors small, +.swagger-ui .scopes h2, +.swagger-ui .scopes h2 a, +.swagger-ui table.model tr.description, +.swagger-ui table.model tr.extension, +.swagger-ui table.headers td, +.swagger-ui table.headers .header-example, +.swagger-ui table thead tr td, +.swagger-ui table thead tr th { + color: #e2e8f0; +} + +/* Empty-spec and fallback messages */ +.swagger-ui .opblock-tag-section p, +.swagger-ui .fallback, +.swagger-ui .info__tos, +.swagger-ui .renderedMarkdown p, +.swagger-ui .markdown p { + color: #cbd5e1; +} + +/* ── Top bar ─────────────────────────────────────────────────── */ +.swagger-ui .topbar { + background: #1b1b1b; + padding: 10px 0; + border-bottom: 1px solid #2d2d3a; +} + +.swagger-ui .topbar .download-url-wrapper input[type="text"] { + background: #161622; + border: 1px solid #2d2d3a; + color: #e2e8f0; + border-radius: 5px; +} + +.swagger-ui .topbar .download-url-wrapper .download-url-button { + background: #252540; + color: #e2e8f0; + border: 1px solid #2d2d3a; + border-radius: 5px; +} + +/* ── Info block ──────────────────────────────────────────────── */ +.swagger-ui .information-container { + background: rgba(15, 17, 23, 0.45); + backdrop-filter: blur(6px); + border-radius: 12px; + padding: 24px; + margin-bottom: 24px; + border: 1px solid #2d2d3a; +} + +.swagger-ui .info .title, +.swagger-ui .info h1, +.swagger-ui .info h2, +.swagger-ui .info h3 { + color: #f1f5f9; +} + +.swagger-ui .info p, +.swagger-ui .info li, +.swagger-ui .info a { + color: #cbd5e1; +} + +.swagger-ui .info a:hover { + color: #f1f5f9; +} + +/* ── Scheme / server selector ────────────────────────────────── */ +.swagger-ui .scheme-container { + background: #161622; + border-radius: 8px; + border: 1px solid #2d2d3a; + box-shadow: none; + padding: 16px 24px; + margin-bottom: 16px; +} + +.swagger-ui .scheme-container .schemes > label { + color: #94a3b8; +} + +.swagger-ui select { + background: #1a1a28; + border: 1px solid #2d2d3a; + color: #e2e8f0; + border-radius: 5px; +} + +/* ── Operation tag headers ───────────────────────────────────── */ +.swagger-ui .opblock-tag { + color: #f1f5f9; + border-bottom: 1px solid #2d2d3a; + font-size: 18px; + font-weight: 600; +} + +.swagger-ui .opblock-tag:hover { + background: #1a1a28; + border-radius: 6px; +} + +.swagger-ui .opblock-tag small { + color: #94a3b8; +} + +/* ── Operation blocks (GET / POST / PUT / DELETE / PATCH) ────── */ +.swagger-ui .opblock { + border-radius: 8px; + border: 1px solid #2d2d3a; + margin-bottom: 8px; + box-shadow: none; +} + +.swagger-ui .opblock .opblock-summary { + border-radius: 8px; +} + +.swagger-ui .opblock .opblock-summary-description { + color: #94a3b8; +} + +/* GET */ +.swagger-ui .opblock.opblock-get { + background: rgba(59, 130, 246, 0.08); + border-color: rgba(59, 130, 246, 0.35); +} +.swagger-ui .opblock.opblock-get .opblock-summary-method { + background: #3b82f6; +} + +/* POST */ +.swagger-ui .opblock.opblock-post { + background: rgba(34, 197, 94, 0.08); + border-color: rgba(34, 197, 94, 0.35); +} +.swagger-ui .opblock.opblock-post .opblock-summary-method { + background: #22c55e; +} + +/* PUT */ +.swagger-ui .opblock.opblock-put { + background: rgba(249, 115, 22, 0.08); + border-color: rgba(249, 115, 22, 0.35); +} +.swagger-ui .opblock.opblock-put .opblock-summary-method { + background: #f97316; +} + +/* DELETE */ +.swagger-ui .opblock.opblock-delete { + background: rgba(239, 68, 68, 0.08); + border-color: rgba(239, 68, 68, 0.35); +} +.swagger-ui .opblock.opblock-delete .opblock-summary-method { + background: #ef4444; +} + +/* PATCH */ +.swagger-ui .opblock.opblock-patch { + background: rgba(20, 184, 166, 0.08); + border-color: rgba(20, 184, 166, 0.35); +} +.swagger-ui .opblock.opblock-patch .opblock-summary-method { + background: #14b8a6; +} + +/* ── Expanded opblock panels — use !important to beat vendor specificity ─── */ +.swagger-ui .opblock-body, +.swagger-ui .opblock-section, +.swagger-ui .opblock .opblock-section-header, +.swagger-ui .opblock-section-header, +.swagger-ui .table-container, +.swagger-ui .parameters-container, +.swagger-ui .body-param, +.swagger-ui .body-param__text, +.swagger-ui .body-param-options, +.swagger-ui .responses-wrapper, +.swagger-ui .responses-inner, +.swagger-ui .response, +.swagger-ui .live-responses-table, +.swagger-ui .request-url, +.swagger-ui .curl-command, +.swagger-ui .curl-command pre, +.swagger-ui .highlight-code, +.swagger-ui .microlight, +.swagger-ui .model-example, +.swagger-ui .example-module-example, +.swagger-ui .scheme-container, +.swagger-ui section.models, +.swagger-ui section.models .model-container, +.swagger-ui section.models .model-container:hover, +.swagger-ui section.models .model-box, +.swagger-ui .model-box, +.swagger-ui .model-box .json-schema-2020-12, +.swagger-ui .model-box .json-schema-2020-12-accordion, +.swagger-ui .model-box .json-schema-2020-12-expand-deep-button, +.swagger-ui .model-container, +.swagger-ui .json-schema-2020-12, +.swagger-ui .json-schema-2020-12--embedded, +.swagger-ui .json-schema-2020-12__constraint, +.swagger-ui .json-schema-2020-12__constraint--string, +.swagger-ui .json-schema-2020-12-accordion, +.swagger-ui .json-schema-2020-12-expand-deep-button, +.swagger-ui .model-hint, +.swagger-ui .markdown pre, +.swagger-ui .renderedMarkdown pre, +.swagger-ui .dialog-ux .modal-ux, +.swagger-ui .dialog-ux .modal-ux-header { + background: #161622 !important; + color: #cbd5e1 !important; +} + +.swagger-ui .opblock-body pre, +.swagger-ui pre.microlight, +.swagger-ui .response pre { + background: #0f1117 !important; + color: #cbd5e1 !important; + border: 1px solid #2d2d3a; + border-radius: 6px; + padding: 12px 16px; +} + +.swagger-ui code, +.swagger-ui .renderedMarkdown code, +.swagger-ui .markdown code, +.swagger-ui .response-col_description code { + background: #0f1117 !important; + color: #94a3b8 !important; + border-radius: 4px; + padding: 1px 5px; +} + +.swagger-ui .opblock-description-wrapper p, +.swagger-ui .opblock-external-docs-wrapper p, +.swagger-ui .opblock .opblock-section-header h4, +.swagger-ui .opblock-section-header h4, +.swagger-ui .opblock .opblock-section-header > label, +.swagger-ui .opblock-section-header label { + color: #94a3b8; +} + +/* ── Parameters table ────────────────────────────────────────── */ +.swagger-ui table { + border-collapse: collapse; + width: 100%; +} + +.swagger-ui table thead tr { + background: #1b1b2e; + color: #94a3b8; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.swagger-ui table thead tr th, +.swagger-ui table thead tr td { + color: #94a3b8; + border-bottom: 1px solid #2d2d3a; + padding: 12px 16px; +} + +.swagger-ui table tbody tr { + background: #161622; +} + +.swagger-ui table tbody tr:nth-child(even) { + background: #1a1a28; +} + +.swagger-ui table tbody tr:hover { + background: #252540; +} + +.swagger-ui table tbody tr td { + color: #cbd5e1; + border-bottom: 1px solid #22222e; + padding: 12px 16px; +} + +.swagger-ui .parameter__name, +.swagger-ui .parameter__type, +.swagger-ui .parameter__deprecated, +.swagger-ui .parameter__in { + color: #94a3b8; +} + +.swagger-ui .parameter__name.required::after { + color: #f87171; +} + +/* ── Input / textarea ────────────────────────────────────────── */ +.swagger-ui input[type="text"], +.swagger-ui input[type="email"], +.swagger-ui input[type="password"], +.swagger-ui textarea { + background: #1a1a28; + border: 1px solid #2d2d3a; + color: #e2e8f0; + border-radius: 5px; +} + +.swagger-ui input[type="text"]:focus, +.swagger-ui textarea:focus { + border-color: #3b82f6; + outline: none; +} + +/* ── Buttons ─────────────────────────────────────────────────── */ +.swagger-ui .btn { + border-radius: 5px; + font-weight: 500; + transition: all 0.2s ease; +} + +.swagger-ui .btn.execute { + background: #3b82f6; + border-color: #3b82f6; + color: #fff; +} + +.swagger-ui .btn.execute:hover { + background: #2563eb; + border-color: #2563eb; +} + +.swagger-ui .btn.cancel { + background: transparent; + border-color: #f87171; + color: #f87171; +} + +.swagger-ui .btn.try-out__btn { + border-color: #4ade80; + color: #4ade80; + background: transparent; +} + +.swagger-ui .btn.try-out__btn.cancel { + border-color: #f87171; + color: #f87171; +} + +.swagger-ui .btn.authorize { + background: rgba(34, 197, 94, 0.1); + border-color: #4ade80; + color: #4ade80; +} + +/* ── Response section ────────────────────────────────────────── */ +.swagger-ui .response-col_status { + color: #4ade80; + font-weight: 600; +} + +.swagger-ui .response-col_description__inner p { + color: #cbd5e1; +} + +/* ── Models section ──────────────────────────────────────────── */ +.swagger-ui section.models h4 { + color: #f1f5f9; + border-bottom: 1px solid #2d2d3a; +} + +.swagger-ui .model-title { + color: #94a3b8; +} + +.swagger-ui .model { + color: #cbd5e1; +} + +.swagger-ui .prop-type { + color: #4ade80; +} + +.swagger-ui .prop-format { + color: #94a3b8; +} + +/* ── Auth modal ──────────────────────────────────────────────── */ +.swagger-ui .dialog-ux .modal-ux { + border: 1px solid #2d2d3a; + border-radius: 12px; +} + +.swagger-ui .dialog-ux .modal-ux-header { + border-bottom: 1px solid #2d2d3a; + border-radius: 12px 12px 0 0; +} + +.swagger-ui .dialog-ux .modal-ux-header h3 { + color: #f1f5f9; +} + +.swagger-ui .dialog-ux .modal-ux-content p, +.swagger-ui .dialog-ux .modal-ux-content label { + color: #94a3b8; +}