A full-stack TypeScript workspace for tracing developer contributions across an Azure DevOps Services organization. It connects each contribution to its pull request, source and target branches, merge destination, builds, and deployment status. The backend is an Express modular monolith and the frontend is a React single-page application built with Vite.
A contribution is the primary delivery record in Azure Project Log. Azure Boards tasks and other work-item types provide the planning, ownership, iteration, priority, and effort context around that delivery story. The dashboard is designed to answer:
- Who contributed the change, through which commits and pull request?
- What was the source branch, target branch, and final merge status?
- Which build or pipeline included the contribution?
- Which environments received it, and is it live, pending, failed, or rolled back?
- Which specifications, decisions, or notes from tools such as Craft explain the change?
- Which work is open, overdue, completed, or unassigned, and who currently owns it?
- Which tasks were completed after a date, asked in plain language and verified against live data?
- Node.js 22.12 or newer
- npm 10 or newer
- An Azure DevOps Services organization
A PAT is the quickest local-development connection and does not require an Entra app registration:
- In Azure DevOps, open User settings -> Personal access tokens.
- Create an organization-scoped token with Project and Team: Read, Work Items: Read, Code: Read, and Build: Read permissions. Reading YAML environment deployment history may additionally require Environment: Read & manage.
- Open the Azure Project Log project panel, enter the organization name if it is not configured by the backend, paste the PAT, and choose Connect with PAT.
The browser sends the PAT once to Express. It is never returned to the browser, written to browser storage, or logged. The temporary backend session expires after eight hours. Local Node development keeps it in memory; the Cloudflare runtime keeps only this expiring server-side session in a Durable Object so Worker restarts do not randomly sign users out. Treat a PAT like a password, grant the smallest scope possible, and revoke it when it is no longer needed. See Microsoft's PAT guidance.
Users can create an Azure Project Log account with a name, email, password, Azure DevOps organization, and PAT. Registration validates the PAT against Azure DevOps before writing the account. The password is hashed with bcrypt (12 rounds by default); the PAT is separately encrypted with authenticated AES-256-GCM because Azure Project Log must recover it to call Azure DevOps. Neither value is returned to React or written to logs.
Generate the server-only PAT encryption key once:
npm run account:keygen --workspace @n3/backendThe key is written to the gitignored backend .env. Back it up as an application secret: losing it
makes saved PATs unrecoverable. Local Express stores accounts in
apps/backend/data/n3-project-log.db. Cloudflare stores them in the n3-project-log-db D1 binding,
and the deployment command applies the versioned SQL migrations. Passport Local verifies
email/password without creating a Passport session; Azure Project Log continues to use its opaque,
expiring
HttpOnly session cookie.
Cloudflare's free Worker plan has a 10 ms CPU limit per request, which is not suitable for reliable bcrypt work at a secure cost. Use Workers Paid for account registration/login, or keep the Node backend on a conventional host. One-time PAT and Microsoft connection flows remain available when account persistence is not configured.
Before exposing self-service accounts publicly, add edge rate limits for registration, login, PAT connection, and chat routes. Also add email verification and account-recovery flows if email ownership is meant to carry security authority. Those controls are deployment/product policy and are intentionally not approximated with per-process memory, which would be inconsistent across Worker isolates.
Azure Project Log uses the OAuth 2.0 authorization-code flow through Microsoft Entra. The signed-in user's delegated token is kept on the Express backend and is never returned to React.
- In Microsoft Entra admin center, create an app registration for Azure Project Log.
- Under Authentication, add the Web platform and this local redirect URI:
http://localhost:3000/api/v1/auth/microsoft/callback. - Under API permissions, add the delegated Azure DevOps permission required to act as the signed-in user. Grant consent if your tenant requires administrator consent.
- Create a client secret and copy its value immediately.
- Copy
apps/backend/.env.exampletoapps/backend/.envand fill in the organization, tenant, client, and secret values.
Microsoft's current setup reference is Use Microsoft Entra OAuth to authenticate to Azure DevOps. Microsoft recommends Entra user delegation for interactive web applications and PATs only as a higher-risk fallback; see its Azure DevOps Entra authentication overview.
The compact floating Ask Taskie chat uses DeepSeek V4 Flash through the OpenAI-compatible
OpenCode Go API. Add the OpenCode Go API key only to the gitignored apps/backend/.env:
CLOUD_LLM_BASE_URL=https://opencode.ai/zen/go/v1
CLOUD_LLM_MODEL=deepseek-v4-flash
CLOUD_LLM_FALLBACK_MODELS=deepseek-v4-pro
CLOUD_LLM_API_KEY=your_opencode_go_keyUse deepseek-v4-pro instead when Taskie needs the higher-quality DeepSeek tier. Then restart
Express.
The browser never receives the key or calls the model endpoint directly. Express runs a LangGraph tool loop through LangChain. Taskie can choose among typed, read-only tools for exact work items, work-item search, completion dates, recent contributions, and delivery evidence. Each tool is closed over the authenticated Azure connection and selected project, so the model cannot choose a different organization, project, or credential. Azure PATs and Entra access tokens are never sent to the cloud model. Verified Azure URLs are returned separately as clickable chat evidence.
Work-item filters run across the complete WIQL match set. The backend hydrates Azure work items in API-safe batches, reports the complete match count, and sends only a bounded evidence page to the model. Contribution and deployment questions remain explicitly scoped to recent history because their status is derived from multiple PR, build, pipeline, and deployment APIs; Taskie reports that scope instead of presenting a recent window as a complete-project count.
There is no hard-coded intent whitelist: Taskie can handle conversational follow-ups and decide which tool or tools are needed. Project claims must still come from current tool evidence, and graph/model/ tool-call limits prevent runaway requests. Examples include “What tasks were completed after 26 June 2026?”, “Is work item #31412 resolved?”, and “Was that PR deployed?”.
Craft is an optional integration and does not require an OAuth app registration:
- In the Craft space that should receive the tasks, open Imagine -> API connections.
- Create a Full Space connection that can list documents, create documents, and add blocks.
- Copy the generated
https://connect.craft.do/...API connection URL. - Connect Azure DevOps in Azure Project Log, choose Connect Craft on the app shelf, and paste that URL.
Once connected, every work-item card gets a Send to Craft action. The user can create a new document or select an existing document. Existing-document mode appends a separated task section at the end; it never replaces current Craft content. Azure Project Log reloads the work item from Azure DevOps at send time so Craft receives the latest state, owner, paths, estimates, tags, dates, and source link.
The generated Craft URL is a credential. React submits it once, clears the field after success,
does not store it in browser storage, and never receives it back. Express accepts only HTTPS
connect.craft.do URLs. Account connections are encrypted at rest and restored after the user
signs in again; PAT-only and Microsoft-only connections remain scoped to their expiring backend
session. A disconnect removes the applicable saved or temporary connection. See Craft's official
API setup guide and
Full Space API reference.
The Express backend exposes exactly two server-to-server scheduler routes outside the versioned API prefix:
GET /scheduler/fieldsreturns then3-daily-cfoapp contract, thedailyOverviewdatasource, its dynamic fields, and its default Telegram HTML template.GET /scheduler/data?datasourceKey=dailyOverview&filters={urlEncodedJson}returns current Azure DevOps delivery values.filtersis optional and supportsprojectId,taskLimit, andcontributionLimit. Work-item summaries use the latest 10 items by default;taskLimitcan explicitly widen that window.
Every request must carry the five X-QNE-Scheduler-* headers. Express verifies the configured key
ID, signature version v1, a Unix timestamp within 300 seconds, and the lowercase SHA-256 of the
empty GET body. It signs this exact canonical value with HMAC-SHA256 and compares the base64url
signature in constant time:
METHOD + "\n" + PATH_AND_QUERY + "\n" + TIMESTAMP + "\n" + CONTENT_SHA256
The raw encoded path and query are signed, including query order and URL encoding. QNE credentials
must only be added to the gitignored apps/backend/.env; the example file deliberately contains no
credential values. They are never part of React, a VITE_ variable, a response, or application log
context.
Scheduled calls do not carry a user's browser session. To return tenant-authorized live data,
configure a dedicated least-privilege QNE_SCHEDULER_AZURE_DEVOPS_PAT and
AZURE_DEVOPS_ORGANIZATION. Set QNE_SCHEDULER_DEFAULT_PROJECT_ID for calls without filters, or
provide an accessible projectId in the encoded filters. The backend verifies that the project is
visible to that credential, then queries current work items, contributions, builds, and deployment
evidence. Missing authorization or upstream failures return non-2xx errors; no sample data is used.
Azure Project Log uses a Cloudflare Pages front door plus an internal Worker backend. Pages serves the cached React
SPA, while Pages Functions forward only /api/* and /scheduler/* to the Express Worker through a
same-account Service Binding. Azure DevOps, cloud-model, Craft, and QNE credentials remain server-side. An
internal SQLite-backed Durable Object stores only expiring authentication and Craft session records;
it is not exposed as a public route. The Worker still serves the complete app on workers.dev as a
fallback.
-
Authenticate Wrangler once from the repository root:
npx wrangler login
-
Keep production secrets in the gitignored
apps/backend/.env.CLOUD_LLM_BASE_URL,CLOUD_LLM_MODEL,CLOUD_LLM_API_KEY, andACCOUNT_PAT_ENCRYPTION_KEYare required. Add the Entra and QNE variables fromapps/backend/.env.exampleonly when those integrations are enabled. PAT users can continue entering their organization and PAT in the browser. -
Validate the complete production bundle without uploading it:
npm run build:cloudflare
-
Deploy the Express Worker, migrations, secrets, and Pages front door together:
npm run deploy:cloudflare
The deploy command allowlists server-only values from apps/backend/.env, applies pending additive
D1 migrations before publishing code that depends on them, passes the values to Wrangler as
encrypted Worker secrets, and deletes its temporary gitignored secrets file even if deployment
fails. Pages owns no application secrets; its N3_API Service Binding invokes the Worker without a
public network hop.
Pushing a verified commit to main starts .github/workflows/deploy.yml. After one shared release
check, GitHub Actions deploys Pages and the Worker in separate jobs. A backend deployment failure
therefore cannot prevent the verified frontend build from reaching Pages, and a Pages failure does
not block the Worker deployment.
Configure only CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN as GitHub Actions repository
secrets. Worker application secrets and server-only dashboard variables remain in Cloudflare and are
preserved across code deployments; they are not duplicated in GitHub. Bootstrap or rotate Worker
configuration intentionally with the local npm run deploy:cloudflare command and a complete
gitignored apps/backend/.env file. Wrangler validates the required remote secret bindings before a
CD deployment succeeds, while the CD command preserves remote dashboard variables with --keep-vars.
Both deployment jobs receive the Cloudflare deployment credentials. Neither job receives Azure DevOps, account encryption, cloud-model, Microsoft Entra, Craft, or QNE scheduler secrets. The frontend publishes the compiled SPA without browser-accessible application secrets. The workflow can also be started manually from GitHub Actions.
The production Pages project is n3-project-log-web.pages.dev. To keep Namecheap authoritative for
mdfahimhossen.space, associate azure.mdfahimhossen.space with the Pages project in Cloudflare and
then add this record in Namecheap: CNAME azure -> n3-project-log-web.pages.dev. Do not add the CNAME
before Pages accepts the custom hostname. Cloudflare Pages provisions the hostname certificate while
Namecheap continues to manage the domain's DNS records.
Microsoft sign-in needs one post-deployment configuration step: add
https://<worker-host>/api/v1/auth/microsoft/callback as a Web redirect URI in Entra, set the same
value as MICROSOFT_ENTRA_REDIRECT_URI in the backend .env, and redeploy. PAT, Craft, Ask Taskie, and
the scheduler do not require a browser-facing secret or a separate frontend deployment.
Install dependencies and prepare the backend environment from the repository root:
npm install
Copy-Item apps/backend/.env.example apps/backend/.env
npm run account:keygen --workspace @n3/backend
npm run devThe development command starts both applications:
- React:
http://localhost:5173 - Express API:
http://localhost:3000/api/v1 - Health check:
http://localhost:3000/api/v1/health - Scheduler contract:
http://localhost:3000/scheduler/fields - Cloud model API: the server-only
CLOUD_LLM_BASE_URLvalue
Open React and connect with either a PAT or Microsoft. The project selector validates the credential against Azure DevOps before creating the backend session, then loads the projects that credential can access. Selecting a project loads a searchable Azure Boards task overview and a separate contribution trail, plus a compact floating Ask Taskie chat for live work-item questions by date, ID, or title. If Craft is connected, task cards can also create or append Craft documents. The trail matches recent pull-request merge/source commits to build source versions and build-change history, then attaches YAML stage and environment deployment evidence. Deployment is not required for any local connection flow; the Microsoft callback URI only needs to match the Entra app registration exactly when that option is enabled.
Vite proxies browser requests beginning with /api to Express. After Microsoft calls the backend
redirect URI, Express creates an opaque HttpOnly, SameSite=Lax session cookie and redirects the
browser back to React.
| Command | Purpose |
|---|---|
npm run dev |
Start the backend and frontend in watch mode |
npm run dev:backend |
Start only the Express API |
npm run dev:frontend |
Start only the React application |
npm run dev:cloudflare |
Run the built SPA and API in the Worker runtime |
npm run account:keygen --workspace @n3/backend |
Generate account PAT encryption key |
npm run build |
Build all workspaces |
npm run build:cloudflare |
Validate the Cloudflare production bundle |
npm run deploy:cloudflare |
Deploy the full stack and server secrets |
npm start |
Run the compiled backend |
npm test |
Run workspace tests |
npm run lint |
Lint all workspaces |
npm run typecheck |
Type-check all workspaces |
npm run format |
Format the repository |
npm run check |
Run formatting, types, lint, tests, and builds |
apps/
+-- backend/ # Express + TypeScript modular monolith
| +-- src/
| +-- migrations/ # D1 account schema migrations
| +-- tests/
| +-- .env.example
| +-- wrangler.jsonc # Worker, assets, and Durable Object bindings
| `-- package.json
`-- frontend/ # React + TypeScript single-page application
+-- src/
| +-- app/ # Application shell and composition
| +-- features/ # Authentication and project features
| +-- index.css # Global design tokens and base styles
| `-- main.tsx # Browser entry point
+-- .env.example
+-- vite.config.ts
`-- package.json
The root package.json owns npm workspace orchestration and repository-wide formatting. Each
application owns its runtime dependencies and tooling. A shared package will only be introduced
when the frontend and backend have stable contracts worth sharing.
apps/backend/src/
+-- core/ # Framework-independent shared capabilities
| +-- config/ # Validated environment configuration
| +-- errors/ # Application error types
| `-- logger/ # Central structured logging boundary
+-- infrastructure/
| +-- cloudflare/ # Durable session runtime and internal client
| `-- http/ # Service-wide Express infrastructure
| +-- middleware/
| +-- routes/
| `-- types/
+-- modules/ # Business capabilities
| +-- account/ # Passport, bcrypt, PAT encryption, SQLite/D1
| +-- auth/
| | +-- domain/ # Session store and Microsoft client ports
| | +-- application/ # Microsoft/PAT authorization orchestration
| | +-- infrastructure/ # MSAL client and temporary in-memory store
| | `-- public/ # Auth routes, controller, cookie, middleware
| +-- chat/ # LangGraph agent, typed tools, and evidence response contract
| +-- contribution/ # PR, build, YAML stage, and deployment correlation
| +-- craft/ # Session connection and task-to-document handoff
| +-- health/
| | `-- public/
| +-- project/
| +-- domain/ # Project model and repository contract
| +-- application/ # Project-list use case
| +-- infrastructure/ # Azure DevOps Services REST implementation
| `-- public/ # Project controller and protected route
| +-- scheduler/ # HMAC contract and live server-to-server datasource
| `-- task/ # Azure Boards WIQL and work-item overview
+-- app.ts # Express application assembly
|-- worker.ts # Cloudflare Express/asset runtime entry
`-- server.ts # Process startup and graceful shutdown
Business capabilities follow public -> application -> domain. Microsoft and Azure DevOps
details stay in module infrastructure so they do not leak into use cases or response contracts.
The frontend contains the application shell and separate features/auth, features/projects,
features/tasks, features/contributions, features/chat, and features/craft boundaries. The
selected-project workspace provides tabbed task and contribution views with loading, empty, error,
retry, search, comprehensive filter states, and 10-item result pages. Ask Taskie is a compact floating
chat that remains available across both project views. Craft owns its connection card and task
export dialog.
Microsoft: React -> Microsoft Entra -> callback -> Express session
PAT: React -> POST /auth/pat -> credential validation -> Express session
Projects: React -> GET /projects + HttpOnly session cookie -> Azure DevOps
Tasks: React -> GET /projects/:id/tasks -> WIQL IDs -> work-items batch
Ask Taskie: React -> Express -> LangGraph tool loop -> Azure facts -> grounded answer + Azure URLs
Trail: React -> GET /projects/:id/contributions -> correlate PR/build/deployment evidence
Craft: React -> Express session -> Craft documents/blocks API
Scheduler: QNE -> signed top-level Express route -> tenant-authorized Azure DevOps data
Project pages are retrieved until exhausted, de-duplicated by project ID, sorted by name, and
returned through GET /api/v1/projects. Contribution results are calculated live over a bounded
window of recent pull requests and builds. No database or application data cache is used yet.
AccountingOnline uses YAML multi-stage pipelines, so Azure Pipelines environments and their deployment records are authoritative evidence when available. QuinnyAI is deployed by an external script; Azure Project Log deliberately reports those deployments as unverified until that script sends a future deployment receipt containing the commit, environment, result, and timestamp.
For later hotfix builds, direct SHA equality is insufficient. Azure Project Log downloads the parent
links for the
deployed build commit with Azure DevOps commitsbatch, builds a bounded commit graph, and runs an
iterative depth-first search from the deployed SHA. A contribution is included when its completed
PR merge commit (or the source commit of an active PR) is reachable as an ancestor. Missing or
truncated DFS history falls back to Azure DevOps' merge-base comparison endpoint; only when both
sources are unavailable is the result reported as unavailable evidence.
The Node development runtime intentionally keeps sessions, PAT credentials, and the MSAL token cache in memory, so a local backend restart signs users out. Cloudflare swaps those session-store adapters for a Durable Object so multiple Worker isolates share the same expiring server-side records. The account database stores identity records and encrypted Azure connection credentials only. The later data phase still owns synchronized project history, caching, and authorization policies beyond basic account login; current connections authorize Azure DevOps access only.
GET /api/v1/auth/microsoft/loginstarts Microsoft sign-in.GET /api/v1/auth/microsoft/callbackcompletes sign-in and sets the backend session cookie.POST /api/v1/auth/registervalidates Azure access, creates an Azure Project Log account, and signs in.POST /api/v1/auth/loginauthenticates an Azure Project Log account through Passport Local.POST /api/v1/auth/patvalidates a PAT and creates a temporary backend session.GET /api/v1/auth/sessionreturns safe connection state; it never returns a token.POST /api/v1/auth/logoutinvalidates the backend session.GET /api/v1/craft/sessionreturns safe Craft connection state without its API URL.POST /api/v1/craft/connectvalidates and retains a Craft API connection URL in the backend.POST /api/v1/craft/disconnectremoves the Craft connection from the current session.GET /api/v1/craft/documentslists documents available to the Craft connection.POST /api/v1/craft/task-exportscreates a document or appends a task to an existing document.GET /api/v1/projectsrequires a valid backend session.GET /api/v1/projects/:projectId/tasks?limit=10returns recent Azure Boards work items. The default is 10; the UI widens the bounded window only when the user chooses Load 10 more.POST /api/v1/projects/:projectId/chatanswers date, ID, title, and status questions from live Azure data.GET /api/v1/projects/:projectId/contributions?limit=20returns the recent delivery graph.GET /scheduler/fieldsreturns the signed QNE datasource and field contract.GET /scheduler/data?datasourceKey=...&filters=...returns signed, live scheduler values.- Successful responses use
{ "success": true, "data": ... }. - Scheduler success responses use the exact QNE top-level contract shapes instead of the API wrapper.
- Errors use
{ "success": false, "error": { "code", "message", "requestId" } }. - Every response includes an
x-request-idheader. - JSON and URL-encoded bodies are limited to 1 MB.
- API responses are non-cacheable and include defensive browser headers. Credentialed browser
writes with an explicitly foreign
OriginorSec-Fetch-Site: cross-siteare rejected.
Backend variables belong in apps/backend/.env:
| Name | Default | Description |
|---|---|---|
NODE_ENV |
development |
Runtime environment |
PORT |
3000 |
Express HTTP port |
API_PREFIX |
/api/v1 |
Versioned API base path |
FRONTEND_URL |
http://localhost:5173 |
Redirect target after Microsoft sign-in |
ACCOUNT_DATABASE_PATH |
./data/n3-project-log.db |
Local SQLite account database |
ACCOUNT_BCRYPT_ROUNDS |
12 |
Password hashing work factor (10-14) |
ACCOUNT_PAT_ENCRYPTION_KEY |
none | Server-only 32-byte base64url AES key |
AZURE_DEVOPS_ORGANIZATION |
none | Default organization name |
AZURE_DEVOPS_API_VERSION |
7.1 |
Azure DevOps REST API version |
AZURE_DEVOPS_REQUEST_TIMEOUT_MS |
10000 |
Upstream timeout in milliseconds |
CLOUD_LLM_BASE_URL |
https://opencode.ai/zen/go/v1 |
Server-only compatible API base URL |
CLOUD_LLM_MODEL |
deepseek-v4-flash |
Tool-capable model used by Ask Taskie |
CLOUD_LLM_FALLBACK_MODELS |
deepseek-v4-pro |
Comma-separated automatic fallback models |
CLOUD_LLM_API_KEY |
none | Server-only cloud-model API key |
CLOUD_LLM_REQUEST_TIMEOUT_MS |
60000 |
Cloud model timeout in milliseconds |
MICROSOFT_ENTRA_TENANT_ID |
none | Entra directory/tenant ID |
MICROSOFT_ENTRA_CLIENT_ID |
none | Entra application/client ID |
MICROSOFT_ENTRA_CLIENT_SECRET |
none | Entra client secret value |
MICROSOFT_ENTRA_REDIRECT_URI |
http://localhost:3000/api/v1/auth/... |
Registered Web callback URI |
QNE_SCHEDULER_APP_ID |
n3-daily-cfo |
Stable QNE scheduler app ID |
QNE_SCHEDULER_APP_NAME |
Azure Project Log |
QNE scheduler display name |
QNE_SCHEDULER_KEY_ID |
none | Server-only QNE request key ID |
QNE_SCHEDULER_SIGNING_SECRET |
none | Server-only QNE HMAC secret |
QNE_SCHEDULER_AZURE_DEVOPS_PAT |
none | Server-only scheduled Azure DevOps PAT |
QNE_SCHEDULER_DEFAULT_PROJECT_ID |
none | Default project for unfiltered schedules |
Frontend variables belong in apps/frontend/.env. Anything prefixed with VITE_ is bundled into
browser code and must never contain credentials or secrets.
- Full-stack modular-monolith foundation (complete)
- PAT/Entra connections and Azure DevOps project retrieval (complete)
- Live Azure Boards task overview and contribution/deployment correlation (current)
- Account persistence and durable sessions started; project synchronization and caching remain
- Azure Project Log registration/login is complete; roles and application authorization policies remain
- Signed QuinnyAI script deployment receipts and historical release evidence
- Lightweight integrations (Craft, QNE Scheduler, and cloud Ask Taskie started), resilience, and deployment hardening