@oneteam-app/apps-cli 0.1.0-alpha.5 → 0.1.0-alpha.7

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oneteam-app/apps-cli",
3
- "version": "0.1.0-alpha.5",
4
- "description": "Set up and check Oneteam login in an app: init, validate, doctor.",
3
+ "version": "0.1.0-alpha.7",
4
+ "description": "Set up, check and deploy apps for Oneteam: init, validate, doctor, login, deploy, status, logs.",
5
5
  "keywords": [
6
6
  "oneteam",
7
7
  "cli",
@@ -27,7 +27,8 @@
27
27
  "CHANGELOG.md",
28
28
  "LICENSE",
29
29
  "README.md",
30
- "dist"
30
+ "dist",
31
+ "templates"
31
32
  ],
32
33
  "engines": {
33
34
  "node": ">=20.9"
@@ -37,11 +38,12 @@
37
38
  "provenance": false
38
39
  },
39
40
  "dependencies": {
40
- "@oneteam-app/apps": "0.1.0-alpha.5"
41
+ "@oneteam-app/apps": "0.1.0-alpha.7",
42
+ "esbuild": "^0.28.1"
41
43
  },
42
44
  "scripts": {
43
- "build": "tsup",
44
- "test": "vitest run",
45
+ "build": "node scripts/copy-template.mjs && tsup",
46
+ "test": "node scripts/copy-template.mjs && vitest run",
45
47
  "typecheck": "tsc --noEmit"
46
48
  }
47
49
  }
@@ -0,0 +1,11 @@
1
+ # Local values for the secrets listed under "secrets" in oneteam.app.json.
2
+ #
3
+ # Copy this file to .dev.vars and fill it in yourself. .dev.vars is git-ignored
4
+ # and never uploaded: the deployed app gets its values from a person who enters
5
+ # them in the dashboard (the app's Secrets tab) or with
6
+ # npx oneteam-apps secret set NAME --env preview
7
+ # npx oneteam-apps secret set NAME --env live
8
+ #
9
+ # Use a test key here, never the live one. Names only in this file:
10
+ #
11
+ # SUPABASE_SERVICE_KEY=
@@ -0,0 +1,119 @@
1
+ # Lost and found (hosted app template)
2
+
3
+ A small, complete hosted app: a React front end, an API under `/api`, and its
4
+ own database. People are signed in before a request reaches the app, so there
5
+ is no login code here. `oneteam-apps init --hosted <name>` copies this folder.
6
+
7
+ ```bash
8
+ pnpm install
9
+ pnpm dev # http://localhost:5310, with a "View as" switch instead of sign-on
10
+ pnpm test # the permission tests
11
+ pnpm run deploy # review, then a preview address
12
+ ```
13
+
14
+ | | |
15
+ |---|---|
16
+ | `src/worker/index.ts` | The API (Hono). Every route has `c.get('session')`; writes check a permission with `assertCan` |
17
+ | `src/worker/items.ts` | All database access: `env.DB` only, placeholders only, always limited to the user's communities |
18
+ | `src/worker/index.test.ts` | Who can see and do what, with fake users |
19
+ | `src/worker/jobs.ts` | Jobs the platform runs on a schedule. Empty to start with |
20
+ | `src/worker/dev.ts` | Local stand-in for sign-on. Not part of what is deployed |
21
+ | `src/client/` | The front end, built from the components of `@oneteam-app/ui`. Calls the API through `apiJson()`, never `fetch('/api/…')` |
22
+ | `src/client/styles.css` | Two imports: Tailwind and the theme. No colours of your own |
23
+ | `migrations/` | The database schema, applied in order |
24
+ | `oneteam.app.json` | Name, address, what personal data is stored, who should see it, which secrets it needs |
25
+ | `.dev.vars.example` | The names of the secrets, for whoever runs the app locally. `.dev.vars` itself is never committed |
26
+
27
+ The app does not live at `/`: it is served at `/<organisation>/<app>/`, and the
28
+ same build at `/<organisation>/<app>--preview/`. That is why Vite builds with
29
+ `base: './'`, why `index.html` has a `<base>` tag, and why the front end uses
30
+ `apiJson()` and `basePath()` from `@oneteam-app/apps/hosted/react`.
31
+
32
+ ## How it looks
33
+
34
+ Screens are built from `@oneteam-app/ui` (`Page`, `List`, `Button`, `Field`, …;
35
+ the list is in `node_modules/@oneteam-app/ui/docs/components.md`). The app is
36
+ wrapped once in `<OneteamTheme theme={themeOf(me)}>`, which applies the
37
+ organisation's colour and logo and follows the device's light or dark mode.
38
+ Tailwind is the one way to style: layout classes in the markup, written for a
39
+ phone first (`grid gap-6 md:grid-cols-2` adds the second column on a wide
40
+ screen), and the theme's names (`bg-card`, `text-muted-foreground`) where a
41
+ class needs a colour. There are no colours and no other `.css` files in this app.
42
+
43
+ `pnpm dev` has a "Button colour" box in the bar at the top: type a hex colour to see the
44
+ app as an organisation with that colour would. `oneteam-apps doctor` warns about
45
+ colours written out and hand-made buttons, inputs and dialogs.
46
+
47
+ ## Secrets
48
+
49
+ The app has its own database and needs no key for it. When it calls another
50
+ service (Supabase, a mail provider), the key for that service is a secret: the
51
+ code and `oneteam.app.json` only name it, and a person enters the value.
52
+
53
+ 1. List the name, and the host it is sent to, in `oneteam.app.json`:
54
+ ```json
55
+ "outbound_hosts": ["xyz.supabase.co"],
56
+ "secrets": [{ "name": "SUPABASE_SERVICE_KEY", "host": "xyz.supabase.co" }]
57
+ ```
58
+ 2. Add it to `Env` in `src/worker/index.ts` and read it in the Worker only:
59
+ ```ts
60
+ import { secret } from '@oneteam-app/apps/hosted';
61
+
62
+ const key = secret(c.env, 'SUPABASE_SERVICE_KEY');
63
+ ```
64
+ It throws a clear error when the value is missing. Never return the value
65
+ from a route, log it, or give it a `VITE_` name: everything in `src/client/`
66
+ is downloaded by the browser.
67
+ 3. Locally: copy `.dev.vars.example` to `.dev.vars` and fill in a test key yourself.
68
+ 4. Deployed: a person enters the value once per environment, in the dashboard
69
+ (the app's Secrets tab) or at a hidden prompt:
70
+ ```bash
71
+ npx oneteam-apps secret set SUPABASE_SERVICE_KEY --env preview
72
+ npx oneteam-apps secret set SUPABASE_SERVICE_KEY --env live
73
+ npx oneteam-apps secret list
74
+ ```
75
+ Preview and live each have their own value. After saving, nobody can read it
76
+ back, and a version cannot go live while one of its secrets has no live value.
77
+
78
+ `oneteam-apps deploy` and `oneteam-apps doctor` stop when an env file is not
79
+ git-ignored or a key is written into the code.
80
+
81
+ ## Scheduled jobs and sending
82
+
83
+ A job is a function the platform calls on a schedule. Sending is a permission
84
+ an admin approves. Neither needs a key, a cron service or anything outside the app.
85
+
86
+ 1. Say when it runs, and what the app sends, in `oneteam.app.json`:
87
+ ```json
88
+ "schedules": [{ "name": "send-reminders", "every": "day", "at": "07:00" }],
89
+ "sends": ["notifications"]
90
+ ```
91
+ `every` is `15_minutes`, `30_minutes`, `hour` (with `minute`), `day`, `week`
92
+ (with `weekdays`) or `month` (with `day_of_month`, 1 to 28). Not cron syntax.
93
+ 2. Write the handler under the same name in `src/worker/jobs.ts`:
94
+ ```ts
95
+ export const jobs = defineJobs<Env>({
96
+ 'send-reminders': async (job, env) => {
97
+ const waiting = await itemsWaitingLongerThan(env.DB, 7);
98
+
99
+ await notify(job, { userIds: waiting.map((item) => item.foundBy), title: 'Still waiting', body: 'An item you logged is still here.' });
100
+ },
101
+ });
102
+ ```
103
+ Nobody is signed in during a job: there is no session, so take who it is
104
+ for from the app's own data. From a route, send for the person's request:
105
+ `await notify(c.req.raw, { … })`.
106
+ 3. Try it: `pnpm dev`, then "Run job" in the bar at the top. What the app sends
107
+ is printed in the terminal.
108
+ 4. `oneteam-apps deploy` checks that every schedule has a handler and the other
109
+ way round, and says what an admin has to approve: a new job, one that runs
110
+ more often, anything new under `sends`. Then:
111
+ ```bash
112
+ npx oneteam-apps job run send-reminders --env preview
113
+ npx oneteam-apps job list
114
+ ```
115
+
116
+ A job has 30 seconds. A run that was missed is skipped, not caught up, and a
117
+ run can be started by hand, so a job has to be safe to run twice. People
118
+ outside the app's audience do not get notifications (they come back under
119
+ `skipped`), and nobody gets more than five a day from one app.
@@ -0,0 +1,11 @@
1
+ node_modules
2
+ dist
3
+ .wrangler
4
+ *.tsbuildinfo
5
+
6
+ # Local values of secrets. Never commit these: `oneteam-apps deploy` refuses to
7
+ # run while one of them is not ignored.
8
+ .dev.vars*
9
+ .env*
10
+ !.dev.vars.example
11
+ !.env.example
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <!-- Where the app is mounted. Locally that is the root; in production the
5
+ gateway replaces this with /<organisation>/<app>/. -->
6
+ <base href="/" />
7
+ <meta charset="utf-8" />
8
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
9
+ <meta name="theme-color" content="#ffffff" />
10
+ <title>Lost and found</title>
11
+ </head>
12
+ <body>
13
+ <div id="root"></div>
14
+ <script type="module" src="/src/client/main.tsx"></script>
15
+ </body>
16
+ </html>
@@ -0,0 +1,16 @@
1
+ -- One row per found item. Every query filters on community_id: people only
2
+ -- see and change items of the communities (stores) they belong to.
3
+ CREATE TABLE items (
4
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
5
+ community_id TEXT NOT NULL,
6
+ title TEXT NOT NULL,
7
+ location TEXT NOT NULL DEFAULT '',
8
+ found_by_user_id TEXT NOT NULL,
9
+ found_by_name TEXT NOT NULL,
10
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
11
+ returned_at TEXT,
12
+ returned_to TEXT,
13
+ returned_by_user_id TEXT
14
+ );
15
+
16
+ CREATE INDEX items_by_community ON items (community_id, created_at DESC);
@@ -0,0 +1,17 @@
1
+ {
2
+ "$schema": "https://oneteam.io/schemas/app.v2.json",
3
+ "schema_version": 2,
4
+ "hosting": "hosted",
5
+ "name": "Lost and found",
6
+ "description": "Log items found in the store and mark them as returned",
7
+ "slug": "lost-and-found",
8
+ "summary": "First version: log found items, list them per store, mark them as returned.",
9
+ "personal_data": [
10
+ "Name of the employee who logged an item",
11
+ "Name of the person an item was returned to"
12
+ ],
13
+ "outbound_hosts": [],
14
+ "suggested_audience": {
15
+ "description": "Everyone who works in a store"
16
+ }
17
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "hosted-app",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "wrangler d1 migrations apply DB --local && vite dev",
7
+ "build": "vite build",
8
+ "test": "vitest run",
9
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.worker.json --noEmit",
10
+ "deploy": "oneteam-apps deploy"
11
+ },
12
+ "dependencies": {
13
+ "@oneteam-app/apps": "workspace:*",
14
+ "@oneteam-app/ui": "workspace:*",
15
+ "hono": "^4.13.0",
16
+ "react": "^19.1.0",
17
+ "react-dom": "^19.1.0"
18
+ },
19
+ "devDependencies": {
20
+ "@cloudflare/vite-plugin": "^1.64.0",
21
+ "@cloudflare/vitest-plugin": "^1.7.0",
22
+ "@cloudflare/workers-types": "^5.20261110.1",
23
+ "@oneteam-app/apps-cli": "workspace:*",
24
+ "@tailwindcss/vite": "^4.3.5",
25
+ "@types/react": "^19.1.0",
26
+ "@types/react-dom": "^19.1.0",
27
+ "@vitejs/plugin-react": "^6.1.0",
28
+ "tailwindcss": "^4.3.5",
29
+ "typescript": "^5.9.0",
30
+ "vite": "^8.3.0",
31
+ "vitest": "^4.1.11",
32
+ "wrangler": "^4.160.0"
33
+ }
34
+ }
@@ -0,0 +1,276 @@
1
+ import { apiJson, themeOf, useMe } from '@oneteam-app/apps/hosted/react';
2
+ import {
3
+ ActionBar,
4
+ Badge,
5
+ Button,
6
+ Card,
7
+ EmptyState,
8
+ ErrorState,
9
+ Field,
10
+ List,
11
+ ListItem,
12
+ ListSkeleton,
13
+ OneteamTheme,
14
+ Page,
15
+ Person,
16
+ Section,
17
+ Select,
18
+ Tabs,
19
+ TextInput,
20
+ toast,
21
+ } from '@oneteam-app/ui';
22
+ import { useCallback, useEffect, useState, type FormEvent } from 'react';
23
+ import type { Item, ItemsResponse } from '../shared';
24
+
25
+ type Screen = { name: 'list' } | { name: 'add' } | { name: 'item'; item: Item } | { name: 'return'; item: Item };
26
+
27
+ function formatDate(value: string): string {
28
+ return new Date(value).toLocaleDateString(undefined, { day: 'numeric', month: 'short' });
29
+ }
30
+
31
+ export function App() {
32
+ const { me, status } = useMe();
33
+ const [data, setData] = useState<ItemsResponse | null>(null);
34
+ const [screen, setScreen] = useState<Screen>({ name: 'list' });
35
+ const [failed, setFailed] = useState(false);
36
+
37
+ const load = useCallback(async () => {
38
+ try {
39
+ // Always through the helper: it knows the address the app is mounted at.
40
+ setData(await apiJson<ItemsResponse>('/api/items'));
41
+ setFailed(false);
42
+ } catch {
43
+ setFailed(true);
44
+ }
45
+ }, []);
46
+
47
+ useEffect(() => void load(), [load]);
48
+
49
+ const toList = () => setScreen({ name: 'list' });
50
+ const saved = (message: string) => {
51
+ toast.success(message);
52
+ toList();
53
+ void load();
54
+ };
55
+
56
+ return (
57
+ // The organisation's theme (its colours and logo), when it has set any; neutral otherwise.
58
+ <OneteamTheme theme={themeOf(me)}>
59
+ {status === 'error' || failed ? (
60
+ <Page title="Lost and found">
61
+ <ErrorState description="The items could not be loaded." onRetry={() => (status === 'error' ? location.reload() : void load())} />
62
+ </Page>
63
+ ) : screen.name === 'add' && data ? (
64
+ <AddItem communities={data.communities} onSaved={() => saved('Item logged')} onBack={toList} />
65
+ ) : screen.name === 'item' ? (
66
+ <ItemDetail
67
+ item={screen.item}
68
+ storeName={data?.communities.find((community) => community.id === screen.item.communityId)?.name ?? ''}
69
+ onReturn={() => setScreen({ name: 'return', item: screen.item })}
70
+ onBack={toList}
71
+ />
72
+ ) : screen.name === 'return' ? (
73
+ <ReturnItem item={screen.item} onSaved={() => saved('Marked as returned')} onBack={() => setScreen({ name: 'item', item: screen.item })} />
74
+ ) : (
75
+ <ItemList data={data} onAdd={() => setScreen({ name: 'add' })} onOpen={(item) => setScreen({ name: 'item', item })} />
76
+ )}
77
+ </OneteamTheme>
78
+ );
79
+ }
80
+
81
+ function ItemList({ data, onAdd, onOpen }: { data: ItemsResponse | null; onAdd: () => void; onOpen: (item: Item) => void }) {
82
+ const open = data?.items.filter((item) => !item.returnedAt) ?? [];
83
+ const returned = data?.items.filter((item) => item.returnedAt) ?? [];
84
+ const storeName = (id: string) => data?.communities.find((community) => community.id === id)?.name ?? '';
85
+
86
+ return (
87
+ <Page
88
+ title="Lost and found"
89
+ showBrand
90
+ footer={
91
+ // One main action per screen, where the thumb reaches it.
92
+ <ActionBar>
93
+ <Button onClick={onAdd} disabled={!data}>
94
+ Log a found item
95
+ </Button>
96
+ </ActionBar>
97
+ }
98
+ >
99
+ {!data ? (
100
+ <ListSkeleton />
101
+ ) : (
102
+ <Tabs
103
+ tabs={[
104
+ {
105
+ value: 'open',
106
+ label: `Waiting (${open.length})`,
107
+ content:
108
+ open.length === 0 ? (
109
+ <EmptyState title="Nothing waiting to be picked up" description="Items you log show up here." />
110
+ ) : (
111
+ <List>
112
+ {open.map((item) => (
113
+ <ListItem
114
+ key={item.id}
115
+ title={item.title}
116
+ description={[item.location, storeName(item.communityId), formatDate(item.createdAt)].filter(Boolean).join(' · ')}
117
+ onClick={() => onOpen(item)}
118
+ />
119
+ ))}
120
+ </List>
121
+ ),
122
+ },
123
+ {
124
+ value: 'returned',
125
+ label: `Returned (${returned.length})`,
126
+ content:
127
+ returned.length === 0 ? (
128
+ <EmptyState title="Nothing returned yet" />
129
+ ) : (
130
+ <List>
131
+ {returned.map((item) => (
132
+ <ListItem
133
+ key={item.id}
134
+ title={item.title}
135
+ description={`To ${item.returnedTo} · ${formatDate(item.returnedAt!)}`}
136
+ trailing={<Badge tone="success">Returned</Badge>}
137
+ onClick={() => onOpen(item)}
138
+ />
139
+ ))}
140
+ </List>
141
+ ),
142
+ },
143
+ ]}
144
+ />
145
+ )}
146
+ </Page>
147
+ );
148
+ }
149
+
150
+ /** One item. A single column on a phone; what was found and what happened to it side by side on a wide screen. */
151
+ function ItemDetail({ item, storeName, onReturn, onBack }: { item: Item; storeName: string; onReturn: () => void; onBack: () => void }) {
152
+ return (
153
+ <Page
154
+ title={item.title}
155
+ description={storeName}
156
+ back={onBack}
157
+ width="wide"
158
+ // Only for people who may hand items back; the API checks again.
159
+ footer={
160
+ item.canReturn && !item.returnedAt ? (
161
+ <ActionBar>
162
+ <Button onClick={onReturn}>Hand back</Button>
163
+ </ActionBar>
164
+ ) : undefined
165
+ }
166
+ >
167
+ <div className="grid gap-6 md:grid-cols-2">
168
+ <Section title="Found">
169
+ <Card>
170
+ <dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-2">
171
+ <dt className="text-muted-foreground">Where</dt>
172
+ <dd>{item.location || 'Not noted'}</dd>
173
+ <dt className="text-muted-foreground">Store</dt>
174
+ <dd>{storeName}</dd>
175
+ <dt className="text-muted-foreground">When</dt>
176
+ <dd>{formatDate(item.createdAt)}</dd>
177
+ </dl>
178
+ </Card>
179
+ <Person name={item.foundByName} detail="Logged this item" />
180
+ </Section>
181
+ <Section title="Status">
182
+ <Card
183
+ title={item.returnedAt ? <Badge tone="success">Returned</Badge> : <Badge tone="warning">Waiting to be picked up</Badge>}
184
+ description={item.returnedAt ? `To ${item.returnedTo} on ${formatDate(item.returnedAt)}` : 'Kept in the store until someone asks for it.'}
185
+ />
186
+ </Section>
187
+ </div>
188
+ </Page>
189
+ );
190
+ }
191
+
192
+ /** Sends a form once, and turns what can go wrong into words for the person who filled it in. */
193
+ function useSubmit(send: () => Promise<unknown>, onSaved: () => void) {
194
+ const [busy, setBusy] = useState(false);
195
+ const [error, setError] = useState<string | null>(null);
196
+
197
+ const submit = async (event: FormEvent) => {
198
+ event.preventDefault();
199
+ setBusy(true);
200
+ setError(null);
201
+
202
+ try {
203
+ await send();
204
+ onSaved();
205
+ } catch (caught) {
206
+ setError((caught as { status?: number }).status === 403 ? "You're not allowed to do this." : (caught as Error).message);
207
+ setBusy(false);
208
+ }
209
+ };
210
+
211
+ return { busy, error, submit };
212
+ }
213
+
214
+ function AddItem({ communities, onSaved, onBack }: { communities: ItemsResponse['communities']; onSaved: () => void; onBack: () => void }) {
215
+ const [title, setTitle] = useState('');
216
+ const [location, setLocation] = useState('');
217
+ const [communityId, setCommunityId] = useState(communities[0]?.id ?? '');
218
+ const { busy, error, submit } = useSubmit(
219
+ () => apiJson('/api/items', { method: 'POST', body: { title, location, communityId: communityId || undefined } }),
220
+ onSaved,
221
+ );
222
+
223
+ return (
224
+ <form onSubmit={submit}>
225
+ <Page
226
+ title="Log a found item"
227
+ back={onBack}
228
+ footer={
229
+ <ActionBar>
230
+ <Button type="submit" loading={busy}>
231
+ Save
232
+ </Button>
233
+ </ActionBar>
234
+ }
235
+ >
236
+ <Field label="What was found?" error={error} required>
237
+ <TextInput value={title} onChange={(event) => setTitle(event.target.value)} maxLength={120} autoFocus placeholder="Blue umbrella" />
238
+ </Field>
239
+ <Field label="Where?">
240
+ <TextInput value={location} onChange={(event) => setLocation(event.target.value)} maxLength={120} placeholder="Fitting rooms" />
241
+ </Field>
242
+ {communities.length > 1 && (
243
+ <Field label="Store">
244
+ <Select value={communityId} onChange={(event) => setCommunityId(event.target.value)} options={communities.map(({ id, name }) => ({ value: id, label: name }))} />
245
+ </Field>
246
+ )}
247
+ </Page>
248
+ </form>
249
+ );
250
+ }
251
+
252
+ function ReturnItem({ item, onSaved, onBack }: { item: Item; onSaved: () => void; onBack: () => void }) {
253
+ const [returnedTo, setReturnedTo] = useState('');
254
+ const { busy, error, submit } = useSubmit(() => apiJson(`/api/items/${item.id}/return`, { method: 'POST', body: { returnedTo } }), onSaved);
255
+
256
+ return (
257
+ <form onSubmit={submit}>
258
+ <Page
259
+ title="Hand back an item"
260
+ description={item.title}
261
+ back={onBack}
262
+ footer={
263
+ <ActionBar>
264
+ <Button type="submit" loading={busy}>
265
+ Mark as returned
266
+ </Button>
267
+ </ActionBar>
268
+ }
269
+ >
270
+ <Field label="Who picked it up?" error={error} required>
271
+ <TextInput value={returnedTo} onChange={(event) => setReturnedTo(event.target.value)} maxLength={120} autoFocus />
272
+ </Field>
273
+ </Page>
274
+ </form>
275
+ );
276
+ }
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { App } from './App';
4
+ import './styles.css';
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ );
@@ -0,0 +1,6 @@
1
+ /* The look comes from @oneteam-app/ui: its components and its theme. The
2
+ organisation's own colour is applied by <OneteamTheme>. Add your own rules
3
+ below only for layout the components do not cover, and use the theme's
4
+ tokens (bg-card, text-muted-foreground, var(--primary)…), never a colour. */
5
+ @import "tailwindcss";
6
+ @import "@oneteam-app/ui/theme.css";
@@ -0,0 +1,22 @@
1
+ // Shapes the API returns and the front end reads.
2
+
3
+ export interface Item {
4
+ id: number;
5
+ communityId: string;
6
+ title: string;
7
+ location: string;
8
+ foundByName: string;
9
+ createdAt: string;
10
+ returnedAt: string | null;
11
+ returnedTo: string | null;
12
+ /** Whether the signed-in user may mark this item as returned. The API checks again */
13
+ canReturn: boolean;
14
+ }
15
+
16
+ export interface ItemsResponse {
17
+ items: Item[];
18
+ communities: Array<{ id: string; name: string }>;
19
+ }
20
+
21
+ /** Who may mark an item as returned: people who manage colleagues in the item's community. */
22
+ export const RETURN_PERMISSION = 'network::users.update';
@@ -0,0 +1,15 @@
1
+ import { withDevSignOn } from '@oneteam-app/apps/testing';
2
+ import manifest from '../../oneteam.app.json';
3
+ import worker, { type Env } from './index';
4
+ import { jobs } from './jobs';
5
+
6
+ // Local development only (wrangler.jsonc points here). Stands in for the
7
+ // sign-on gateway: every request is made as the person picked in the "View as"
8
+ // bar at the top of the page, so the app runs without a network. The bar can
9
+ // also run a job by hand, and what the app sends is printed in this terminal
10
+ // instead of delivered (only what oneteam.app.json lists under "sends").
11
+ // `oneteam-apps deploy` bundles src/worker/index.ts, which has none of this.
12
+ export default withDevSignOn<Env>(worker, {
13
+ jobs,
14
+ sends: (manifest as { sends?: Array<'notifications' | 'chat_messages'> }).sends ?? [],
15
+ });
@@ -0,0 +1,10 @@
1
+ import type { D1Migration } from '@cloudflare/vitest-plugin';
2
+
3
+ declare global {
4
+ namespace Cloudflare {
5
+ interface Env {
6
+ DB: D1Database;
7
+ TEST_MIGRATIONS: D1Migration[];
8
+ }
9
+ }
10
+ }