Day 2 feedback · T32 · proposed fix · not applied

Team Performance starts blank each day

On day 2 the Team Performance cards still showed day 1’s speed, hours, audits and verdicts beside “0 captures today”. The fix makes every figure on the panel today’s own, so a new day starts blank, while the top bar, the room list and the red flags keep the project to date. API only — the screen and its client do not change.

Files changed
4all in the API
Front-end changes
0same fields, same generated client
Keeps the project to date
3top bar, rooms, red flags
Migrations
0no schema change
the short answer

The team query falls back to the project to date for anyone who has not captured today — that is why day 2 opened on day 1’s numbers. The patch scopes the panel to today for everyone: speed, hours on site, the Audit cell and the verdict. The top bar’s audit totals and the failed-audit red flag move onto their own to-date figures, so they do not reset with it.

The askday 2 feedback

this dashboard was the most misleading of day - for day 2 be nice if this part was refreshed for starts so its blank for day 2, while all the other data stays (top bar and room data)

Day 2 feedback · 27 Sep

The screenshot behind it: every card read Captures 0 · Today, yet still carried a speed (5.9, 5.6, 3.4 captures/min), on-site hours such as 5.7 / 8, audits such as 2/2 and an Ahead or On pace verdict — with the header claiming a team speed of 21.3 captures/min and 1,278 projected per hour. All of it was day 1.

Day 2, first thingwhat each part reads

Header“Team speed —” until someone has a pace today, then the sum of today’s paces. The front end already adds up the rows, so it follows on its own.
Speed“— · Not enough data” until today holds enough in-room gaps (the pace setting, 12 by default).
Captures“0 · Today” — already today’s; unchanged.
On-site“0 / 0 · Active / total hrs”, counting from the day’s first capture.
Audit“0 · Audited”. On a consumables run, audits made today on products the person captured; on an asset run, the required-field check over today’s captures.
Verdict“No pace yet · Not enough captures to measure a pace yet”; Slow down once today’s failed audits reach the limit (2 by default). The median behind Ahead / Speed up is today’s.
OrderBy today’s speed, then name — so whoever starts first rises to the top.

Why day 2 opened on day 1cause

The panel’s pace, hours and on-site time come from paceCte(). Its day filter keeps today for anyone who has captured today and every day so far for anyone who has not — so at 8 AM on day 2, before the first capture, every card showed day 1’s figures under a “Today” label.

-- pace.sql.ts, scoped_day — which days a person's pace, hours and on-site time cover
WHERE CASE
        WHEN EXISTS (SELECT 1 FROM worked_today w WHERE w.user_id = s.user_id) THEN s.day = <today>
        ELSE TRUE          -- not captured today: every day of the project so far
      END

The Audit cell had the same shape: both audit queries (consumable_audits on a consumables run, the required-field check on an asset run) cover the whole project, with no day in them. The verdict then judged those day-1 figures against a day-1 median.

The fix gives the panel its own scope rather than changing the filter itself, because the Speed side sheet leans on the same filter to fill its day picker:

-- the team panel asks for today only; the Speed side sheet keeps today-or-to-date
paceCte(consumables, { todayOnly: true })   →   WHERE s.day = <today>
paceCte(consumables, { oneUser: true })     →   WHERE CASE … END   (unchanged)

What keeps the project to dateunchanged on screen

Top barUnchanged, including audits passed and audits pending. Those totals used to be summed from the Audit cells; they now read their own to-date audit figures in the same query, so the cells can reset without taking the totals with them.
Room listUntouched — it never read the team query.
Red flags“Name has N failed audits” reads failed audits to date: a quality concern should not vanish overnight.
Speed side sheetStill opens on the person’s last day worked, with its day picker, so opening a blank card on day 2 shows day 1 there — under that day’s date.

The changefour files, API only

pace.sql.tspaceCte takes { oneUser, todayOnly }; todayOnly keeps every person to today.
capture-live-pace.service.tsThe Speed side sheet passes { oneUser: true } — same behaviour as today.
consumable-scope.sql.tsThe consumables audit takes a name and a today flag (audits made today). The list of products each person captured is shared, so the two audits cost one scan of it.
capture-live.service.tsThe team query runs today-only, with audited (to date) beside audited_today. Cells and verdict read today; the banner totals and the red flags read to date.

No DTO changes, so the swagger spec is identical and the front end needs neither a code change nor a client regeneration.

API patchbackendApi · capExpertAPI

capture-live-pace.service.tsbackendApi/src/primary/modules/capture-dashboard/capture-live/capture-live-pace.service.ts · +1 −1

--- a/src/primary/modules/capture-dashboard/capture-live/capture-live-pace.service.ts+++ b/src/primary/modules/capture-dashboard/capture-live/capture-live-pace.service.ts@@ -86,7 +86,7 @@     const iso = (col: string) => `to_char(${col} AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`;      const [row] = await db.sequelize.query<PaceDetailRow>(-      `WITH ${paceCte(consumables, true)},+      `WITH ${paceCte(consumables, { oneUser: true })},        pick AS (          SELECT COALESCE((SELECT day FROM scoped_day WHERE day = CAST(:day AS date)), (SELECT MAX(day) FROM scoped_day)) AS day        ),

capture-live.service.tsbackendApi/src/primary/modules/capture-dashboard/capture-live/capture-live.service.ts · +50 −38

--- a/src/primary/modules/capture-dashboard/capture-live/capture-live.service.ts+++ b/src/primary/modules/capture-dashboard/capture-live/capture-live.service.ts@@ -8,7 +8,7 @@ import { DenovoInventoryStatusEnum, ScopeType } from 'src/shared/utils/enum'; import { readableWhere, resolveCaptureScope } from '../access/capture-scope'; import { CaptureColorKeyEnum, CaptureInsightTypeEnum, CaptureLocationStatusEnum, CaptureSeverityEnum } from '../dto/capture-dashboard.enum';-import { CONSUMABLE_GROUP_CTE, CONSUMABLE_ROOM_CTE, CONSUMABLE_TEAM_AUDIT_CTE } from './consumable-scope.sql';+import { CONSUMABLE_CAPTURER_SKU_CTE, CONSUMABLE_GROUP_CTE, CONSUMABLE_ROOM_CTE, consumableTeamAuditCte } from './consumable-scope.sql'; import { paceCte, paceReplacements, PaceSettings, resolvePaceSettings } from './pace.sql'; import { projectCapturesSql, projectFacilitiesSql, projectRoomsSql, projectStartSql, projectTeamSql, stickerScopeSql } from './project-rooms.sql'; import {@@ -173,6 +173,7 @@   audit_shown: number;   audit_base: number;   clean_rate: string;+  project_audit_failed: number;   insight_type: CaptureInsightTypeEnum;   account_type: string | null;   team_median: string | null;@@ -181,13 +182,20 @@   audits_pending: number; } -/** The team's audits, summed as its Audit tiles show them. */+/** The team's audits to date, summed person by person. */ interface TeamAudits {   auditsPassed: number;   auditsDone: number;   auditsPending: number; } +/** A person's failed audits to date — what the quality red flag reads, whatever today holds. */+interface TeamQuality {+  id: string;+  name: string;+  auditFailed: number;+}+ interface NthWorkingDayRow {   day: string | null; }@@ -332,18 +340,24 @@    /**    * What the team tile's audit rate is measured over: required fields on an asset run, the room audits-   * of the capturer's products on a consumables one. Both hand back `total` and `needs_review` per capturer.+   * of the capturer's products on a consumables one. Both hand back `total` and `needs_review` per+   * capturer, twice: `audited` for the project to date and `audited_today` for today's work.    */   private auditedCte(consumables: boolean): string {-    return consumables ? CONSUMABLE_TEAM_AUDIT_CTE : this.assetAuditCte(consumables);+    return consumables+      ? `${CONSUMABLE_CAPTURER_SKU_CTE}, ${consumableTeamAuditCte('audited')}, ${consumableTeamAuditCte('audited_today', true)}`+      : `${this.assetAuditCte(consumables, 'audited')}, ${this.assetAuditCte(consumables, 'audited_today', true)}`;   }    /**-   * The Audit cell's figures, `audit_shown` over `audit_base`: audited over captured on an asset run,-   * SKUs whose audit matched the system count over the SKUs audited on a consumables one.+   * Today's Audit cell, `audit_shown` over `audit_base`: audited over captured on an asset run, SKUs+   * whose audit matched the system count over the SKUs audited on a consumables one. Plus the passes to+   * date the banner totals read — a consumables figure, so an asset run carries 0.    */   private auditFiguresSql(consumables: boolean): string {-    return consumables ? 'COALESCE(au.passed, 0)::int AS audit_shown, COALESCE(au.total, 0)::int AS audit_base' : 'COALESCE(au.total, 0)::int AS audit_shown, COALESCE(ps.total, 0)::int AS audit_base';+    return consumables+      ? 'COALESCE(aut.passed, 0)::int AS audit_shown, COALESCE(aut.total, 0)::int AS audit_base, COALESCE(au.passed, 0)::int AS project_audit_passed'+      : 'COALESCE(aut.total, 0)::int AS audit_shown, COALESCE(ps.today_count, 0)::int AS audit_base, 0 AS project_audit_passed';   }    /**@@ -353,11 +367,11 @@    * An asset with an inventory row is read from its columns, a standalone processed sticker from its    * `extracted_data`. A known model answers manufacturer, model and model number at once, and    * declaring a tag missing answers that tag. Consumables and items no longer on the floor are out of-   * the population rather than counted against it.+   * the population rather than counted against it. Written as CTE `name`; `today` keeps it to today's captures.    */-  private assetAuditCte(consumables: boolean): string {+  private assetAuditCte(consumables: boolean, name: string, today = false): string {     return `-    audited AS (+    ${name} AS (       SELECT k.created_by AS user_id,              COUNT(*)::int AS total,              COUNT(*) FILTER (@@ -389,7 +403,7 @@                COALESCE((CASE WHEN pa.id IS NOT NULL THEN pa.additional_details ->> 'biomedTagEnabled' ELSE a.additional_details ->> 'biomedTagEnabled' END)::boolean, false) AS biomed_on       ) tag       WHERE ${projectRoomsSql()} AND k.deleted_at IS NULL AND ${projectCapturesSql()}-        AND ${stickerScopeSql(consumables)}+        AND ${stickerScopeSql(consumables)}${today ? ' AND (k.created_at AT TIME ZONE :tz)::date = (now() AT TIME ZONE :tz)::date' : ''}         AND (           (k.inventory_id IS NOT NULL AND i.id IS NOT NULL             AND i.discontinue_date IS NULL@@ -423,7 +437,7 @@       this.staleRooms(project.id, consumables),     ]);     // Reads the banner's forecast, so it runs once that has resolved.-    const redFlags = await this.buildRedFlags(project.id, consumables, banner, stale, team.rows, pace);+    const redFlags = await this.buildRedFlags(project.id, consumables, banner, stale, team.quality, pace);      return {       // The audit totals come off the team rows, so the banner and the Audit tiles cannot disagree.@@ -1045,14 +1059,7 @@    * Each one is a condition the data actually shows — inaccessible rooms nobody revisited, a    * capturer whose clean rate slipped, rooms left mid-capture, and a run tracking behind its window.    */-  private async buildRedFlags(-    projectId: number,-    consumables: boolean,-    banner: CaptureLiveBannerDto,-    stale: StaleRoomRow[],-    team: CaptureTeamPerfRowDto[],-    pace: PaceSettings,-  ): Promise<CaptureRedFlagDto[]> {+  private async buildRedFlags(projectId: number, consumables: boolean, banner: CaptureLiveBannerDto, stale: StaleRoomRow[], quality: TeamQuality[], pace: PaceSettings): Promise<CaptureRedFlagDto[]> {     const flags: CaptureRedFlagDto[] = [];      // Rooms the team could not enter, and how many of those still have nothing captured. The@@ -1091,9 +1098,8 @@       });     } -    // The team rows already carry each person's clean-capture rate; flagging off them costs no-    // second pass over the stickers, and the two panels cannot disagree on the number.-    const failing = team.filter((row) => row.auditFailed >= pace.maxFailedAudits);+    // Failed audits to date, off the team query: a quality concern outlasts the day its audits failed.+    const failing = quality.filter((row) => row.auditFailed >= pace.maxFailedAudits);     for (const person of failing.sort((a, b) => b.auditFailed - a.auditFailed)) {       flags.push({         id: `quality-${person.id}`,@@ -1132,11 +1138,12 @@   /**    * One row per person who captured on this project or was rostered onto one of its visits.    *-   * Pace covers today when they scanned today, the project to date otherwise. `capturesPerMinute` is-   * captures per active minute, null until there is enough to measure. The team median spans the rows-   * with a pace and is what each verdict is judged against.+   * Every figure on a row covers today, so a new day starts blank: pace, hours on site and the Audit+   * cell. `capturesPerMinute` is captures per active minute, null until today holds enough to measure.+   * The team median spans today's paces and is what each verdict is judged against. `audits` and+   * `quality` are the project to date, for the banner totals and the red flags.    */-  private async buildTeam(projectId: number, consumables: boolean, pace: PaceSettings): Promise<{ rows: CaptureTeamPerfRowDto[]; median: number | null; audits: TeamAudits }> {+  private async buildTeam(projectId: number, consumables: boolean, pace: PaceSettings): Promise<{ rows: CaptureTeamPerfRowDto[]; median: number | null; audits: TeamAudits; quality: TeamQuality[] }> {     const replacements = {       projectId,       auditsPerCapturer: AUDITS_PER_CAPTURER,@@ -1155,7 +1162,7 @@     };      const rows = await db.sequelize.query<TeamRow>(-      `WITH ${paceCte(consumables)}, ${this.auditedCte(consumables)},+      `WITH ${paceCte(consumables, { todayOnly: true })}, ${this.auditedCte(consumables)},        project_stickers AS (          SELECT k.created_by AS user_id,                 COUNT(*)::int AS total,@@ -1182,29 +1189,32 @@                 ROUND(COALESCE(sc.active_hours, 0), 1)::numeric AS active_hours,                 COALESCE(ps.today_count, 0)::int AS today_count,                 ROUND(COALESCE(sc.hours, 0), 1)::numeric AS hours_on_site,-                COALESCE(au.total, 0)::int AS audit_total,-                COALESCE(au.needs_review, 0)::int AS audit_failed,+                COALESCE(aut.total, 0)::int AS audit_total,+                COALESCE(aut.needs_review, 0)::int AS audit_failed,                 ${this.auditFiguresSql(consumables)},-                CASE WHEN COALESCE(au.total, 0) = 0 THEN 1::numeric-                     ELSE 1 - (au.needs_review::numeric / au.total) END AS clean_rate+                CASE WHEN COALESCE(aut.total, 0) = 0 THEN 1::numeric+                     ELSE 1 - (aut.needs_review::numeric / aut.total) END AS clean_rate,+                COALESCE(au.total, 0)::int AS project_audit_done,+                COALESCE(au.needs_review, 0)::int AS project_audit_failed          FROM people p          LEFT JOIN users u ON u.id = p.user_id          LEFT JOIN accounts acc ON acc.id = u.account_id          LEFT JOIN project_stickers ps ON ps.user_id = p.user_id          LEFT JOIN audited au ON au.user_id = p.user_id+         LEFT JOIN audited_today aut ON aut.user_id = p.user_id          LEFT JOIN scoped sc ON sc.user_id = p.user_id        ),        med AS (          SELECT (PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY captures_per_minute))::numeric AS team_median FROM measured        ),-       -- per person, so one capturer's extra audits do not cover another's+       -- to date and per person, so one capturer's extra audits do not cover another's        team_audit AS (-         SELECT COALESCE(SUM(audit_shown), 0)::int AS audits_passed,-                COALESCE(SUM(audit_total), 0)::int AS audits_done,-                COALESCE(SUM(GREATEST(:auditsPerCapturer - audit_total, 0)), 0)::int AS audits_pending+         SELECT COALESCE(SUM(project_audit_passed), 0)::int AS audits_passed,+                COALESCE(SUM(project_audit_done), 0)::int AS audits_done,+                COALESCE(SUM(GREATEST(:auditsPerCapturer - project_audit_done, 0)), 0)::int AS audits_pending          FROM measured        )-       SELECT m.user_id, m.name, m.account_type, m.captures_per_minute, m.active_hours, m.today_count, m.hours_on_site, m.audit_total, m.audit_failed, m.audit_shown, m.audit_base, m.clean_rate, med.team_median,+       SELECT m.user_id, m.name, m.account_type, m.captures_per_minute, m.active_hours, m.today_count, m.hours_on_site, m.audit_total, m.audit_failed, m.audit_shown, m.audit_base, m.clean_rate, m.project_audit_failed, med.team_median,               ta.audits_passed, ta.audits_done, ta.audits_pending,               CASE                 WHEN m.audit_failed >= :maxFailedAudits THEN :qualityWarn@@ -1221,6 +1231,7 @@     );      const first = rows[0];+    const nameOf = (row: TeamRow): string => row.name?.trim() || `User ${row.user_id}`;     return {       median: first?.team_median == null ? null : round1(Number(first.team_median)),       audits: {@@ -1228,9 +1239,10 @@         auditsDone: Number(first?.audits_done) || 0,         auditsPending: Number(first?.audits_pending) || 0,       },+      quality: rows.map((row) => ({ id: String(row.user_id), name: nameOf(row), auditFailed: Number(row.project_audit_failed) || 0 })),       rows: rows.map((row) => ({         id: String(row.user_id),-        name: row.name?.trim() || `User ${row.user_id}`,+        name: nameOf(row),         capturesPerMinute: row.captures_per_minute == null ? null : round1(Number(row.captures_per_minute)),         activeHours: round1(Number(row.active_hours) || 0),         todayCount: Number(row.today_count) || 0,

consumable-scope.sql.tsbackendApi/src/primary/modules/capture-dashboard/capture-live/consumable-scope.sql.ts · +20 −14

--- a/src/primary/modules/capture-dashboard/capture-live/consumable-scope.sql.ts+++ b/src/primary/modules/capture-dashboard/capture-live/consumable-scope.sql.ts@@ -91,34 +91,40 @@       GROUP BY g.location_id     )`; +/** The SKUs — a live product in a room, the unit an audit covers — each capturer captured into. */+export const CONSUMABLE_CAPTURER_SKU_CTE = `+    capturer_sku AS (+      SELECT DISTINCT t.created_by AS user_id, l.id AS location_id, t.consumables_id+      ${PROJECT_STOCK_FROM}+      JOIN consumables c ON c.id = t.consumables_id AND c.deleted_at IS NULL+      ${PROJECT_STOCK_WHERE}+    )`;+ /**- * The team tile's audit for a consumables run, per capturer, in SKUs — a live product in a room, the- * unit an audit covers — they captured into: `total` those audited, `passed` the ones whose latest- * audit counted exactly its system count and `needs_review` the ones that did not match.+ * The team tile's audit for a consumables run, per capturer, over `capturer_sku`: `total` the SKUs+ * audited, `passed` the ones whose latest audit counted exactly its system count and `needs_review`+ * the ones that did not match. Written as CTE `name`; `today` counts only audits made today.  *  * The mobile audit screen's rule — an audit is one product in one room, the latest one is its state,  * and it passes when `audited_count` equals `system_count`. `total` and `needs_review` are the shape  * the asset audit hands the tile too.  */-export const CONSUMABLE_TEAM_AUDIT_CTE = `-    latest_audit AS (+export const consumableTeamAuditCte = (name: string, today = false): string => {+  const madeToday = today ? ' AND (a.audited_at AT TIME ZONE :tz)::date = (now() AT TIME ZONE :tz)::date' : '';+  return `+    ${name}_latest AS (       SELECT DISTINCT ON (a.location_id, a.consumables_id) a.location_id, a.consumables_id, a.system_count, a.audited_count       FROM consumable_audits a-      WHERE a.account_id IN (${projectFacilitiesSql()}) AND a.deleted_at IS NULL AND a.consumables_id IS NOT NULL+      WHERE a.account_id IN (${projectFacilitiesSql()}) AND a.deleted_at IS NULL AND a.consumables_id IS NOT NULL${madeToday}       ORDER BY a.location_id, a.consumables_id, a.audited_at DESC     ),-    capturer_sku AS (-      SELECT DISTINCT t.created_by AS user_id, l.id AS location_id, t.consumables_id-      ${PROJECT_STOCK_FROM}-      JOIN consumables c ON c.id = t.consumables_id AND c.deleted_at IS NULL-      ${PROJECT_STOCK_WHERE}-    ),-    audited AS (+    ${name} AS (       SELECT s.user_id,              COUNT(la.consumables_id)::int AS total,              COUNT(*) FILTER (WHERE la.audited_count = la.system_count)::int AS passed,              COUNT(*) FILTER (WHERE la.audited_count <> la.system_count)::int AS needs_review       FROM capturer_sku s-      LEFT JOIN latest_audit la ON la.location_id = s.location_id AND la.consumables_id = s.consumables_id+      LEFT JOIN ${name}_latest la ON la.location_id = s.location_id AND la.consumables_id = s.consumables_id       GROUP BY s.user_id     )`;+};

pace.sql.tsbackendApi/src/primary/modules/capture-dashboard/capture-live/pace.sql.ts · +6 −7

--- a/src/primary/modules/capture-dashboard/capture-live/pace.sql.ts+++ b/src/primary/modules/capture-dashboard/capture-live/pace.sql.ts@@ -65,11 +65,13 @@  * On-site runs from one usual line before the first scan to now while today is open (to the last  * scan once the day has ended), capped by the planned hours. Active time drops breaks but keeps the  * walk between rooms; `gap_kind` splits every gap into counting, walking and break seconds.- * The scope is today for anyone who scanned today, the project to date otherwise. `oneUser`- * narrows it to `:userId`; limits are per person, so that person's figures do not change.+ * The scope is today for anyone who scanned today, the project to date otherwise; `todayOnly` keeps+ * everyone to today, so a new day starts empty. `oneUser` narrows it to `:userId`; limits are per+ * person, so that person's figures do not change.  */-export const paceCte = (consumables: boolean, oneUser = false): string => {+export const paceCte = (consumables: boolean, { oneUser = false, todayOnly = false }: { oneUser?: boolean; todayOnly?: boolean } = {}): string => {   const today = '(now() AT TIME ZONE :tz)::date';+  const dayScope = todayOnly ? `s.day = ${today}` : `CASE WHEN EXISTS (SELECT 1 FROM worked_today w WHERE w.user_id = s.user_id) THEN s.day = ${today} ELSE TRUE END`;   return `   scan AS (     SELECT k.id, k.created_by AS user_id, (k.created_at AT TIME ZONE :tz)::date AS day, k.created_at, k.location_id@@ -101,10 +103,7 @@     LEFT JOIN plan_day p ON p.user_id = s.user_id AND p.day = s.day     -- a day stays open until the capturer has been silent past the day-end limit     CROSS JOIN LATERAL (SELECT s.day = ${today} AND now() - s.last_scan <= make_interval(secs => :dayEndSecs) AS open) o-    WHERE CASE-            WHEN EXISTS (SELECT 1 FROM worked_today w WHERE w.user_id = s.user_id) THEN s.day = ${today}-            ELSE TRUE-          END+    WHERE ${dayScope}   ),   gap AS (     SELECT g.user_id, g.day, g.secs, g.moved, g.at, g.prev_at, g.location_id, g.prev_location_id

Apply and checkin this order

  1. cd backendApi && git apply day-reset-api.patch. Both API watchers rebuild; wait for Found 0 errors and Nest application successfully started in .run/primary.log.
  2. No swagger change is expected: CaptureTeamPerfRowDto and CaptureLiveBannerDto keep every field, so the front end keeps running as it is.
  3. Open the Live tab on a running project before anyone has captured today: every card is blank and the header reads “Team speed —”, while the top bar, the room list and the red flags read as before.
  4. After the first captures of the day, only those people’s cards fill in, and they move to the top.
  5. Before committing: npm run quality, and a CAP-<n>: subject — the hooks enforce it.

What was checkedand what was not

Patch appliesYes — git apply --check passes against main.
FormattingRun through the repository’s own Prettier config, the step the commit hook runs.
CompilesNot checked — no type-check was run. Step 1 is the first compile.
SQL against dataNot run. Steps 3 and 4 are the first real check.

Decisionsso they are not rediscovered

  1. 1A day is the tab’s day — UTC, like the rest of the Live tab, until projects carry a timezone.
  2. 2Today’s audits on a consumables run are audits made today, credited to whoever captured that product in that room, whichever day they captured it.
  3. 3On an asset run the Audit cell counts today’s captures, over today’s captures.
  4. 4The panel still lists the whole team; anyone not working today shows as a blank card rather than disappearing.
  5. 5Red flags and the top bar’s audits pending (5 per person) stay project to date.