@burdenoff/microfe-ecoimpacthub 2026.1030.2 → 2026.1030.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ecoimpacthub/EcoImpactHubRoutes.d.ts.map +1 -1
- package/dist/ecoimpacthub/EcoImpactHubRoutes.js +67 -43
- package/dist/ecoimpacthub/EcoImpactHubRoutes.js.map +1 -1
- package/dist/ecoimpacthub/components/EcoDiscussion.js +1 -1
- package/dist/ecoimpacthub/field/device.d.ts +26 -0
- package/dist/ecoimpacthub/field/device.d.ts.map +1 -0
- package/dist/ecoimpacthub/field/device.js +39 -0
- package/dist/ecoimpacthub/field/device.js.map +1 -0
- package/dist/ecoimpacthub/field/fieldSync.d.ts +22 -0
- package/dist/ecoimpacthub/field/fieldSync.d.ts.map +1 -0
- package/dist/ecoimpacthub/field/fieldSync.js +45 -0
- package/dist/ecoimpacthub/field/fieldSync.js.map +1 -0
- package/dist/ecoimpacthub/field/index.d.ts +8 -0
- package/dist/ecoimpacthub/field/index.d.ts.map +1 -0
- package/dist/ecoimpacthub/field/queueStore.d.ts +17 -0
- package/dist/ecoimpacthub/field/queueStore.d.ts.map +1 -0
- package/dist/ecoimpacthub/field/queueStore.js +102 -0
- package/dist/ecoimpacthub/field/queueStore.js.map +1 -0
- package/dist/ecoimpacthub/field/syncProcessor.d.ts +46 -0
- package/dist/ecoimpacthub/field/syncProcessor.d.ts.map +1 -0
- package/dist/ecoimpacthub/field/syncProcessor.js +89 -0
- package/dist/ecoimpacthub/field/syncProcessor.js.map +1 -0
- package/dist/ecoimpacthub/field/types.d.ts +72 -0
- package/dist/ecoimpacthub/field/types.d.ts.map +1 -0
- package/dist/ecoimpacthub/field/useFieldQueue.d.ts +47 -0
- package/dist/ecoimpacthub/field/useFieldQueue.d.ts.map +1 -0
- package/dist/ecoimpacthub/field/useFieldQueue.js +181 -0
- package/dist/ecoimpacthub/field/useFieldQueue.js.map +1 -0
- package/dist/ecoimpacthub/hooks/index.js +1 -1
- package/dist/ecoimpacthub/hooks/useEcoFiles.d.ts +12 -0
- package/dist/ecoimpacthub/hooks/useEcoFiles.d.ts.map +1 -1
- package/dist/ecoimpacthub/hooks/useEcoFiles.js +30 -23
- package/dist/ecoimpacthub/hooks/useEcoFiles.js.map +1 -1
- package/dist/ecoimpacthub/index.d.ts +1 -0
- package/dist/ecoimpacthub/index.d.ts.map +1 -1
- package/dist/ecoimpacthub/index.js +3 -1
- package/dist/ecoimpacthub/pages/DashboardPage.js +1 -1
- package/dist/ecoimpacthub/pages/_shared/EvidenceThumb.js +8 -8
- package/dist/ecoimpacthub/pages/carbon/DraftReviewPanel.js +78 -78
- package/dist/ecoimpacthub/pages/carbon/DraftReviewPanel.js.map +1 -1
- package/dist/ecoimpacthub/pages/carbon/DraftWithAiDialog.js +20 -20
- package/dist/ecoimpacthub/pages/carbon/SuggestFactorPanel.js +29 -29
- package/dist/ecoimpacthub/pages/carbonVisuals.js +1 -1
- package/dist/ecoimpacthub/pages/habitat/HabitatProjectDetailPage.js +1 -1
- package/dist/ecoimpacthub/pages/habitat/HabitatProjectsPage.js +1 -1
- package/dist/ecoimpacthub/pages/mobile/FieldQueuePanel.d.ts +11 -0
- package/dist/ecoimpacthub/pages/mobile/FieldQueuePanel.d.ts.map +1 -0
- package/dist/ecoimpacthub/pages/mobile/FieldQueuePanel.js +119 -0
- package/dist/ecoimpacthub/pages/mobile/FieldQueuePanel.js.map +1 -0
- package/dist/ecoimpacthub/pages/mobile/MobileDashboardPage.d.ts +12 -0
- package/dist/ecoimpacthub/pages/mobile/MobileDashboardPage.d.ts.map +1 -0
- package/dist/ecoimpacthub/pages/mobile/MobileDashboardPage.js +253 -0
- package/dist/ecoimpacthub/pages/mobile/MobileDashboardPage.js.map +1 -0
- package/dist/ecoimpacthub/pages/mobile/MobileRecordObservationPage.d.ts +13 -0
- package/dist/ecoimpacthub/pages/mobile/MobileRecordObservationPage.d.ts.map +1 -0
- package/dist/ecoimpacthub/pages/mobile/MobileRecordObservationPage.js +494 -0
- package/dist/ecoimpacthub/pages/mobile/MobileRecordObservationPage.js.map +1 -0
- package/dist/ecoimpacthub/pages/mobile/MobileShell.d.ts +10 -0
- package/dist/ecoimpacthub/pages/mobile/MobileShell.d.ts.map +1 -0
- package/dist/ecoimpacthub/pages/mobile/MobileShell.js +120 -0
- package/dist/ecoimpacthub/pages/mobile/MobileShell.js.map +1 -0
- package/dist/ecoimpacthub/pages/observations/ObservationsPage.d.ts.map +1 -1
- package/dist/ecoimpacthub/pages/observations/ObservationsPage.js +132 -126
- package/dist/ecoimpacthub/pages/observations/ObservationsPage.js.map +1 -1
- package/dist/ecoimpacthub/pages/reports/ReportDetailPage.js +1 -1
- package/dist/ecoimpacthub/pages/reports/ReportsPage.js +1 -1
- package/dist/ecoimpacthub/pages/sites/SiteDetailPage.js +1 -1
- package/dist/ecoimpacthub/pages/sites/SitesPage.js +1 -1
- package/dist/ecoimpacthub/pages/species/SpeciesDetailPage.js +9 -9
- package/dist/ecoimpacthub/pages/species/SpeciesPage.js +1 -1
- package/dist/ecoimpacthub/utils/graphqlErrors.js +8 -2
- package/dist/ecoimpacthub/utils/graphqlErrors.js.map +1 -1
- package/dist/index.js +8 -6
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EcoImpactHubRoutes.d.ts","sourceRoot":"","sources":["../../src/ecoimpacthub/EcoImpactHubRoutes.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"EcoImpactHubRoutes.d.ts","sourceRoot":"","sources":["../../src/ecoimpacthub/EcoImpactHubRoutes.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,EAAE,EAA4C,MAAM,OAAO,CAAC;AAkH1E,eAAO,MAAM,kBAAkB,EAAE,EAyEhC,CAAC"}
|
|
@@ -1,88 +1,112 @@
|
|
|
1
1
|
import { useEcoImpactHub as e } from "./context/EcoImpactHubContext.js";
|
|
2
2
|
import { FeatureFlagGate as t } from "./components/FeatureFlagGate.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
3
|
+
import { useFieldSyncBridge as n } from "./field/useFieldQueue.js";
|
|
4
|
+
import { DashboardPage as r } from "./pages/DashboardPage.js";
|
|
5
|
+
import { Suspense as i, lazy as a, useState as o } from "react";
|
|
6
|
+
import { Navigate as s, Route as c, Routes as l, useLocation as u } from "react-router";
|
|
7
|
+
import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
8
|
+
import { Spinner as p } from "@burdenoff/fe-libs/ui";
|
|
8
9
|
//#region src/ecoimpacthub/EcoImpactHubRoutes.tsx
|
|
9
|
-
var
|
|
10
|
+
var m = a(() => import("./pages/sites/SitesPage.js").then((e) => ({ default: e.SitesPage }))), h = a(() => import("./pages/sites/SiteDetailPage.js").then((e) => ({ default: e.SiteDetailPage }))), g = a(() => import("./pages/monitoring/MonitoringPage.js").then((e) => ({ default: e.MonitoringPage }))), _ = a(() => import("./pages/species/SpeciesPage.js").then((e) => ({ default: e.SpeciesPage }))), v = a(() => import("./pages/species/SpeciesDetailPage.js").then((e) => ({ default: e.SpeciesDetailPage }))), y = a(() => import("./pages/observations/ObservationsPage.js").then((e) => ({ default: e.ObservationsPage }))), b = a(() => import("./pages/habitat/HabitatProjectsPage.js").then((e) => ({ default: e.HabitatProjectsPage }))), x = a(() => import("./pages/habitat/HabitatProjectDetailPage.js").then((e) => ({ default: e.HabitatProjectDetailPage }))), S = a(() => import("./pages/carbon/CarbonPage.js").then((e) => ({ default: e.CarbonPage }))), C = a(() => import("./pages/reports/ReportsPage.js").then((e) => ({ default: e.ReportsPage }))), w = a(() => import("./pages/reports/ReportDetailPage.js").then((e) => ({ default: e.ReportDetailPage }))), T = a(() => import("./pages/mobile/MobileDashboardPage.js").then((e) => ({ default: e.MobileDashboardPage }))), E = a(() => import("./pages/mobile/MobileRecordObservationPage.js").then((e) => ({ default: e.MobileRecordObservationPage }))), D = () => (n(), null), O = "ecoimpacthub.field.nativeHomeShown";
|
|
11
|
+
function k() {
|
|
12
|
+
if (!globalThis.Capacitor?.isNativePlatform?.()) return !1;
|
|
13
|
+
try {
|
|
14
|
+
if (sessionStorage.getItem(O)) return !1;
|
|
15
|
+
sessionStorage.setItem(O, "1");
|
|
16
|
+
} catch {}
|
|
17
|
+
return !0;
|
|
18
|
+
}
|
|
19
|
+
var A = () => {
|
|
20
|
+
let [e] = o(k);
|
|
21
|
+
return e ? /* @__PURE__ */ d(s, {
|
|
22
|
+
to: "mobile",
|
|
23
|
+
replace: !0
|
|
24
|
+
}) : /* @__PURE__ */ d(r, {});
|
|
25
|
+
}, j = () => /* @__PURE__ */ d("div", {
|
|
10
26
|
className: "flex min-h-[400px] items-center justify-center",
|
|
11
|
-
children: /* @__PURE__ */
|
|
27
|
+
children: /* @__PURE__ */ d(p, { size: "lg" })
|
|
12
28
|
});
|
|
13
|
-
function
|
|
14
|
-
return /* @__PURE__ */
|
|
29
|
+
function M(e, n) {
|
|
30
|
+
return /* @__PURE__ */ d(t, {
|
|
15
31
|
flag: e,
|
|
16
32
|
children: n
|
|
17
33
|
});
|
|
18
34
|
}
|
|
19
|
-
var
|
|
20
|
-
let { basePath: t, isStandalone:
|
|
21
|
-
return
|
|
35
|
+
var N = () => {
|
|
36
|
+
let { basePath: t, isStandalone: n } = e(), r = u(), a = t === "/" ? "" : (t ?? "").replace(/\/$/, "");
|
|
37
|
+
return a && !n && !(r.pathname === a || r.pathname.startsWith(a + "/")) ? null : /* @__PURE__ */ f("div", {
|
|
22
38
|
className: "flex min-h-full flex-col",
|
|
23
|
-
children: /* @__PURE__ */
|
|
24
|
-
fallback: /* @__PURE__ */
|
|
25
|
-
children: /* @__PURE__ */
|
|
26
|
-
/* @__PURE__ */
|
|
39
|
+
children: [/* @__PURE__ */ d(D, {}), /* @__PURE__ */ d(i, {
|
|
40
|
+
fallback: /* @__PURE__ */ d(j, {}),
|
|
41
|
+
children: /* @__PURE__ */ f(l, { children: [
|
|
42
|
+
/* @__PURE__ */ d(c, {
|
|
27
43
|
index: !0,
|
|
28
|
-
element:
|
|
44
|
+
element: M("ecoimpacthub.page.dashboard", /* @__PURE__ */ d(A, {}))
|
|
29
45
|
}),
|
|
30
|
-
/* @__PURE__ */
|
|
46
|
+
/* @__PURE__ */ d(c, {
|
|
31
47
|
path: "sites",
|
|
32
|
-
element:
|
|
48
|
+
element: M("ecoimpacthub.page.sites", /* @__PURE__ */ d(m, {}))
|
|
33
49
|
}),
|
|
34
|
-
/* @__PURE__ */
|
|
50
|
+
/* @__PURE__ */ d(c, {
|
|
35
51
|
path: "sites/:id",
|
|
36
|
-
element:
|
|
52
|
+
element: M("ecoimpacthub.page.sites", /* @__PURE__ */ d(h, {}))
|
|
37
53
|
}),
|
|
38
|
-
/* @__PURE__ */
|
|
54
|
+
/* @__PURE__ */ d(c, {
|
|
39
55
|
path: "monitoring",
|
|
40
|
-
element:
|
|
56
|
+
element: M("ecoimpacthub.page.monitoring", /* @__PURE__ */ d(g, {}))
|
|
41
57
|
}),
|
|
42
|
-
/* @__PURE__ */
|
|
58
|
+
/* @__PURE__ */ d(c, {
|
|
43
59
|
path: "species",
|
|
44
|
-
element:
|
|
60
|
+
element: M("ecoimpacthub.page.species", /* @__PURE__ */ d(_, {}))
|
|
45
61
|
}),
|
|
46
|
-
/* @__PURE__ */
|
|
62
|
+
/* @__PURE__ */ d(c, {
|
|
47
63
|
path: "species/:id",
|
|
48
|
-
element:
|
|
64
|
+
element: M("ecoimpacthub.page.species", /* @__PURE__ */ d(v, {}))
|
|
49
65
|
}),
|
|
50
|
-
/* @__PURE__ */
|
|
66
|
+
/* @__PURE__ */ d(c, {
|
|
51
67
|
path: "observations",
|
|
52
|
-
element:
|
|
68
|
+
element: M("ecoimpacthub.page.observations", /* @__PURE__ */ d(y, {}))
|
|
53
69
|
}),
|
|
54
|
-
/* @__PURE__ */
|
|
70
|
+
/* @__PURE__ */ d(c, {
|
|
55
71
|
path: "habitat",
|
|
56
|
-
element:
|
|
72
|
+
element: M("ecoimpacthub.page.habitat", /* @__PURE__ */ d(b, {}))
|
|
57
73
|
}),
|
|
58
|
-
/* @__PURE__ */
|
|
74
|
+
/* @__PURE__ */ d(c, {
|
|
59
75
|
path: "habitat/:id",
|
|
60
|
-
element:
|
|
76
|
+
element: M("ecoimpacthub.page.habitat", /* @__PURE__ */ d(x, {}))
|
|
61
77
|
}),
|
|
62
|
-
/* @__PURE__ */
|
|
78
|
+
/* @__PURE__ */ d(c, {
|
|
63
79
|
path: "carbon",
|
|
64
|
-
element:
|
|
80
|
+
element: M("ecoimpacthub.page.carbon", /* @__PURE__ */ d(S, {}))
|
|
65
81
|
}),
|
|
66
|
-
/* @__PURE__ */
|
|
82
|
+
/* @__PURE__ */ d(c, {
|
|
67
83
|
path: "reports",
|
|
68
|
-
element:
|
|
84
|
+
element: M("ecoimpacthub.page.reports", /* @__PURE__ */ d(C, {}))
|
|
69
85
|
}),
|
|
70
|
-
/* @__PURE__ */
|
|
86
|
+
/* @__PURE__ */ d(c, {
|
|
71
87
|
path: "reports/:id",
|
|
72
|
-
element:
|
|
88
|
+
element: M("ecoimpacthub.page.reports", /* @__PURE__ */ d(w, {}))
|
|
89
|
+
}),
|
|
90
|
+
/* @__PURE__ */ d(c, {
|
|
91
|
+
path: "mobile",
|
|
92
|
+
element: M("ecoimpacthub.page.mobile", /* @__PURE__ */ d(T, {}))
|
|
93
|
+
}),
|
|
94
|
+
/* @__PURE__ */ d(c, {
|
|
95
|
+
path: "mobile/record-observation",
|
|
96
|
+
element: M("ecoimpacthub.page.observations", /* @__PURE__ */ d(E, {}))
|
|
73
97
|
}),
|
|
74
|
-
/* @__PURE__ */
|
|
98
|
+
/* @__PURE__ */ d(c, {
|
|
75
99
|
path: "*",
|
|
76
|
-
element: /* @__PURE__ */
|
|
100
|
+
element: /* @__PURE__ */ d(s, {
|
|
77
101
|
to: "",
|
|
78
102
|
replace: !0
|
|
79
103
|
})
|
|
80
104
|
})
|
|
81
105
|
] })
|
|
82
|
-
})
|
|
106
|
+
})]
|
|
83
107
|
});
|
|
84
108
|
};
|
|
85
109
|
//#endregion
|
|
86
|
-
export {
|
|
110
|
+
export { N as EcoImpactHubRoutes };
|
|
87
111
|
|
|
88
112
|
//# sourceMappingURL=EcoImpactHubRoutes.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EcoImpactHubRoutes.js","names":[],"sources":["../../src/ecoimpacthub/EcoImpactHubRoutes.tsx"],"sourcesContent":["/**\n * EcoImpactHub Routes\n * @module ecoimpacthub\n *\n * Domain routes. The dashboard is eager-loaded (primary landing); all other\n * pages are lazy-loaded. Every route maps to a real `eco*` backend capability.\n */\n\nimport { type FC, lazy, Suspense, type ReactNode } from 'react';\nimport { Routes, Route, Navigate, useLocation } from 'react-router';\nimport { Spinner } from '@burdenoff/fe-libs/ui';\nimport { useEcoImpactHub } from './context/EcoImpactHubContext';\nimport { FeatureFlagGate } from './components/FeatureFlagGate';\n\n// Eager — primary landing\nimport { DashboardPage } from './pages/DashboardPage';\n\n// Sites (Environmental Monitoring)\nconst SitesPage = lazy(() =>\n import('./pages/sites/SitesPage').then((m) => ({ default: m.SitesPage }))\n);\nconst SiteDetailPage = lazy(() =>\n import('./pages/sites/SiteDetailPage').then((m) => ({ default: m.SiteDetailPage }))\n);\n\n// Monitoring (sensors / measurements / thresholds)\nconst MonitoringPage = lazy(() =>\n import('./pages/monitoring/MonitoringPage').then((m) => ({ default: m.MonitoringPage }))\n);\n\n// Species (Conservation)\nconst SpeciesPage = lazy(() =>\n import('./pages/species/SpeciesPage').then((m) => ({ default: m.SpeciesPage }))\n);\nconst SpeciesDetailPage = lazy(() =>\n import('./pages/species/SpeciesDetailPage').then((m) => ({ default: m.SpeciesDetailPage }))\n);\n\n// Observations (citizen science / field data)\nconst ObservationsPage = lazy(() =>\n import('./pages/observations/ObservationsPage').then((m) => ({ default: m.ObservationsPage }))\n);\n\n// Habitat projects & restoration\nconst HabitatProjectsPage = lazy(() =>\n import('./pages/habitat/HabitatProjectsPage').then((m) => ({ default: m.HabitatProjectsPage }))\n);\nconst HabitatProjectDetailPage = lazy(() =>\n import('./pages/habitat/HabitatProjectDetailPage').then((m) => ({\n default: m.HabitatProjectDetailPage,\n }))\n);\n\n// Carbon ledger / emission factors / reduction targets (Sustainability)\nconst CarbonPage = lazy(() =>\n import('./pages/carbon/CarbonPage').then((m) => ({ default: m.CarbonPage }))\n);\n\n// ESG Reports\nconst ReportsPage = lazy(() =>\n import('./pages/reports/ReportsPage').then((m) => ({ default: m.ReportsPage }))\n);\nconst ReportDetailPage = lazy(() =>\n import('./pages/reports/ReportDetailPage').then((m) => ({ default: m.ReportDetailPage }))\n);\n\nconst PageLoader: FC = () => (\n <div className=\"flex min-h-[400px] items-center justify-center\">\n <Spinner size=\"lg\" />\n </div>\n);\n\nfunction gated(flag: string, element: ReactNode): ReactNode {\n return <FeatureFlagGate flag={flag}>{element}</FeatureFlagGate>;\n}\n\nexport const EcoImpactHubRoutes: FC = () => {\n const { basePath, isStandalone } = useEcoImpactHub();\n const location = useLocation();\n\n const normalizedBasePath = basePath === '/' ? '' : (basePath ?? '').replace(/\\/$/, '');\n\n // Guard: only render if the current path matches basePath (integrated mode).\n if (normalizedBasePath && !isStandalone) {\n const pathMatches =\n location.pathname === normalizedBasePath ||\n location.pathname.startsWith(normalizedBasePath + '/');\n if (!pathMatches) {\n return null;\n }\n }\n\n return (\n <div className=\"flex min-h-full flex-col\">\n <Suspense fallback={<PageLoader />}>\n <Routes>\n <Route index element={gated('ecoimpacthub.page.dashboard', <DashboardPage />)} />\n\n <Route path=\"sites\" element={gated('ecoimpacthub.page.sites', <SitesPage />)} />\n <Route path=\"sites/:id\" element={gated('ecoimpacthub.page.sites', <SiteDetailPage />)} />\n\n <Route\n path=\"monitoring\"\n element={gated('ecoimpacthub.page.monitoring', <MonitoringPage />)}\n />\n\n <Route path=\"species\" element={gated('ecoimpacthub.page.species', <SpeciesPage />)} />\n <Route\n path=\"species/:id\"\n element={gated('ecoimpacthub.page.species', <SpeciesDetailPage />)}\n />\n\n <Route\n path=\"observations\"\n element={gated('ecoimpacthub.page.observations', <ObservationsPage />)}\n />\n\n <Route\n path=\"habitat\"\n element={gated('ecoimpacthub.page.habitat', <HabitatProjectsPage />)}\n />\n <Route\n path=\"habitat/:id\"\n element={gated('ecoimpacthub.page.habitat', <HabitatProjectDetailPage />)}\n />\n\n <Route path=\"carbon\" element={gated('ecoimpacthub.page.carbon', <CarbonPage />)} />\n\n <Route path=\"reports\" element={gated('ecoimpacthub.page.reports', <ReportsPage />)} />\n <Route\n path=\"reports/:id\"\n element={gated('ecoimpacthub.page.reports', <ReportDetailPage />)}\n />\n\n <Route path=\"*\" element={<Navigate to=\"\" replace />} />\n </Routes>\n </Suspense>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EcoImpactHubRoutes.js","names":[],"sources":["../../src/ecoimpacthub/EcoImpactHubRoutes.tsx"],"sourcesContent":["/**\n * EcoImpactHub Routes\n * @module ecoimpacthub\n *\n * Domain routes. The dashboard is eager-loaded (primary landing); all other\n * pages are lazy-loaded. Every route maps to a real `eco*` backend capability.\n */\n\nimport { type FC, lazy, Suspense, type ReactNode, useState } from 'react';\nimport { Routes, Route, Navigate, useLocation } from 'react-router';\nimport { Spinner } from '@burdenoff/fe-libs/ui';\nimport { useEcoImpactHub } from './context/EcoImpactHubContext';\nimport { FeatureFlagGate } from './components/FeatureFlagGate';\nimport { useFieldSyncBridge } from './field/useFieldQueue';\n\n// Eager — primary landing\nimport { DashboardPage } from './pages/DashboardPage';\n\n// Sites (Environmental Monitoring)\nconst SitesPage = lazy(() =>\n import('./pages/sites/SitesPage').then((m) => ({ default: m.SitesPage }))\n);\nconst SiteDetailPage = lazy(() =>\n import('./pages/sites/SiteDetailPage').then((m) => ({ default: m.SiteDetailPage }))\n);\n\n// Monitoring (sensors / measurements / thresholds)\nconst MonitoringPage = lazy(() =>\n import('./pages/monitoring/MonitoringPage').then((m) => ({ default: m.MonitoringPage }))\n);\n\n// Species (Conservation)\nconst SpeciesPage = lazy(() =>\n import('./pages/species/SpeciesPage').then((m) => ({ default: m.SpeciesPage }))\n);\nconst SpeciesDetailPage = lazy(() =>\n import('./pages/species/SpeciesDetailPage').then((m) => ({ default: m.SpeciesDetailPage }))\n);\n\n// Observations (citizen science / field data)\nconst ObservationsPage = lazy(() =>\n import('./pages/observations/ObservationsPage').then((m) => ({ default: m.ObservationsPage }))\n);\n\n// Habitat projects & restoration\nconst HabitatProjectsPage = lazy(() =>\n import('./pages/habitat/HabitatProjectsPage').then((m) => ({ default: m.HabitatProjectsPage }))\n);\nconst HabitatProjectDetailPage = lazy(() =>\n import('./pages/habitat/HabitatProjectDetailPage').then((m) => ({\n default: m.HabitatProjectDetailPage,\n }))\n);\n\n// Carbon ledger / emission factors / reduction targets (Sustainability)\nconst CarbonPage = lazy(() =>\n import('./pages/carbon/CarbonPage').then((m) => ({ default: m.CarbonPage }))\n);\n\n// ESG Reports\nconst ReportsPage = lazy(() =>\n import('./pages/reports/ReportsPage').then((m) => ({ default: m.ReportsPage }))\n);\nconst ReportDetailPage = lazy(() =>\n import('./pages/reports/ReportDetailPage').then((m) => ({ default: m.ReportDetailPage }))\n);\n\n// Mobile field app (Capacitor iOS/Android home + offline field capture)\nconst MobileDashboardPage = lazy(() =>\n import('./pages/mobile/MobileDashboardPage').then((m) => ({ default: m.MobileDashboardPage }))\n);\nconst MobileRecordObservationPage = lazy(() =>\n import('./pages/mobile/MobileRecordObservationPage').then((m) => ({\n default: m.MobileRecordObservationPage,\n }))\n);\n\n/**\n * Drains the offline field-observation queue for the active workspace while\n * the MFE is mounted, whichever page is showing (see `field/fieldSync.ts`).\n */\nconst FieldSyncBridge: FC = () => {\n useFieldSyncBridge();\n return null;\n};\n\nconst NATIVE_HOME_KEY = 'ecoimpacthub.field.nativeHomeShown';\n\n/**\n * Inside the installed iOS/Android app the first screen of each launch is the\n * field app's mobile dashboard; after that the regular dashboard stays one tap\n * away (sidebar \"Dashboard\"). Capacitor injects `window.Capacitor`, so the MFE\n * needs no Capacitor dependency to detect it.\n */\nfunction shouldOpenNativeHome(): boolean {\n const cap = (globalThis as { Capacitor?: { isNativePlatform?: () => boolean } }).Capacitor;\n if (!cap?.isNativePlatform?.()) return false;\n try {\n if (sessionStorage.getItem(NATIVE_HOME_KEY)) return false;\n sessionStorage.setItem(NATIVE_HOME_KEY, '1');\n } catch {\n // Storage unavailable: still open the field home this time.\n }\n return true;\n}\n\nconst DashboardIndex: FC = () => {\n const [openNativeHome] = useState(shouldOpenNativeHome);\n if (openNativeHome) return <Navigate to=\"mobile\" replace />;\n return <DashboardPage />;\n};\n\nconst PageLoader: FC = () => (\n <div className=\"flex min-h-[400px] items-center justify-center\">\n <Spinner size=\"lg\" />\n </div>\n);\n\nfunction gated(flag: string, element: ReactNode): ReactNode {\n return <FeatureFlagGate flag={flag}>{element}</FeatureFlagGate>;\n}\n\nexport const EcoImpactHubRoutes: FC = () => {\n const { basePath, isStandalone } = useEcoImpactHub();\n const location = useLocation();\n\n const normalizedBasePath = basePath === '/' ? '' : (basePath ?? '').replace(/\\/$/, '');\n\n // Guard: only render if the current path matches basePath (integrated mode).\n if (normalizedBasePath && !isStandalone) {\n const pathMatches =\n location.pathname === normalizedBasePath ||\n location.pathname.startsWith(normalizedBasePath + '/');\n if (!pathMatches) {\n return null;\n }\n }\n\n return (\n <div className=\"flex min-h-full flex-col\">\n <FieldSyncBridge />\n <Suspense fallback={<PageLoader />}>\n <Routes>\n <Route index element={gated('ecoimpacthub.page.dashboard', <DashboardIndex />)} />\n\n <Route path=\"sites\" element={gated('ecoimpacthub.page.sites', <SitesPage />)} />\n <Route path=\"sites/:id\" element={gated('ecoimpacthub.page.sites', <SiteDetailPage />)} />\n\n <Route\n path=\"monitoring\"\n element={gated('ecoimpacthub.page.monitoring', <MonitoringPage />)}\n />\n\n <Route path=\"species\" element={gated('ecoimpacthub.page.species', <SpeciesPage />)} />\n <Route\n path=\"species/:id\"\n element={gated('ecoimpacthub.page.species', <SpeciesDetailPage />)}\n />\n\n <Route\n path=\"observations\"\n element={gated('ecoimpacthub.page.observations', <ObservationsPage />)}\n />\n\n <Route\n path=\"habitat\"\n element={gated('ecoimpacthub.page.habitat', <HabitatProjectsPage />)}\n />\n <Route\n path=\"habitat/:id\"\n element={gated('ecoimpacthub.page.habitat', <HabitatProjectDetailPage />)}\n />\n\n <Route path=\"carbon\" element={gated('ecoimpacthub.page.carbon', <CarbonPage />)} />\n\n <Route path=\"reports\" element={gated('ecoimpacthub.page.reports', <ReportsPage />)} />\n <Route\n path=\"reports/:id\"\n element={gated('ecoimpacthub.page.reports', <ReportDetailPage />)}\n />\n\n <Route\n path=\"mobile\"\n element={gated('ecoimpacthub.page.mobile', <MobileDashboardPage />)}\n />\n <Route\n path=\"mobile/record-observation\"\n element={gated('ecoimpacthub.page.observations', <MobileRecordObservationPage />)}\n />\n\n <Route path=\"*\" element={<Navigate to=\"\" replace />} />\n </Routes>\n </Suspense>\n </div>\n );\n};\n"],"mappings":";;;;;;;;;AAmBA,IAAM,IAAY,QAChB,OAAO,8BAA2B,MAAM,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,CAC1E,GACM,IAAiB,QACrB,OAAO,mCAAgC,MAAM,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,CACpF,GAGM,IAAiB,QACrB,OAAO,wCAAqC,MAAM,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,CACzF,GAGM,IAAc,QAClB,OAAO,kCAA+B,MAAM,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,CAChF,GACM,IAAoB,QACxB,OAAO,wCAAqC,MAAM,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAC5F,GAGM,IAAmB,QACvB,OAAO,4CAAyC,MAAM,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,CAC/F,GAGM,IAAsB,QAC1B,OAAO,0CAAuC,MAAM,OAAO,EAAE,SAAS,EAAE,oBAAoB,EAAE,CAChG,GACM,IAA2B,QAC/B,OAAO,+CAA4C,MAAM,OAAO,EAC9D,SAAS,EAAE,yBACb,EAAE,CACJ,GAGM,IAAa,QACjB,OAAO,gCAA6B,MAAM,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,CAC7E,GAGM,IAAc,QAClB,OAAO,kCAA+B,MAAM,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,CAChF,GACM,IAAmB,QACvB,OAAO,uCAAoC,MAAM,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,CAC1F,GAGM,IAAsB,QAC1B,OAAO,yCAAsC,MAAM,OAAO,EAAE,SAAS,EAAE,oBAAoB,EAAE,CAC/F,GACM,IAA8B,QAClC,OAAO,iDAA8C,MAAM,OAAO,EAChE,SAAS,EAAE,4BACb,EAAE,CACJ,GAMM,WACJ,EAAmB,GACZ,OAGH,IAAkB;AAQxB,SAAS,IAAgC;CAEvC,IAAI,CADS,WAAoE,WACvE,mBAAmB,GAAG,OAAO;CACvC,IAAI;EACF,IAAI,eAAe,QAAQ,CAAe,GAAG,OAAO;EACpD,eAAe,QAAQ,GAAiB,GAAG;CAC7C,QAAQ,CAER;CACA,OAAO;AACT;AAEA,IAAM,UAA2B;CAC/B,IAAM,CAAC,KAAkB,EAAS,CAAoB;CAEtD,OADI,IAAuB,kBAAC,GAAD;EAAU,IAAG;EAAS,SAAA;CAAS,CAAA,IACnD,kBAAC,GAAD,CAAgB,CAAA;AACzB,GAEM,UACJ,kBAAC,OAAD;CAAK,WAAU;WACb,kBAAC,GAAD,EAAS,MAAK,KAAM,CAAA;AACjB,CAAA;AAGP,SAAS,EAAM,GAAc,GAA+B;CAC1D,OAAO,kBAAC,GAAD;EAAuB;YAAO;CAAyB,CAAA;AAChE;AAEA,IAAa,UAA+B;CAC1C,IAAM,EAAE,aAAU,oBAAiB,EAAgB,GAC7C,IAAW,EAAY,GAEvB,IAAqB,MAAa,MAAM,MAAM,KAAY,IAAI,QAAQ,OAAO,EAAE;CAYrF,OATI,KAAsB,CAAC,KAIrB,EAFF,EAAS,aAAa,KACtB,EAAS,SAAS,WAAW,IAAqB,GAAG,KAE9C,OAKT,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,GAAD,CAAkB,CAAA,GAClB,kBAAC,GAAD;GAAU,UAAU,kBAAC,GAAD,CAAa,CAAA;aAC/B,kBAAC,GAAD,EAAA,UAAA;IACE,kBAAC,GAAD;KAAO,OAAA;KAAM,SAAS,EAAM,+BAA+B,kBAAC,GAAD,CAAiB,CAAA,CAAC;IAAI,CAAA;IAEjF,kBAAC,GAAD;KAAO,MAAK;KAAQ,SAAS,EAAM,2BAA2B,kBAAC,GAAD,CAAY,CAAA,CAAC;IAAI,CAAA;IAC/E,kBAAC,GAAD;KAAO,MAAK;KAAY,SAAS,EAAM,2BAA2B,kBAAC,GAAD,CAAiB,CAAA,CAAC;IAAI,CAAA;IAExF,kBAAC,GAAD;KACE,MAAK;KACL,SAAS,EAAM,gCAAgC,kBAAC,GAAD,CAAiB,CAAA,CAAC;IAClE,CAAA;IAED,kBAAC,GAAD;KAAO,MAAK;KAAU,SAAS,EAAM,6BAA6B,kBAAC,GAAD,CAAc,CAAA,CAAC;IAAI,CAAA;IACrF,kBAAC,GAAD;KACE,MAAK;KACL,SAAS,EAAM,6BAA6B,kBAAC,GAAD,CAAoB,CAAA,CAAC;IAClE,CAAA;IAED,kBAAC,GAAD;KACE,MAAK;KACL,SAAS,EAAM,kCAAkC,kBAAC,GAAD,CAAmB,CAAA,CAAC;IACtE,CAAA;IAED,kBAAC,GAAD;KACE,MAAK;KACL,SAAS,EAAM,6BAA6B,kBAAC,GAAD,CAAsB,CAAA,CAAC;IACpE,CAAA;IACD,kBAAC,GAAD;KACE,MAAK;KACL,SAAS,EAAM,6BAA6B,kBAAC,GAAD,CAA2B,CAAA,CAAC;IACzE,CAAA;IAED,kBAAC,GAAD;KAAO,MAAK;KAAS,SAAS,EAAM,4BAA4B,kBAAC,GAAD,CAAa,CAAA,CAAC;IAAI,CAAA;IAElF,kBAAC,GAAD;KAAO,MAAK;KAAU,SAAS,EAAM,6BAA6B,kBAAC,GAAD,CAAc,CAAA,CAAC;IAAI,CAAA;IACrF,kBAAC,GAAD;KACE,MAAK;KACL,SAAS,EAAM,6BAA6B,kBAAC,GAAD,CAAmB,CAAA,CAAC;IACjE,CAAA;IAED,kBAAC,GAAD;KACE,MAAK;KACL,SAAS,EAAM,4BAA4B,kBAAC,GAAD,CAAsB,CAAA,CAAC;IACnE,CAAA;IACD,kBAAC,GAAD;KACE,MAAK;KACL,SAAS,EAAM,kCAAkC,kBAAC,GAAD,CAA8B,CAAA,CAAC;IACjF,CAAA;IAED,kBAAC,GAAD;KAAO,MAAK;KAAI,SAAS,kBAAC,GAAD;MAAU,IAAG;MAAG,SAAA;KAAS,CAAA;IAAI,CAAA;GAChD,EAAA,CAAA;EACA,CAAA,CACP;;AAET"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useEcoImpactHub as e } from "../context/EcoImpactHubContext.js";
|
|
2
|
+
import "../context/index.js";
|
|
2
3
|
import { formatDateTime as t } from "../utils/formatters.js";
|
|
3
4
|
import { ErrorView as n, LoadingView as r, Section as i } from "./StateViews.js";
|
|
4
5
|
import { EmptyState as a } from "./EmptyState.js";
|
|
5
|
-
import "../context/index.js";
|
|
6
6
|
import { useEcoComments as o, useEcoCreateComment as s } from "../hooks/useEcoComments.js";
|
|
7
7
|
import "../hooks/index.js";
|
|
8
8
|
import { useState as c } from "react";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { FieldPhoto } from './types';
|
|
2
|
+
|
|
3
|
+
export interface FieldPosition {
|
|
4
|
+
latitude: number;
|
|
5
|
+
longitude: number;
|
|
6
|
+
accuracyMeters: number | null;
|
|
7
|
+
}
|
|
8
|
+
export interface FieldDeviceAdapter {
|
|
9
|
+
/** Short name for diagnostics, e.g. `web` or `capacitor`. */
|
|
10
|
+
readonly kind: string;
|
|
11
|
+
/** True when a location fix can be requested at all. */
|
|
12
|
+
canLocate(): boolean;
|
|
13
|
+
/** Resolve a GPS fix, or reject with an observer-readable message. */
|
|
14
|
+
getCurrentPosition(): Promise<FieldPosition>;
|
|
15
|
+
/**
|
|
16
|
+
* Take (or pick) a photo with the native camera UI. Resolves to null when
|
|
17
|
+
* the observer cancels. Absent on the web adapter, where the capture form
|
|
18
|
+
* falls back to a camera-capable file input.
|
|
19
|
+
*/
|
|
20
|
+
capturePhoto?: () => Promise<FieldPhoto | null>;
|
|
21
|
+
}
|
|
22
|
+
export declare const webFieldDeviceAdapter: FieldDeviceAdapter;
|
|
23
|
+
/** Called by a native shell at boot to route GPS/camera through the OS APIs. */
|
|
24
|
+
export declare function registerFieldDeviceAdapter(adapter: FieldDeviceAdapter): void;
|
|
25
|
+
export declare function getFieldDeviceAdapter(): FieldDeviceAdapter;
|
|
26
|
+
//# sourceMappingURL=device.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device.d.ts","sourceRoot":"","sources":["../../../src/ecoimpacthub/field/device.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,kBAAkB;IACjC,6DAA6D;IAC7D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,wDAAwD;IACxD,SAAS,IAAI,OAAO,CAAC;IACrB,sEAAsE;IACtE,kBAAkB,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;IAC7C;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;CACjD;AAiBD,eAAO,MAAM,qBAAqB,EAAE,kBAsBnC,CAAC;AAIF,gFAAgF;AAChF,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAE5E;AAED,wBAAgB,qBAAqB,IAAI,kBAAkB,CAE1D"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//#region src/ecoimpacthub/field/device.ts
|
|
2
|
+
var e = 15e3;
|
|
3
|
+
function t(e) {
|
|
4
|
+
switch (e.code) {
|
|
5
|
+
case e.PERMISSION_DENIED: return "Location permission denied. Enter the coordinates manually or allow location access.";
|
|
6
|
+
case e.POSITION_UNAVAILABLE: return "No position fix available right now. Try again with a clear view of the sky.";
|
|
7
|
+
case e.TIMEOUT: return "Timed out waiting for a GPS fix. Try again.";
|
|
8
|
+
default: return "Could not read this device’s location.";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
var n = {
|
|
12
|
+
kind: "web",
|
|
13
|
+
canLocate: () => typeof navigator < "u" && "geolocation" in navigator,
|
|
14
|
+
getCurrentPosition: () => new Promise((n, r) => {
|
|
15
|
+
if (typeof navigator > "u" || !("geolocation" in navigator)) {
|
|
16
|
+
r(/* @__PURE__ */ Error("This device does not expose a location service."));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
navigator.geolocation.getCurrentPosition((e) => n({
|
|
20
|
+
latitude: e.coords.latitude,
|
|
21
|
+
longitude: e.coords.longitude,
|
|
22
|
+
accuracyMeters: Number.isFinite(e.coords.accuracy) ? e.coords.accuracy : null
|
|
23
|
+
}), (e) => r(Error(t(e))), {
|
|
24
|
+
enableHighAccuracy: !0,
|
|
25
|
+
timeout: e,
|
|
26
|
+
maximumAge: 0
|
|
27
|
+
});
|
|
28
|
+
})
|
|
29
|
+
};
|
|
30
|
+
function r(e) {
|
|
31
|
+
n = e;
|
|
32
|
+
}
|
|
33
|
+
function i() {
|
|
34
|
+
return n;
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { i as getFieldDeviceAdapter, r as registerFieldDeviceAdapter };
|
|
38
|
+
|
|
39
|
+
//# sourceMappingURL=device.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device.js","names":[],"sources":["../../../src/ecoimpacthub/field/device.ts"],"sourcesContent":["/**\n * Offline field capture — device access (GPS + camera).\n * @module ecoimpacthub/field/device\n *\n * The MFE stays free of Capacitor: it talks to a small adapter. The default\n * adapter uses the web Geolocation API and leaves photo capture to an\n * `<input type=\"file\" capture=\"environment\">`, which opens the camera on\n * mobile browsers. The native app shell registers an adapter backed by\n * `@capacitor/geolocation` and `@capacitor/camera` at boot through\n * {@link registerFieldDeviceAdapter}, so the installed iOS/Android app uses the\n * OS location and camera APIs with their native permission prompts.\n */\n\nimport type { FieldPhoto } from './types';\n\nexport interface FieldPosition {\n latitude: number;\n longitude: number;\n accuracyMeters: number | null;\n}\n\nexport interface FieldDeviceAdapter {\n /** Short name for diagnostics, e.g. `web` or `capacitor`. */\n readonly kind: string;\n /** True when a location fix can be requested at all. */\n canLocate(): boolean;\n /** Resolve a GPS fix, or reject with an observer-readable message. */\n getCurrentPosition(): Promise<FieldPosition>;\n /**\n * Take (or pick) a photo with the native camera UI. Resolves to null when\n * the observer cancels. Absent on the web adapter, where the capture form\n * falls back to a camera-capable file input.\n */\n capturePhoto?: () => Promise<FieldPhoto | null>;\n}\n\nconst GEOLOCATION_TIMEOUT_MS = 15_000;\n\nfunction describeGeolocationError(error: GeolocationPositionError): string {\n switch (error.code) {\n case error.PERMISSION_DENIED:\n return 'Location permission denied. Enter the coordinates manually or allow location access.';\n case error.POSITION_UNAVAILABLE:\n return 'No position fix available right now. Try again with a clear view of the sky.';\n case error.TIMEOUT:\n return 'Timed out waiting for a GPS fix. Try again.';\n default:\n return 'Could not read this device’s location.';\n }\n}\n\nexport const webFieldDeviceAdapter: FieldDeviceAdapter = {\n kind: 'web',\n canLocate: () => typeof navigator !== 'undefined' && 'geolocation' in navigator,\n getCurrentPosition: () =>\n new Promise<FieldPosition>((resolve, reject) => {\n if (typeof navigator === 'undefined' || !('geolocation' in navigator)) {\n reject(new Error('This device does not expose a location service.'));\n return;\n }\n navigator.geolocation.getCurrentPosition(\n (position) =>\n resolve({\n latitude: position.coords.latitude,\n longitude: position.coords.longitude,\n accuracyMeters: Number.isFinite(position.coords.accuracy)\n ? position.coords.accuracy\n : null,\n }),\n (error) => reject(new Error(describeGeolocationError(error))),\n { enableHighAccuracy: true, timeout: GEOLOCATION_TIMEOUT_MS, maximumAge: 0 }\n );\n }),\n};\n\nlet activeAdapter: FieldDeviceAdapter = webFieldDeviceAdapter;\n\n/** Called by a native shell at boot to route GPS/camera through the OS APIs. */\nexport function registerFieldDeviceAdapter(adapter: FieldDeviceAdapter): void {\n activeAdapter = adapter;\n}\n\nexport function getFieldDeviceAdapter(): FieldDeviceAdapter {\n return activeAdapter;\n}\n"],"mappings":";AAoCA,IAAM,IAAyB;AAE/B,SAAS,EAAyB,GAAyC;CACzE,QAAQ,EAAM,MAAd;EACE,KAAK,EAAM,mBACT,OAAO;EACT,KAAK,EAAM,sBACT,OAAO;EACT,KAAK,EAAM,SACT,OAAO;EACT,SACE,OAAO;CACX;AACF;AA0BA,IAAI,IAAoC;CAvBtC,MAAM;CACN,iBAAiB,OAAO,YAAc,OAAe,iBAAiB;CACtE,0BACE,IAAI,SAAwB,GAAS,MAAW;EAC9C,IAAI,OAAO,YAAc,OAAe,EAAE,iBAAiB,YAAY;GACrE,EAAO,gBAAI,MAAM,iDAAiD,CAAC;GACnE;EACF;EACA,UAAU,YAAY,oBACnB,MACC,EAAQ;GACN,UAAU,EAAS,OAAO;GAC1B,WAAW,EAAS,OAAO;GAC3B,gBAAgB,OAAO,SAAS,EAAS,OAAO,QAAQ,IACpD,EAAS,OAAO,WAChB;EACN,CAAC,IACF,MAAU,EAAW,MAAM,EAAyB,CAAK,CAAC,CAAC,GAC5D;GAAE,oBAAoB;GAAM,SAAS;GAAwB,YAAY;EAAE,CAC7E;CACF,CAAC;AAGmC;AAGxC,SAAgB,EAA2B,GAAmC;CAC5E,IAAgB;AAClB;AAEA,SAAgB,IAA4C;CAC1D,OAAO;AACT"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { FieldSyncResult, FieldSyncTransport } from './syncProcessor';
|
|
2
|
+
|
|
3
|
+
export interface RegisteredFieldSyncer {
|
|
4
|
+
workspaceId: string;
|
|
5
|
+
transport: FieldSyncTransport;
|
|
6
|
+
}
|
|
7
|
+
/** Bind the driver to a workspace. Returns an unregister function. */
|
|
8
|
+
export declare function registerFieldSyncer(syncer: RegisteredFieldSyncer): () => void;
|
|
9
|
+
/**
|
|
10
|
+
* Drain the active workspace's queue once. Resolves to an empty result when no
|
|
11
|
+
* workspace is registered or the device is offline. Never rejects: a failure
|
|
12
|
+
* to reach the service is recorded on the queued item, not thrown.
|
|
13
|
+
*/
|
|
14
|
+
export declare function syncFieldQueue(options?: {
|
|
15
|
+
force?: boolean;
|
|
16
|
+
}): Promise<FieldSyncResult>;
|
|
17
|
+
/**
|
|
18
|
+
* Native shell hook (`bootNativeShell({ runtimeHooks: { syncPendingOperations } })`):
|
|
19
|
+
* drain the field queue when the app returns to the foreground.
|
|
20
|
+
*/
|
|
21
|
+
export declare function syncPendingFieldOperations(): Promise<void>;
|
|
22
|
+
//# sourceMappingURL=fieldSync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fieldSync.d.ts","sourceRoot":"","sources":["../../../src/ecoimpacthub/field/fieldSync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAqB,KAAK,eAAe,EAAE,KAAK,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAGnG,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,kBAAkB,CAAC;CAC/B;AAOD,sEAAsE;AACtE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,IAAI,CAK7E;AAMD;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,eAAe,CAAC,CAyBhG;AAED;;;GAGG;AACH,wBAAsB,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC,CAEhE"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { processFieldQueue as e } from "./syncProcessor.js";
|
|
2
|
+
import { getFieldQueueStore as t } from "./queueStore.js";
|
|
3
|
+
//#region src/ecoimpacthub/field/fieldSync.ts
|
|
4
|
+
var n = null, r = /* @__PURE__ */ new Map(), i = {
|
|
5
|
+
synced: 0,
|
|
6
|
+
retrying: 0,
|
|
7
|
+
failed: 0,
|
|
8
|
+
deferred: 0
|
|
9
|
+
};
|
|
10
|
+
function a(e) {
|
|
11
|
+
return n = e, () => {
|
|
12
|
+
n === e && (n = null);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function o() {
|
|
16
|
+
return typeof navigator < "u" && navigator.onLine === !1;
|
|
17
|
+
}
|
|
18
|
+
async function s(a = {}) {
|
|
19
|
+
let s = n;
|
|
20
|
+
if (!s || o()) return i;
|
|
21
|
+
let c = r.get(s.workspaceId);
|
|
22
|
+
if (c) return c;
|
|
23
|
+
let l = (async () => {
|
|
24
|
+
try {
|
|
25
|
+
return await e({
|
|
26
|
+
store: await t(),
|
|
27
|
+
workspaceId: s.workspaceId,
|
|
28
|
+
transport: s.transport,
|
|
29
|
+
force: a.force
|
|
30
|
+
});
|
|
31
|
+
} catch {
|
|
32
|
+
return i;
|
|
33
|
+
} finally {
|
|
34
|
+
r.delete(s.workspaceId);
|
|
35
|
+
}
|
|
36
|
+
})();
|
|
37
|
+
return r.set(s.workspaceId, l), l;
|
|
38
|
+
}
|
|
39
|
+
async function c() {
|
|
40
|
+
await s();
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { a as registerFieldSyncer, s as syncFieldQueue, c as syncPendingFieldOperations };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=fieldSync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fieldSync.js","names":[],"sources":["../../../src/ecoimpacthub/field/fieldSync.ts"],"sourcesContent":["/**\n * Offline field capture — sync driver.\n * @module ecoimpacthub/field/fieldSync\n *\n * Owns *when* the queue drains; {@link processFieldQueue} owns *how*.\n *\n * While the EcoImpactHub MFE is mounted it registers a syncer bound to the\n * active workspace (the workspace token the gateway sees is the only safe\n * place to send that workspace's observations). The driver drains:\n * - right after an observation is queued,\n * - when the device comes back online,\n * - on a timer while items are waiting for their backoff,\n * - when the native shell resumes the app: the app shell's\n * `runtimeHooks.syncPendingOperations` calls\n * {@link syncPendingFieldOperations}.\n *\n * Runs are serialised per workspace, so a resume and an `online` event that\n * fire together never upload the same photo twice.\n */\n\nimport { processFieldQueue, type FieldSyncResult, type FieldSyncTransport } from './syncProcessor';\nimport { getFieldQueueStore } from './queueStore';\n\nexport interface RegisteredFieldSyncer {\n workspaceId: string;\n transport: FieldSyncTransport;\n}\n\nlet registered: RegisteredFieldSyncer | null = null;\nconst inFlight = new Map<string, Promise<FieldSyncResult>>();\n\nconst EMPTY_RESULT: FieldSyncResult = { synced: 0, retrying: 0, failed: 0, deferred: 0 };\n\n/** Bind the driver to a workspace. Returns an unregister function. */\nexport function registerFieldSyncer(syncer: RegisteredFieldSyncer): () => void {\n registered = syncer;\n return () => {\n if (registered === syncer) registered = null;\n };\n}\n\nfunction isOffline(): boolean {\n return typeof navigator !== 'undefined' && navigator.onLine === false;\n}\n\n/**\n * Drain the active workspace's queue once. Resolves to an empty result when no\n * workspace is registered or the device is offline. Never rejects: a failure\n * to reach the service is recorded on the queued item, not thrown.\n */\nexport async function syncFieldQueue(options: { force?: boolean } = {}): Promise<FieldSyncResult> {\n const syncer = registered;\n if (!syncer || isOffline()) return EMPTY_RESULT;\n\n const running = inFlight.get(syncer.workspaceId);\n if (running) return running;\n\n const run = (async () => {\n try {\n const store = await getFieldQueueStore();\n return await processFieldQueue({\n store,\n workspaceId: syncer.workspaceId,\n transport: syncer.transport,\n force: options.force,\n });\n } catch {\n // Storage itself failed (quota, corrupted DB); the items stay queued.\n return EMPTY_RESULT;\n } finally {\n inFlight.delete(syncer.workspaceId);\n }\n })();\n inFlight.set(syncer.workspaceId, run);\n return run;\n}\n\n/**\n * Native shell hook (`bootNativeShell({ runtimeHooks: { syncPendingOperations } })`):\n * drain the field queue when the app returns to the foreground.\n */\nexport async function syncPendingFieldOperations(): Promise<void> {\n await syncFieldQueue();\n}\n"],"mappings":";;;AA4BA,IAAI,IAA2C,MACzC,oBAAW,IAAI,IAAsC,GAErD,IAAgC;CAAE,QAAQ;CAAG,UAAU;CAAG,QAAQ;CAAG,UAAU;AAAE;AAGvF,SAAgB,EAAoB,GAA2C;CAE7E,OADA,IAAa,SACA;EACX,AAAI,MAAe,MAAQ,IAAa;CAC1C;AACF;AAEA,SAAS,IAAqB;CAC5B,OAAO,OAAO,YAAc,OAAe,UAAU,WAAW;AAClE;AAOA,eAAsB,EAAe,IAA+B,CAAC,GAA6B;CAChG,IAAM,IAAS;CACf,IAAI,CAAC,KAAU,EAAU,GAAG,OAAO;CAEnC,IAAM,IAAU,EAAS,IAAI,EAAO,WAAW;CAC/C,IAAI,GAAS,OAAO;CAEpB,IAAM,KAAO,YAAY;EACvB,IAAI;GAEF,OAAO,MAAM,EAAkB;IAC7B,OAAA,MAFkB,EAAmB;IAGrC,aAAa,EAAO;IACpB,WAAW,EAAO;IAClB,OAAO,EAAQ;GACjB,CAAC;EACH,QAAQ;GAEN,OAAO;EACT,UAAU;GACR,EAAS,OAAO,EAAO,WAAW;EACpC;CACF,GAAG;CAEH,OADA,EAAS,IAAI,EAAO,aAAa,CAAG,GAC7B;AACT;AAMA,eAAsB,IAA4C;CAChE,MAAM,EAAe;AACvB"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline field capture — public surface.
|
|
3
|
+
* @module ecoimpacthub/field
|
|
4
|
+
*/
|
|
5
|
+
export { registerFieldDeviceAdapter, getFieldDeviceAdapter, webFieldDeviceAdapter, type FieldDeviceAdapter, type FieldPosition, } from './device';
|
|
6
|
+
export { syncPendingFieldOperations } from './fieldSync';
|
|
7
|
+
export type { FieldPhoto } from './types';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ecoimpacthub/field/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,0BAA0B,EAC1B,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,kBAAkB,EACvB,KAAK,aAAa,GACnB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AACzD,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { FieldQueueStore } from './types';
|
|
2
|
+
|
|
3
|
+
type ChangeListener = () => void;
|
|
4
|
+
/** Subscribe to queue changes made in this tab. Returns an unsubscribe. */
|
|
5
|
+
export declare function subscribeFieldQueue(listener: ChangeListener): () => void;
|
|
6
|
+
/** In-memory store: the test double and the no-IndexedDB fallback. */
|
|
7
|
+
export declare function createMemoryFieldQueueStore(): FieldQueueStore;
|
|
8
|
+
/** IndexedDB-backed store. Rejects when IndexedDB cannot be opened. */
|
|
9
|
+
export declare function createIndexedDbFieldQueueStore(factory: IDBFactory): Promise<FieldQueueStore>;
|
|
10
|
+
/**
|
|
11
|
+
* The process-wide queue store: IndexedDB when available, otherwise memory.
|
|
12
|
+
* Also asks the browser to keep the origin's storage from being evicted under
|
|
13
|
+
* pressure, since the queue may hold the only copy of a field observation.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getFieldQueueStore(): Promise<FieldQueueStore>;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=queueStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queueStore.d.ts","sourceRoot":"","sources":["../../../src/ecoimpacthub/field/queueStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAuB,eAAe,EAAqB,MAAM,SAAS,CAAC;AAOvF,KAAK,cAAc,GAAG,MAAM,IAAI,CAAC;AAGjC,2EAA2E;AAC3E,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,IAAI,CAKxE;AAgBD,sEAAsE;AACtE,wBAAgB,2BAA2B,IAAI,eAAe,CA0B7D;AA4BD,uEAAuE;AACvE,wBAAsB,8BAA8B,CAClD,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,eAAe,CAAC,CAuD1B;AAID;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,OAAO,CAAC,eAAe,CAAC,CAmB7D"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//#region src/ecoimpacthub/field/queueStore.ts
|
|
2
|
+
var e = "ecoimpacthub-field", t = 1, n = "observations", r = "lookups", i = /* @__PURE__ */ new Set();
|
|
3
|
+
function a(e) {
|
|
4
|
+
return i.add(e), () => {
|
|
5
|
+
i.delete(e);
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
function o() {
|
|
9
|
+
for (let e of i) try {
|
|
10
|
+
e();
|
|
11
|
+
} catch {}
|
|
12
|
+
}
|
|
13
|
+
function s(e) {
|
|
14
|
+
return [...e].sort((e, t) => e.createdAt - t.createdAt);
|
|
15
|
+
}
|
|
16
|
+
function c() {
|
|
17
|
+
let e = /* @__PURE__ */ new Map(), t = /* @__PURE__ */ new Map();
|
|
18
|
+
return {
|
|
19
|
+
persistent: !1,
|
|
20
|
+
async list(t) {
|
|
21
|
+
return s([...e.values()].filter((e) => e.workspaceId === t));
|
|
22
|
+
},
|
|
23
|
+
async get(t) {
|
|
24
|
+
return e.get(t) ?? null;
|
|
25
|
+
},
|
|
26
|
+
async put(t) {
|
|
27
|
+
e.set(t.clientRef, t), o();
|
|
28
|
+
},
|
|
29
|
+
async remove(t) {
|
|
30
|
+
e.delete(t), o();
|
|
31
|
+
},
|
|
32
|
+
async saveLookups(e) {
|
|
33
|
+
t.set(e.workspaceId, e);
|
|
34
|
+
},
|
|
35
|
+
async loadLookups(e) {
|
|
36
|
+
return t.get(e) ?? null;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function l(e) {
|
|
41
|
+
return new Promise((t, n) => {
|
|
42
|
+
e.onsuccess = () => t(e.result), e.onerror = () => n(e.error ?? /* @__PURE__ */ Error("IndexedDB request failed"));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function u(i) {
|
|
46
|
+
return new Promise((a, o) => {
|
|
47
|
+
let s = i.open(e, t);
|
|
48
|
+
s.onupgradeneeded = () => {
|
|
49
|
+
let e = s.result;
|
|
50
|
+
e.objectStoreNames.contains(n) || e.createObjectStore(n, { keyPath: "clientRef" }).createIndex("workspaceId", "workspaceId", { unique: !1 }), e.objectStoreNames.contains(r) || e.createObjectStore(r, { keyPath: "workspaceId" });
|
|
51
|
+
}, s.onsuccess = () => a(s.result), s.onerror = () => o(s.error ?? /* @__PURE__ */ Error("Could not open IndexedDB")), s.onblocked = () => o(/* @__PURE__ */ Error("IndexedDB upgrade is blocked by another tab"));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async function d(e) {
|
|
55
|
+
let t = await u(e), i = async (e, n, r) => {
|
|
56
|
+
let i = t.transaction(e, n), a = new Promise((e, t) => {
|
|
57
|
+
i.oncomplete = () => e(), i.onerror = () => t(i.error ?? /* @__PURE__ */ Error("IndexedDB transaction failed")), i.onabort = () => t(i.error ?? /* @__PURE__ */ Error("IndexedDB transaction aborted"));
|
|
58
|
+
}), [o] = await Promise.all([l(r(i.objectStore(e))), a]);
|
|
59
|
+
return o;
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
persistent: !0,
|
|
63
|
+
async list(e) {
|
|
64
|
+
return s(await i(n, "readonly", (t) => t.index("workspaceId").getAll(e)));
|
|
65
|
+
},
|
|
66
|
+
async get(e) {
|
|
67
|
+
return await i(n, "readonly", (t) => t.get(e)) ?? null;
|
|
68
|
+
},
|
|
69
|
+
async put(e) {
|
|
70
|
+
await i(n, "readwrite", (t) => t.put(e)), o();
|
|
71
|
+
},
|
|
72
|
+
async remove(e) {
|
|
73
|
+
await i(n, "readwrite", (t) => t.delete(e)), o();
|
|
74
|
+
},
|
|
75
|
+
async saveLookups(e) {
|
|
76
|
+
await i(r, "readwrite", (t) => t.put(e));
|
|
77
|
+
},
|
|
78
|
+
async loadLookups(e) {
|
|
79
|
+
return await i(r, "readonly", (t) => t.get(e)) ?? null;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
var f = null;
|
|
84
|
+
function p() {
|
|
85
|
+
return f ||= (async () => {
|
|
86
|
+
let e = typeof indexedDB > "u" ? null : indexedDB;
|
|
87
|
+
if (!e) return c();
|
|
88
|
+
try {
|
|
89
|
+
let t = await d(e);
|
|
90
|
+
try {
|
|
91
|
+
await navigator.storage?.persist?.();
|
|
92
|
+
} catch {}
|
|
93
|
+
return t;
|
|
94
|
+
} catch {
|
|
95
|
+
return c();
|
|
96
|
+
}
|
|
97
|
+
})(), f;
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
export { p as getFieldQueueStore, a as subscribeFieldQueue };
|
|
101
|
+
|
|
102
|
+
//# sourceMappingURL=queueStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queueStore.js","names":[],"sources":["../../../src/ecoimpacthub/field/queueStore.ts"],"sourcesContent":["/**\n * Offline field capture — queue persistence.\n * @module ecoimpacthub/field/queueStore\n *\n * IndexedDB is the store on every runtime the field app ships to: the\n * Capacitor iOS/Android WebView, Electron and the browser. It keeps the queued\n * observation and its evidence photo (as a Blob) across app restarts and\n * offline periods. The database is origin-scoped, and the Capacitor shells\n * deliberately keep the default `localhost` origin, so an app update never\n * orphans the queue.\n *\n * When IndexedDB is unavailable (some private-browsing modes) the queue falls\n * back to memory and reports `persistent: false`, so the capture screen can\n * warn that unsynced observations will not survive closing the app.\n */\n\nimport type { FieldLookupSnapshot, FieldQueueStore, QueuedObservation } from './types';\n\nconst DB_NAME = 'ecoimpacthub-field';\nconst DB_VERSION = 1;\nconst OBSERVATIONS = 'observations';\nconst LOOKUPS = 'lookups';\n\ntype ChangeListener = () => void;\nconst listeners = new Set<ChangeListener>();\n\n/** Subscribe to queue changes made in this tab. Returns an unsubscribe. */\nexport function subscribeFieldQueue(listener: ChangeListener): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\nfunction notifyChange(): void {\n for (const listener of listeners) {\n try {\n listener();\n } catch {\n // A broken subscriber must not stop the others from refreshing.\n }\n }\n}\n\nfunction sortOldestFirst(items: QueuedObservation[]): QueuedObservation[] {\n return [...items].sort((a, b) => a.createdAt - b.createdAt);\n}\n\n/** In-memory store: the test double and the no-IndexedDB fallback. */\nexport function createMemoryFieldQueueStore(): FieldQueueStore {\n const items = new Map<string, QueuedObservation>();\n const lookups = new Map<string, FieldLookupSnapshot>();\n return {\n persistent: false,\n async list(workspaceId) {\n return sortOldestFirst([...items.values()].filter((i) => i.workspaceId === workspaceId));\n },\n async get(clientRef) {\n return items.get(clientRef) ?? null;\n },\n async put(item) {\n items.set(item.clientRef, item);\n notifyChange();\n },\n async remove(clientRef) {\n items.delete(clientRef);\n notifyChange();\n },\n async saveLookups(snapshot) {\n lookups.set(snapshot.workspaceId, snapshot);\n },\n async loadLookups(workspaceId) {\n return lookups.get(workspaceId) ?? null;\n },\n };\n}\n\nfunction requestToPromise<T>(request: IDBRequest<T>): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error ?? new Error('IndexedDB request failed'));\n });\n}\n\nfunction openDatabase(factory: IDBFactory): Promise<IDBDatabase> {\n return new Promise<IDBDatabase>((resolve, reject) => {\n const request = factory.open(DB_NAME, DB_VERSION);\n request.onupgradeneeded = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(OBSERVATIONS)) {\n const store = db.createObjectStore(OBSERVATIONS, { keyPath: 'clientRef' });\n store.createIndex('workspaceId', 'workspaceId', { unique: false });\n }\n if (!db.objectStoreNames.contains(LOOKUPS)) {\n db.createObjectStore(LOOKUPS, { keyPath: 'workspaceId' });\n }\n };\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error ?? new Error('Could not open IndexedDB'));\n request.onblocked = () => reject(new Error('IndexedDB upgrade is blocked by another tab'));\n });\n}\n\n/** IndexedDB-backed store. Rejects when IndexedDB cannot be opened. */\nexport async function createIndexedDbFieldQueueStore(\n factory: IDBFactory\n): Promise<FieldQueueStore> {\n const db = await openDatabase(factory);\n\n const run = async <T>(\n storeName: string,\n mode: IDBTransactionMode,\n action: (store: IDBObjectStore) => IDBRequest<T>\n ): Promise<T> => {\n const tx = db.transaction(storeName, mode);\n // Listen for completion before issuing the request: a write is only\n // durable once the transaction commits, not when the request succeeds.\n const committed = new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error ?? new Error('IndexedDB transaction failed'));\n tx.onabort = () => reject(tx.error ?? new Error('IndexedDB transaction aborted'));\n });\n const [result] = await Promise.all([\n requestToPromise(action(tx.objectStore(storeName))),\n committed,\n ]);\n return result;\n };\n\n return {\n persistent: true,\n async list(workspaceId) {\n const rows = await run<QueuedObservation[]>(OBSERVATIONS, 'readonly', (store) =>\n store.index('workspaceId').getAll(workspaceId)\n );\n return sortOldestFirst(rows);\n },\n async get(clientRef) {\n const row = await run<QueuedObservation | undefined>(OBSERVATIONS, 'readonly', (store) =>\n store.get(clientRef)\n );\n return row ?? null;\n },\n async put(item) {\n await run(OBSERVATIONS, 'readwrite', (store) => store.put(item));\n notifyChange();\n },\n async remove(clientRef) {\n await run(OBSERVATIONS, 'readwrite', (store) => store.delete(clientRef));\n notifyChange();\n },\n async saveLookups(snapshot) {\n await run(LOOKUPS, 'readwrite', (store) => store.put(snapshot));\n },\n async loadLookups(workspaceId) {\n const row = await run<FieldLookupSnapshot | undefined>(LOOKUPS, 'readonly', (store) =>\n store.get(workspaceId)\n );\n return row ?? null;\n },\n };\n}\n\nlet storePromise: Promise<FieldQueueStore> | null = null;\n\n/**\n * The process-wide queue store: IndexedDB when available, otherwise memory.\n * Also asks the browser to keep the origin's storage from being evicted under\n * pressure, since the queue may hold the only copy of a field observation.\n */\nexport function getFieldQueueStore(): Promise<FieldQueueStore> {\n if (!storePromise) {\n storePromise = (async () => {\n const factory = typeof indexedDB === 'undefined' ? null : indexedDB;\n if (!factory) return createMemoryFieldQueueStore();\n try {\n const store = await createIndexedDbFieldQueueStore(factory);\n try {\n await navigator.storage?.persist?.();\n } catch {\n // Best effort: persistence is a hint, the queue works without it.\n }\n return store;\n } catch {\n return createMemoryFieldQueueStore();\n }\n })();\n }\n return storePromise;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { EcoRecordObservationInput } from '../types';
|
|
2
|
+
import { FieldPhoto, FieldQueueStore, QueuedObservation } from './types';
|
|
3
|
+
|
|
4
|
+
/** First retry delay; doubles per attempt up to {@link MAX_RETRY_DELAY_MS}. */
|
|
5
|
+
export declare const BASE_RETRY_DELAY_MS = 15000;
|
|
6
|
+
export declare const MAX_RETRY_DELAY_MS: number;
|
|
7
|
+
/**
|
|
8
|
+
* Transient failures tolerated while the device reports itself online before
|
|
9
|
+
* an item is parked as `failed` for the observer to look at (about four hours
|
|
10
|
+
* of attempts at the capped delay). Offline periods do not count: the sync
|
|
11
|
+
* driver does not attempt a drain while the device is offline.
|
|
12
|
+
*/
|
|
13
|
+
export declare const MAX_TRANSIENT_ATTEMPTS = 25;
|
|
14
|
+
export interface FieldSyncTransport {
|
|
15
|
+
/** Upload evidence and return the platform file id. */
|
|
16
|
+
uploadEvidence(photo: FieldPhoto): Promise<string>;
|
|
17
|
+
/** Record the observation; resolves to the created (or replayed) row id. */
|
|
18
|
+
recordObservation(input: EcoRecordObservationInput & {
|
|
19
|
+
clientRef: string;
|
|
20
|
+
}): Promise<string>;
|
|
21
|
+
}
|
|
22
|
+
export interface FieldSyncResult {
|
|
23
|
+
synced: number;
|
|
24
|
+
/** Items that hit a transient failure and are scheduled for a retry. */
|
|
25
|
+
retrying: number;
|
|
26
|
+
/** Items the service rejected permanently during this run. */
|
|
27
|
+
failed: number;
|
|
28
|
+
/** Items left untouched because their backoff has not elapsed. */
|
|
29
|
+
deferred: number;
|
|
30
|
+
}
|
|
31
|
+
export declare function isPermanentSyncError(err: unknown): boolean;
|
|
32
|
+
export declare function retryDelayMs(attempts: number): number;
|
|
33
|
+
export declare function toRecordInput(item: QueuedObservation, evidenceFileId: string | null): EcoRecordObservationInput & {
|
|
34
|
+
clientRef: string;
|
|
35
|
+
};
|
|
36
|
+
export interface ProcessFieldQueueOptions {
|
|
37
|
+
store: FieldQueueStore;
|
|
38
|
+
workspaceId: string;
|
|
39
|
+
transport: FieldSyncTransport;
|
|
40
|
+
/** Clock, injectable for tests. */
|
|
41
|
+
now?: () => number;
|
|
42
|
+
/** Ignore backoff (an explicit "Sync now" from the observer). */
|
|
43
|
+
force?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare function processFieldQueue({ store, workspaceId, transport, now, force, }: ProcessFieldQueueOptions): Promise<FieldSyncResult>;
|
|
46
|
+
//# sourceMappingURL=syncProcessor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"syncProcessor.d.ts","sourceRoot":"","sources":["../../../src/ecoimpacthub/field/syncProcessor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAC;AAC1D,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAE9E,+EAA+E;AAC/E,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAC1C,eAAO,MAAM,kBAAkB,QAAc,CAAC;AAC9C;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAYzC,MAAM,WAAW,kBAAkB;IACjC,uDAAuD;IACvD,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnD,4EAA4E;IAC5E,iBAAiB,CAAC,KAAK,EAAE,yBAAyB,GAAG;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC9F;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,QAAQ,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAG1D;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGrD;AAOD,wBAAgB,aAAa,CAC3B,IAAI,EAAE,iBAAiB,EACvB,cAAc,EAAE,MAAM,GAAG,IAAI,GAC5B,yBAAyB,GAAG;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,CAcnD;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,eAAe,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,kBAAkB,CAAC;IAC9B,mCAAmC;IACnC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,iEAAiE;IACjE,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,wBAAsB,iBAAiB,CAAC,EACtC,KAAK,EACL,WAAW,EACX,SAAS,EACT,GAAc,EACd,KAAa,GACd,EAAE,wBAAwB,GAAG,OAAO,CAAC,eAAe,CAAC,CAmDrD"}
|