coworking-agents 0.9.0 → 0.10.0
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/CHANGELOG.md +12 -0
- package/README.md +9 -3
- package/bin/coworking-agents.js +1 -1
- package/docs/achievements.png +0 -0
- package/docs/building.png +0 -0
- package/docs/camera.png +0 -0
- package/docs/christmas.png +0 -0
- package/docs/customise.png +0 -0
- package/docs/game-room.png +0 -0
- package/docs/halloween.png +0 -0
- package/docs/house-wall.png +0 -0
- package/docs/mobile.png +0 -0
- package/docs/office-day.png +0 -0
- package/docs/office-night.png +0 -0
- package/docs/panel.png +0 -0
- package/docs/paper.png +0 -0
- package/docs/servers.png +0 -0
- package/docs/talk.png +0 -0
- package/docs/top-bar.png +0 -0
- package/docs/trophy-room.png +0 -0
- package/docs/usage.png +0 -0
- package/docs/work-site.png +0 -0
- package/package.json +1 -1
- package/public/favicon.ico +0 -0
- package/public/favicon.png +0 -0
- package/public/index.html +4 -4
- package/public/js/floorplan.js +7 -3
- package/public/js/i18n.js +11 -2
- package/public/js/main.js +2 -2
- package/public/js/places.js +123 -0
- package/public/js/player.js +2 -0
- package/public/js/rooms.js +1 -1
- package/public/js/scene.js +1 -0
- package/public/js/tasks.js +46 -5
- package/public/js/topbar.js +1 -1
- package/public/js/views.js +3 -2
- package/public/style.css +12 -0
- package/public/tray.png +0 -0
- package/src/autostart.js +2 -1
- package/src/menubar.js +1 -1
- package/src/server.js +29 -6
- package/src/tasks.js +1 -0
- package/public/favicon.svg +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.0 — 2026-10-14
|
|
4
|
+
- Fix: the test suite no longer stops a running office started at login (it turned the login item off through the real launchd label)
|
|
5
|
+
- Three new rooms on the office floor, under the game room: the library, the lab and the waiting room. The agents stay at their desks; walk into a room (arrows/WASD or a click) and you see who is reading or browsing, running commands, or waiting on you right now. A number on the door says how many; Enter next to someone opens them (the waiting room opens the question)
|
|
6
|
+
- Walk inside the rooftop, the trophy room and the server room too; the elevator door in the corner takes you back
|
|
7
|
+
|
|
8
|
+
## 0.9.1 — 2026-10-14
|
|
9
|
+
- ☰ → Settings → *Open with the computer*: the start-at-login switch right in the menu, with its green dot (it also stays in Personalise)
|
|
10
|
+
- The browser tab shows the new app icon (the agent at its desk) instead of the old face; the menu bar keeps the small face, which reads best at that size
|
|
11
|
+
- Phone: always the same address (the computer's name and port 4778, whatever port the office got), so the home-screen icon added once keeps working
|
|
12
|
+
- Phone: turn it on from the office (☰ → 📱 → *Turn the phone on*) and it stays on, starting with the office every time with the same QR (the app and autostart included); *Turn the phone off* ends it. `--phone` still works
|
|
13
|
+
- ☰ menu: three groups (See, Do, Settings) side by side, every item with a pixel icon in the same style, and it scrolls instead of running off the screen
|
|
14
|
+
|
|
3
15
|
## 0.9.0 — 2026-10-14
|
|
4
16
|
- Tell me when… (☰ → ⏰, or the agent's panel): a notice when an agent finishes (once), when a limit passes a level you pick (again after it resets), or when any agent edits a file whose path has some text; in the office, the bell and as a system notification. Kept with your progress
|
|
5
17
|
- Past conversations (☰ → 🗄): every Claude Code and Codex conversation on this computer from the last 60 days, open or closed, found by name, folder or what was said (last 30 days), and read as a chat with its helpers and a search inside it. Read only; off in private mode
|
package/README.md
CHANGED
|
@@ -212,13 +212,16 @@ The 🔔 bell collects agent requests, task updates, recommendations (*Apply*, *
|
|
|
212
212
|
The office works in any browser. If you prefer an app:
|
|
213
213
|
|
|
214
214
|
- **Create the app** (Personalise, or `npx coworking-agents install-app`): a *Coworking Agents* app made on your computer — free, no store, no signing — in Applications (macOS), the Start Menu and Desktop (Windows) or the applications menu (Linux). Opening it starts the office if needed and shows it in an app window. `uninstall-app` removes it.
|
|
215
|
-
- **On the phone:** with `--phone
|
|
215
|
+
- **On the phone:** turn the phone on in ☰ → 📱 Phone (or start with `--phone`) and add it to the home screen (Safari → Share → Add to Home Screen; Chrome → Add to Home screen). Its address never changes: the computer's own name on the network (`yourmac.local`, so a new Wi-Fi address doesn't matter) and port 4778, with a kept code, so the icon added once keeps working after restarts and updates. *Change the phone code* cuts every phone off.
|
|
216
216
|
- **Install as an app** (Personalise, or Chrome/Edge's install icon): its own window, no tabs, an icon in the Dock or taskbar. Nothing is cached; it is the same live office.
|
|
217
217
|
- **`npx coworking-agents --app`** opens it in an app window of Chrome, Edge, Brave or Chromium without installing anything.
|
|
218
|
-
- **Start with the computer:** `npx coworking-agents autostart on` (or
|
|
218
|
+
- **Start with the computer:** `npx coworking-agents autostart on` (or ☰ → Settings → *Open with the computer*) starts it hidden at login with the tray icon; `autostart off` undoes it. It uses each system's own login entry (macOS LaunchAgent, Windows Startup folder, Linux XDG autostart) with the program and its arguments, never a shell line.
|
|
219
219
|
|
|
220
220
|
## More panels
|
|
221
221
|
|
|
222
|
+
The ☰ menu groups everything in three columns: **See** (tasks, costs, notices, history, repositories, past conversations, timelapse, releases), **Do** (new session, message several, tell me when…, phone, TV mode, search, replay, photo, decorate) and **Settings** (system notices, sounds, focus, walking, radio, open with the computer, language, personalise).
|
|
223
|
+
|
|
224
|
+
|
|
222
225
|
- **📦 Repositories:** who is in each repo now, what is not committed, to push or behind, worktrees, open tasks, PRs and today's activity.
|
|
223
226
|
- **📣 Message several agents:** one request to the agents you pick, typed into terminal sessions that are waiting and continued in the background for VS Code and app conversations; busy ones are left out.
|
|
224
227
|
- **Office events and mood:** confetti for tests passing, a PR opened or a week under the cost goal; rain when tests or a task fail; the room turns grey-blue when many agents are stuck on you and warm when everything flows.
|
|
@@ -230,13 +233,14 @@ The office works in any browser. If you prefer an app:
|
|
|
230
233
|
- **⏰ Tell me when…:** a notice when an agent finishes (also a button in its panel), when a limit passes a level you pick, or when any agent edits a file you name.
|
|
231
234
|
- **🗄 Past conversations:** every Claude Code and Codex conversation from the last 60 days, open or closed, found by name, folder or what was said, and read as a chat with its helpers. Read only.
|
|
232
235
|
- **🎞 Day timelapse:** a picture of the office every 5 minutes (no names), turned into a ~20 s GIF in your browser. The pictures stay in this browser for 3 days.
|
|
236
|
+
- **👥 Everyone in the office:** the count under the office name (`6 agents +2 🤖` while helpers work); click it for every agent with its state and the helpers working for it.
|
|
233
237
|
- **📺 TV mode** (or `?tv=1`): full screen, only the office and big counters; Esc leaves.
|
|
234
238
|
- **Waiting on you today** in the daily report: measured time the agents waited on your answers and tasks stood blocked on you.
|
|
235
239
|
- **Sounds and do not disturb** (Personalise): each sound on or off, and a daily quiet window that mutes sounds and system notifications.
|
|
236
240
|
|
|
237
241
|
## Phone panel (opt-in)
|
|
238
242
|
|
|
239
|
-
`npx coworking-agents --phone
|
|
243
|
+
Turn the phone on in ☰ → 📱 Phone (it stays on and starts with the office every time), or start with `npx coworking-agents --phone`: a small app for your phone on the same Wi-Fi, in the office's pixel look, updated live as things change. Scan the QR code printed in the terminal or shown in ☰ → 📱 Phone, then add it to the home screen.
|
|
240
244
|
|
|
241
245
|
- **Now:** who needs you and every agent with what it is doing.
|
|
242
246
|
- **Tap an agent** to see it in full: what it asks or does right now, your last request, its last answer, what it did, the files it touched, its helpers, model, context and cost. The **Conversation** tab shows the whole conversation as a chat, with a search inside it (only what you wrote and what it answered, never tool output). When it finished its turn you can **write to it**; the text is typed into its terminal, like the office does (not into the Claude app or VS Code).
|
|
@@ -271,6 +275,8 @@ Click the elevator (or press Enter next to it) to ride between floors, up to the
|
|
|
271
275
|
|
|
272
276
|

|
|
273
277
|
|
|
278
|
+
|
|
279
|
+
**More rooms on the floor:** under the game room are the **library**, the **lab** and the **waiting room**. The agents stay at their desks; walk into a room and you see who is reading or browsing, running commands or tests, or waiting on you right now (a number on the door says how many). Enter next to someone opens them. You can also walk inside the rooftop, the trophy room and the server room.
|
|
274
280
|
## Achievements, coins and the shop
|
|
275
281
|
|
|
276
282
|
- **86 achievements**, each with five tiers from bronze to legend, measured from your history (hours, actions, streaks, nights, Friday evenings, weeks and months with work, tokens, cache, models, cost), the live office (parallel sessions, subagents, giant contexts, commits and pushes, repos and branches, quick answers, clean desks) and what you do in the office (walking, talking, the radio, the shop, customising). A few are secret.
|
package/bin/coworking-agents.js
CHANGED
|
@@ -117,7 +117,7 @@ async function reuse() {
|
|
|
117
117
|
} catch {}
|
|
118
118
|
}
|
|
119
119
|
console.log(`coworking-agents → ${hq.url}${privacy ? ' (private mode)' : ''}\nCtrl+C to close.`);
|
|
120
|
-
if (phone) {
|
|
120
|
+
if (phone || hq.phone) { // --phone, or the phone turned on in the office (remembered)
|
|
121
121
|
if (hq.phone) console.log(`\nPhone (same Wi-Fi, trusted networks only — the LAN link is not encrypted) → ${hq.phone.url}\n${require('../src/qr').toTerminal(require('../src/qr').encode(hq.phone.url))}\n`);
|
|
122
122
|
else console.log(`\nPhone panel not started: ${hq.phoneReason === 'nolan' ? 'no local network address found' : hq.phoneReason}`);
|
|
123
123
|
}
|
package/docs/achievements.png
CHANGED
|
Binary file
|
package/docs/building.png
CHANGED
|
Binary file
|
package/docs/camera.png
CHANGED
|
Binary file
|
package/docs/christmas.png
CHANGED
|
Binary file
|
package/docs/customise.png
CHANGED
|
Binary file
|
package/docs/game-room.png
CHANGED
|
Binary file
|
package/docs/halloween.png
CHANGED
|
Binary file
|
package/docs/house-wall.png
CHANGED
|
Binary file
|
package/docs/mobile.png
CHANGED
|
Binary file
|
package/docs/office-day.png
CHANGED
|
Binary file
|
package/docs/office-night.png
CHANGED
|
Binary file
|
package/docs/panel.png
CHANGED
|
Binary file
|
package/docs/paper.png
CHANGED
|
Binary file
|
package/docs/servers.png
CHANGED
|
Binary file
|
package/docs/talk.png
CHANGED
|
Binary file
|
package/docs/top-bar.png
CHANGED
|
Binary file
|
package/docs/trophy-room.png
CHANGED
|
Binary file
|
package/docs/usage.png
CHANGED
|
Binary file
|
package/docs/work-site.png
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coworking-agents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "A pixel-art coworking office for your AI coding agents (Claude Code, Codex, …): see who's working, who needs you, and who's editing the same checkout.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"coworking-agents": "bin/coworking-agents.js"
|
package/public/favicon.ico
CHANGED
|
Binary file
|
package/public/favicon.png
CHANGED
|
Binary file
|
package/public/index.html
CHANGED
|
@@ -4,10 +4,9 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>coworking-agents</title>
|
|
7
|
-
<link rel="icon" href="favicon.ico?v=
|
|
8
|
-
<link rel="icon" href="favicon.png?v=
|
|
9
|
-
<link rel="icon" href="
|
|
10
|
-
<link rel="apple-touch-icon" href="apple-touch-icon.png?v=4">
|
|
7
|
+
<link rel="icon" href="favicon.ico?v=5" sizes="32x32">
|
|
8
|
+
<link rel="icon" href="favicon.png?v=5" type="image/png" sizes="64x64">
|
|
9
|
+
<link rel="apple-touch-icon" href="apple-touch-icon.png?v=5">
|
|
11
10
|
<link rel="manifest" href="manifest.webmanifest">
|
|
12
11
|
<meta name="theme-color" content="#17151a">
|
|
13
12
|
<link rel="stylesheet" href="style.css">
|
|
@@ -86,6 +85,7 @@
|
|
|
86
85
|
<script src="js/timelapse.js"></script>
|
|
87
86
|
<script src="js/archive.js"></script>
|
|
88
87
|
<script src="js/watches.js"></script>
|
|
88
|
+
<script src="js/places.js"></script>
|
|
89
89
|
<script src="js/main.js"></script>
|
|
90
90
|
</body>
|
|
91
91
|
</html>
|
package/public/js/floorplan.js
CHANGED
|
@@ -10,7 +10,7 @@ let ctx = mainCtx; // switched to the thumbnail canvas while drawing another flo
|
|
|
10
10
|
Art.setCtx(ctx);
|
|
11
11
|
const overlay = document.getElementById('overlay');
|
|
12
12
|
let S = 3, W = 480, H = 300, CX = 320, RX = 330;
|
|
13
|
-
let game = null, data = null, selected = null, layout = [], desks = [], rooms = [], floors = [], floor = 0, spots = null, hallBox = null, boardBox = null, wing = null, floorStates = [], building = false;
|
|
13
|
+
let game = null, places = [], data = null, selected = null, layout = [], desks = [], rooms = [], floors = [], floor = 0, spots = null, hallBox = null, boardBox = null, wing = null, floorStates = [], building = false;
|
|
14
14
|
|
|
15
15
|
function rawRoomKey(p) {
|
|
16
16
|
if (p.repo && p.repo.name) return p.repo.name;
|
|
@@ -131,15 +131,19 @@ function buildFloor(i) {
|
|
|
131
131
|
const game = { x: 8, y: gy, w: gw, h: GH, doorY: gy + GH / 2,
|
|
132
132
|
doors: [{ side: 'right', x: 8 + gw, y: gy + GH / 2 }, { side: 'top', x: 8 + Math.round(gw * .2), y: gy }, { side: 'top', x: 8 + Math.round(gw * .62), y: gy }, { side: 'bottom', x: 8 + Math.round(gw * .75), y: gy + GH }] };
|
|
133
133
|
y += GH + SHELF_GAP + 4;
|
|
134
|
+
// under the game room: the library, the lab and the waiting room, side by side, doors on their top wall to the hallway
|
|
135
|
+
const PH = 118, pw = Math.floor((CX - 20 - 16) / 3), py = y + 6;
|
|
136
|
+
const places = ['library', 'lab', 'waiting'].map((name, k) => { const x = 8 + k * (pw + 8); return { name, x, y: py, w: pw, h: PH, doors: [{ side: 'top', x: x + Math.round(pw / 2), y: py }] }; });
|
|
137
|
+
y += PH + SHELF_GAP + 10;
|
|
134
138
|
const occupied = ds.filter(d => d.p);
|
|
135
139
|
const lay = occupied.map(d => Object.assign({}, d));
|
|
136
140
|
lay.forEach((c, k) => { occupied[k].cell = c; });
|
|
137
|
-
return { rooms: rs, desks: ds, layout: lay, bottom: y, H: 0, game };
|
|
141
|
+
return { rooms: rs, desks: ds, layout: lay, bottom: y, H: 0, game, places };
|
|
138
142
|
}
|
|
139
143
|
|
|
140
144
|
function useFloor(i) {
|
|
141
145
|
const f = floorStates[i];
|
|
142
|
-
rooms = f.rooms; desks = f.desks; layout = f.layout; H = f.H; game = f.game;
|
|
146
|
+
rooms = f.rooms; desks = f.desks; layout = f.layout; H = f.H; game = f.game; places = f.places || [];
|
|
143
147
|
}
|
|
144
148
|
|
|
145
149
|
function fitScale() {
|
package/public/js/i18n.js
CHANGED
|
@@ -124,7 +124,7 @@ const I18N = {
|
|
|
124
124
|
surprise: { hint: 'Uma surpresa! Clique para pegar', cake: 'Bolo surpresa na copa!', pigeon: 'Um pombo na janela!', ticket: 'Um bilhete dourado no chão!' },
|
|
125
125
|
scoreboard: { title: 'Placar da semana entre as IAs', hours: 'horas', actions: 'ações', tokens: 'tokens' },
|
|
126
126
|
share: { bestDay: 'Melhor dia', topAi: 'Mais usada', cost: 'Custo estimado', ready: 'Seu cartão da semana está pronto', readyWhy: 'Horas, ações, melhor dia, a IA mais usada e o custo da semana, numa imagem para compartilhar (Jornal → Cartão da semana).', card: 'Cartão da semana', visit: 'Foto para visita', week: 'Minha semana no escritório', hours: 'horas de agentes trabalhando', actions: 'ações', trophies: 'níveis de conquista · lendas', saved: 'Cartão salvo', visitSaved: 'Foto para visita salva', visitNote: 'Sem nomes, branches nem o nome do escritório.' },
|
|
127
|
-
elevator: { title: 'Elevador', sub: 'Escolha o andar: terraço, escritório,
|
|
127
|
+
elevator: { title: 'Elevador', sub: 'Escolha o andar: terraço, escritório, troféus ou servidores. Dentro das salas você anda com as setas.', terrace: '🌇 Terraço', trophy: '🏆 Sala de troféus', servers: '🖧 Sala de servidores', office: '🏢 Escritório', floor: n => `🏢 Andar ${n}` },
|
|
128
128
|
tasks: { tab: n => n ? `Tarefas (${n})` : 'Tarefas', sub: 'Crie uma tarefa e um agente trabalha nela em segundo plano. Você responde, aprova e revisa por aqui, sem abrir o terminal. Só as tarefas criadas aqui podem ser controladas; as sessões abertas por fora seguem só monitoradas.',
|
|
129
129
|
newT: 'Nova tarefa', title: 'Título (opcional)', desc: 'O que fazer', folder: 'Pasta', agent: 'Agente', auto: 'Automático', profile: 'Perfil', prio: 'Prioridade', prios: ['', 'Baixa', 'Normal', 'Alta'],
|
|
130
130
|
budget: 'Orçamento (US$, 0 = sem limite)', test: 'Comando de teste (opcional)', wt: 'Worktree separada (isola as alterações)', perm: 'Permissões', perms: { acceptEdits: 'Pode editar arquivos', plan: 'Só planejar', default: 'Pedir para tudo' },
|
|
@@ -349,7 +349,7 @@ const I18N = {
|
|
|
349
349
|
surprise: { hint: 'A surprise! Click to grab it', cake: 'Surprise cake in the kitchen!', pigeon: 'A pigeon at the window!', ticket: 'A golden ticket on the floor!' },
|
|
350
350
|
scoreboard: { title: "This week's AI scoreboard", hours: 'hours', actions: 'actions', tokens: 'tokens' },
|
|
351
351
|
share: { bestDay: 'Best day', topAi: 'Used most', cost: 'Estimated cost', ready: 'Your weekly card is ready', readyWhy: "Hours, actions, best day, the AI used most and the week's cost, in one image to share (Newspaper → Weekly card).", card: 'Weekly card', visit: 'Visitor photo', week: 'My week at the office', hours: 'hours of agents at work', actions: 'actions', trophies: 'achievement tiers · legends', saved: 'Card saved', visitSaved: 'Visitor photo saved', visitNote: 'No names, branches or office name.' },
|
|
352
|
-
elevator: { title: 'Elevator', sub: 'Pick a floor: rooftop, office,
|
|
352
|
+
elevator: { title: 'Elevator', sub: 'Pick a floor: rooftop, office, trophies or servers. Inside the rooms you walk with the arrows.', terrace: '🌇 Rooftop', trophy: '🏆 Trophy room', servers: '🖧 Server room', office: '🏢 Office', floor: n => `🏢 Floor ${n}` },
|
|
353
353
|
tasks: { tab: n => n ? `Tasks (${n})` : 'Tasks', sub: 'Create a task and an agent works on it in the background. You answer, approve and review here, no terminal needed. Only tasks created here can be controlled; sessions opened elsewhere stay monitor-only.',
|
|
354
354
|
newT: 'New task', title: 'Title (optional)', desc: 'What to do', folder: 'Folder', agent: 'Agent', auto: 'Automatic', profile: 'Profile', prio: 'Priority', prios: ['', 'Low', 'Normal', 'High'],
|
|
355
355
|
budget: 'Budget (USD, 0 = no limit)', test: 'Test command (optional)', wt: 'Separate worktree (isolates the changes)', perm: 'Permissions', perms: { acceptEdits: 'May edit files', plan: 'Plan only', default: 'Ask for everything' },
|
|
@@ -626,6 +626,15 @@ Object.assign(I18N.pt.officeObj.menu, { watches: 'Me avise quando…' });
|
|
|
626
626
|
Object.assign(I18N.en.officeObj.menu, { watches: 'Tell me when…' });
|
|
627
627
|
Object.assign(I18N.pt, { watch: { title: 'Me avise quando…', sub: 'Avisos que você escolhe. Chegam aqui, no sino e como notificação do sistema.', empty: 'Nenhum aviso ainda.', add: 'Adicionar', addDone: 'Quando um agente terminar', addLimit: 'Quando um limite passar de', addFile: 'Quando algum agente mexer num arquivo', filePh: 'parte do caminho, ex.: checkout.ts ou src/auth', note: 'Funcionam enquanto o escritório estiver aberto. O de agente vale uma vez; o de limite volta a valer quando o limite renova; o de arquivo avisa a cada edição nova.', whenDone: 'Me avise quando terminar', cancelDone: 'Cancelar o aviso', set: 'Aviso criado', whenDoneOf: n => `Quando ${n} terminar`, whenLimit: (l, p) => `Quando ${l} passar de ${p}%`, whenFile: t => `Quando algum agente mexer em “${t}”`, done: n => `${n} terminou`, needs: n => `${n} parou e precisa de você`, closed: n => `${n} fechou`, limit: (l, p) => `${l} chegou a ${p}%`, limitWhy: p => `Você pediu aviso a partir de ${p}%.`, file: (n, f) => `${n} mexeu em ${f}` } });
|
|
628
628
|
Object.assign(I18N.en, { watch: { title: 'Tell me when…', sub: 'Notices you choose. They show here, in the bell and as system notifications.', empty: 'No notices yet.', add: 'Add', addDone: 'When an agent finishes', addLimit: 'When a limit passes', addFile: 'When any agent touches a file', filePh: 'part of the path, e.g. checkout.ts or src/auth', note: 'They work while the office is open. The agent one fires once; the limit one again after the limit resets; the file one on every new edit.', whenDone: 'Tell me when it finishes', cancelDone: 'Cancel the notice', set: 'Notice set', whenDoneOf: n => `When ${n} finishes`, whenLimit: (l, p) => `When ${l} passes ${p}%`, whenFile: t => `When any agent touches “${t}”`, done: n => `${n} finished`, needs: n => `${n} stopped and needs you`, closed: n => `${n} closed`, limit: (l, p) => `${l} reached ${p}%`, limitWhy: p => `You asked to be told from ${p}%.`, file: (n, f) => `${n} edited ${f}` } });
|
|
629
|
+
// the phone panel turned on from the office
|
|
630
|
+
Object.assign(I18N.pt, { phoneOn: { how: 'Veja os agentes, as tarefas e responda pelo celular, no mesmo Wi-Fi. Ao ligar aqui, ele volta a ligar sozinho toda vez que o escritório abrir.', on: 'Ligar o celular', off: 'Desligar o celular', cli: 'Ou abra o escritório com', kept: 'Fica ligado: abre junto com o escritório nas próximas vezes, com o mesmo QR.', fail: { nolan: 'não achei uma rede Wi-Fi neste computador', port: 'a porta do celular está ocupada', error: 'não deu para ligar', demo: 'modo demonstração' } } });
|
|
631
|
+
Object.assign(I18N.en, { phoneOn: { how: 'See the agents and tasks and answer from your phone, on the same Wi-Fi. Turned on here, it comes back on by itself every time the office opens.', on: 'Turn the phone on', off: 'Turn the phone off', cli: 'Or start the office with', kept: 'It stays on: it opens with the office next time, with the same QR.', fail: { nolan: 'no Wi-Fi network found on this computer', port: 'the phone port is taken', error: 'could not turn it on', demo: 'demo mode' } } });
|
|
632
|
+
// the ☰ menu's groups
|
|
633
|
+
Object.assign(I18N.pt, { menuGroups: { see: 'Ver', do: 'Fazer', prefs: 'Ajustes', autostart: 'Abrir com o computador' } });
|
|
634
|
+
Object.assign(I18N.en, { menuGroups: { see: 'See', do: 'Do', prefs: 'Settings', autostart: 'Open with the computer' } });
|
|
635
|
+
// the library, the lab and the meeting room
|
|
636
|
+
Object.assign(I18N.pt, { places: { waiting: 'Sala de espera', no_library: 'Ninguém lendo agora', no_lab: 'Nenhum comando rodando', no_waiting: 'Ninguém esperando você', library: 'Biblioteca', lab: 'Laboratório', meeting: 'Sala de reunião', back: 'Voltar ao escritório', noReaders: 'NINGUEM LENDO AGORA', noRunners: 'NENHUM COMANDO RODANDO', noMeeting: 'NENHUMA REUNIAO AGORA', pipeline: 'PIPELINE', plan: 'PLANO', helpersNow: 'AJUDANTES AGORA', meetCount: (a, h) => `${a} AGENTE${a === 1 ? '' : 'S'} · ${h} AJUDANTE${h === 1 ? '' : 'S'}` } });
|
|
637
|
+
Object.assign(I18N.en, { places: { waiting: 'Waiting room', no_library: 'Nobody reading now', no_lab: 'No commands running', no_waiting: 'Nobody waiting on you', library: 'Library', lab: 'Lab', meeting: 'Meeting room', back: 'Back to the office', noReaders: 'NOBODY READING NOW', noRunners: 'NO COMMANDS RUNNING', noMeeting: 'NO MEETING NOW', pipeline: 'PIPELINE', plan: 'PLAN', helpersNow: 'HELPERS NOW', meetCount: (a, h) => `${a} AGENT${a === 1 ? '' : 'S'} · ${h} HELPER${h === 1 ? '' : 'S'}` } });
|
|
629
638
|
let T = I18N[lang];
|
|
630
639
|
// sound is on by default; alerts follow the browser permission (asked on open). Only the buttons turn them off.
|
|
631
640
|
let soundOn = store.get('sound', true);
|
package/public/js/main.js
CHANGED
|
@@ -24,10 +24,10 @@ function loop(t) {
|
|
|
24
24
|
if (document.hidden) return; // tab not visible: draw nothing (browsers also slow rAF, this makes it zero) // schedule first: an error in one frame must not stop the animation
|
|
25
25
|
if (data && spots) {
|
|
26
26
|
const f = (t / 70) | 0; // ~14 frames per second, a pixel-art pace
|
|
27
|
-
if (f !== lastFrame || (playerOn && (me.moving || me.path.length || keysDown.size))) {
|
|
27
|
+
if (f !== lastFrame || (playerOn && (me.moving || me.path.length || keysDown.size || (trophyView && (vme.moving || vme.path.length))))) {
|
|
28
28
|
const dt = lastT ? Math.min(.25, (t - lastT) / 1000) : 0;
|
|
29
29
|
lastT = t; lastFrame = f;
|
|
30
|
-
if (trophyView) (
|
|
30
|
+
if (trophyView) { roomUpdate(dt); ({ terrace: drawTerrace, servers: drawServers }[viewName] || drawTrophyRoom)(t); } else if (building) { if (f % 3 === 0) drawThumbs(t, dt * 3); } else drawScene(t, dt);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// ---------------- the library, the lab and the waiting room on the office floor; walking inside the elevator rooms ----------------
|
|
3
|
+
// The agents stay at their desks. Walk into one of the three rooms under the game room and it shows who is doing that
|
|
4
|
+
// right now (measured from their state): reading or browsing (library), running commands (lab), waiting on you
|
|
5
|
+
// (waiting room). Enter next to someone opens them. The rooftop, trophy and server rooms can be walked in too.
|
|
6
|
+
|
|
7
|
+
// --- walking in a room: its floor, where you arrive, the door back ---
|
|
8
|
+
const ROOM_WALK = {
|
|
9
|
+
terrace: { x: 20, y: 186, w: 600, h: 192, door: { x: 26, y: 222 } },
|
|
10
|
+
servers: { x: 20, y: 200, w: 600, h: 178, door: { x: 26, y: 236 } },
|
|
11
|
+
trophy: { x: 20, y: 290, w: 600, h: 88, door: { x: 26, y: 326 } },
|
|
12
|
+
};
|
|
13
|
+
const vme = { x: 0, y: 0, path: [], flip: false, moving: false, room: '' };
|
|
14
|
+
let vTag = null, vHint = null, vLastHint = '';
|
|
15
|
+
const roomWalk = () => (trophyView && playerOn && ROOM_WALK[viewName]) || null;
|
|
16
|
+
function roomUpdate(dt) {
|
|
17
|
+
const F = roomWalk(); if (!F) { if (vTag) vTag.hidden = vHint.hidden = true; return; }
|
|
18
|
+
if (vme.room !== viewName) Object.assign(vme, { x: F.door.x + 22, y: F.door.y - 6, path: [], room: viewName }); // in through the door
|
|
19
|
+
let dx = 0, dy = 0; const k = c => keysDown.has(c);
|
|
20
|
+
if (k('ArrowLeft') || k('a')) dx--; if (k('ArrowRight') || k('d')) dx++; if (k('ArrowUp') || k('w')) dy--; if (k('ArrowDown') || k('s')) dy++;
|
|
21
|
+
vme.moving = false;
|
|
22
|
+
const clamp = p => ({ x: Math.max(F.x, Math.min(F.x + F.w - 16, p.x)), y: Math.max(F.y, Math.min(F.y + F.h - 24, p.y)) });
|
|
23
|
+
if (dx || dy) {
|
|
24
|
+
vme.path = []; const len = Math.hypot(dx, dy), step = PLAYER_SPEED * (k('Shift') ? 2 : 1) * dt;
|
|
25
|
+
Object.assign(vme, clamp({ x: vme.x + dx / len * step, y: vme.y + dy / len * step })); vme.moving = true; if (dx) vme.flip = dx < 0;
|
|
26
|
+
} else if (vme.path.length) {
|
|
27
|
+
const w = vme.path[0], ddx = w.x - vme.x, ddy = w.y - vme.y, d = Math.hypot(ddx, ddy), step = PLAYER_SPEED * 1.4 * dt;
|
|
28
|
+
if (d <= step) { vme.x = w.x; vme.y = w.y; vme.path.shift(); } else { vme.x += ddx / d * step; vme.y += ddy / d * step; if (Math.abs(ddx) > .5) vme.flip = ddx < 0; }
|
|
29
|
+
vme.moving = true;
|
|
30
|
+
}
|
|
31
|
+
if (vme.moving) { const rc = cv.getBoundingClientRect(), py = rc.top + vme.y * S; if (py > innerHeight - 80) scrollBy({ top: py - (innerHeight - 160) }); else if (py < 130) scrollBy({ top: py - 190 }); } // the page follows you
|
|
32
|
+
if (!vTag) { vTag = document.createElement('span'); vHint = document.createElement('span'); vTag.className = 'me-tag'; vHint.className = 'me-hint'; overlay.append(vTag, vHint); }
|
|
33
|
+
vTag.hidden = false; vTag.textContent = T.walk.you; vTag.style.left = (vme.x + 8) * S + 'px'; vTag.style.top = (vme.y - 4) * S + 'px';
|
|
34
|
+
const n = roomNear(), label = n ? n.label : '';
|
|
35
|
+
vHint.hidden = !n; if (label !== vLastHint) { vLastHint = label; vHint.innerHTML = `<b>Enter ↵</b> ${esc(label)}`; }
|
|
36
|
+
if (n) { vHint.style.left = (vme.x + 8) * S + 'px'; vHint.style.top = (vme.y + 30) * S + 'px'; }
|
|
37
|
+
}
|
|
38
|
+
// what Enter does where you stand: talk to the agent next to you, or take the door back
|
|
39
|
+
function roomNear() {
|
|
40
|
+
const F = roomWalk(); if (!F) return null;
|
|
41
|
+
const c = { x: vme.x + 8, y: vme.y + 12 };
|
|
42
|
+
let best = null;
|
|
43
|
+
for (const h of viewHits) if (h.id) { const d = Math.hypot(h.x + h.w / 2 - c.x, h.y + h.h / 2 - c.y); if (d < 30 && (!best || d < best.d)) best = { d, h }; }
|
|
44
|
+
if (best) { const p = data.people.find(x => x.id === best.h.id); if (p) return { label: T.walk.talk(best.h.who || p.name), run: () => { setView(null); showPerson(p.id); } }; }
|
|
45
|
+
if (Math.hypot(F.door.x + 8 - c.x, F.door.y - c.y) < 26) return { label: T.places.back, run: () => setView(null) };
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
// you, drawn last in the room, and the elevator door you came in through
|
|
49
|
+
function drawRoomWalker(t) {
|
|
50
|
+
const F = roomWalk(); if (!F) return;
|
|
51
|
+
const d = F.door; r(d.x - 4, d.y - 30, 26, 32, PAL.ink); r(d.x - 2, d.y - 28, 10, 28, '#9aa3b0'); r(d.x + 9, d.y - 28, 10, 28, '#9aa3b0'); r(d.x + 8, d.y - 28, 1, 28, '#5a6270');
|
|
52
|
+
r(d.x + 2, d.y - 37, 14, 8, '#1b1d2e'); r(d.x + 5, d.y - 35, 3, 1, '#feae34'); r(d.x + 6, d.y - 36, 1, 1, '#feae34'); r(d.x + 10, d.y - 33, 3, 1, '#feae34'); r(d.x + 11, d.y - 32, 1, 1, '#feae34'); // the lift's ▲▼
|
|
53
|
+
ctx.fillStyle = 'rgba(255,216,77,.35)'; ctx.fillRect(vme.x + 1, vme.y + 21, 14, 3);
|
|
54
|
+
Art.drawStanding(vme.x, vme.y, youLook(), t, vme.moving); if (typeof drawOutfit === 'function') drawOutfit(vme.x, vme.y - 2);
|
|
55
|
+
}
|
|
56
|
+
document.addEventListener('keydown', e => {
|
|
57
|
+
if (!roomWalk() || !reportEl.hidden) return;
|
|
58
|
+
const tag = (e.target.tagName || '').toLowerCase(); if (tag === 'input' || tag === 'textarea') return;
|
|
59
|
+
const key = e.key.length === 1 ? e.key.toLowerCase() : e.key;
|
|
60
|
+
if (key === 'Shift') keysDown.add('Shift');
|
|
61
|
+
if (key.startsWith('Arrow') || (!e.metaKey && !e.ctrlKey && !e.altKey && key.length === 1 && 'wasd'.includes(key))) { keysDown.add(key); e.preventDefault(); e.stopImmediatePropagation(); }
|
|
62
|
+
if (e.key === 'Enter') { const n = roomNear(); if (n) { e.preventDefault(); n.run(); } }
|
|
63
|
+
}, true);
|
|
64
|
+
overlay.addEventListener('click', e => {
|
|
65
|
+
const F = roomWalk(); if (!F || e.target.closest('button,.tag')) return;
|
|
66
|
+
const rc = overlay.getBoundingClientRect(), x = (e.clientX - rc.left) / S - 8, y = (e.clientY - rc.top) / S - 14;
|
|
67
|
+
vme.path = [{ x: Math.max(F.x, Math.min(F.x + F.w - 16, x)), y: Math.max(F.y, Math.min(F.y + F.h - 24, y)) }];
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// --- drawing helpers shared by the three rooms ---
|
|
71
|
+
const plain = x => String(x || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toUpperCase(); // the pixel font has no accents
|
|
72
|
+
const roomName = (x, y, txt) => { const s = plain(txt).slice(0, 12), w = s.length * 4 + 4; r(x + 8 - w / 2, y, w, 7, '#000000aa'); pixText(x + 10 - w / 2, y + 1, s, '#f4ecd8'); };
|
|
73
|
+
const roomSign = (x, y, txt, bg, fg) => { txt = plain(txt); const w = txt.length * 4 + 12; r(x - w / 2 - 2, y - 2, w + 4, 13, PAL.ink); r(x - w / 2, y, w, 9, bg); pixText(x - w / 2 + 6, y + 2, txt, fg); };
|
|
74
|
+
const roomEmpty = (Wt, Ht, txt) => { txt = plain(txt); const w = txt.length * 4 + 16; r(Wt / 2 - w / 2, Ht - 46, w, 14, '#00000066'); pixText(Wt / 2 - w / 2 + 8, Ht - 42, txt, '#f4ecd8'); };
|
|
75
|
+
const doingOf = p => (p.doing ? `${p.doing.tool || ''} ${p.doing.what || ''}`.trim() : '');
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
// ---- the three rooms on the office floor ----
|
|
79
|
+
const PLACE_STATES = { library: ['read', 'web'], lab: ['terminal', 'mcp'], waiting: ['needs_you', 'waiting'] };
|
|
80
|
+
let placeHits = [];
|
|
81
|
+
const placeOf = name => places.find(P => P.name === name);
|
|
82
|
+
function drawPlaceRooms(t, movers) {
|
|
83
|
+
placeHits = [];
|
|
84
|
+
for (const P of places) {
|
|
85
|
+
const { x, y, w, h } = P, d = P.doors[0], L = T.places;
|
|
86
|
+
// floor, walls with the door gap on top, a sign
|
|
87
|
+
const floorC = P.name === 'library' ? ['#8a5a3c', '#93613f'] : P.name === 'lab' ? ['#9aa3b0', '#a7b0bd'] : ['#5a4a6e', '#64547a'];
|
|
88
|
+
r(x, y, w, h, floorC[0]); for (let yy = y; yy < y + h; yy += 8) for (let xx = x + ((yy - y) / 8 % 2) * 8; xx < x + w; xx += 16) r(xx, yy, 8, 8, floorC[1]);
|
|
89
|
+
r(x - 2, y - 2, w + 4, 3, PAL.ink); r(x - 2, y, 2, h + 2, PAL.ink); r(x + w, y, 2, h + 2, PAL.ink); r(x - 2, y + h, w + 4, 2, PAL.ink);
|
|
90
|
+
r(d.x - 9, y - 2, 18, 3, floorC[1]); r(d.x - 9, y + 1, 18, 3, '#00000033'); // the open door
|
|
91
|
+
if (P.name === 'library') { // shelves along the back wall, two reading tables with green lamps
|
|
92
|
+
for (let i = 0; i < Math.floor((w - 12) / 34); i++) { const sx = x + 6 + i * 34; if (Math.abs(sx + 15 - d.x) < 22) continue; r(sx, y + 6, 30, 26, '#5a3a28'); for (let sh = 0; sh < 2; sh++) for (let k = 0; k < 6; k++) { const hh = hash(`pl${P.x}${i}${sh}${k}`); r(sx + 2 + k * 5, y + 8 + sh * 12 + (hh % 3), 4, 10 - (hh % 3), ['#a8434f', '#3b5dc9', '#63c74d', '#feae34', '#7a5cff', '#2ce8f5'][hh % 6]); } }
|
|
93
|
+
for (const tx of [x + w * .18, x + w * .58]) { r(tx, y + 70, w * .26, 12, '#6b4a33'); r(tx, y + 70, w * .26, 2, '#8a6a4a'); r(tx + w * .13 - 1, y + 62, 2, 8, '#c9a34a'); r(tx + w * .13 - 5, y + 59, 10, 4, '#2f8f4e'); }
|
|
94
|
+
} else if (P.name === 'lab') { // a CI screen on the back wall, two benches of monitors
|
|
95
|
+
r(x + w / 2 - 40, y + 6, 80, 24, PAL.ink); r(x + w / 2 - 38, y + 8, 76, 20, '#0c1410'); pixText(x + w / 2 - 34, y + 11, 'CI', '#62ff7a');
|
|
96
|
+
['B', 'T', 'L'].forEach((c, i) => { const on = ((t / 600 + i) | 0) % 3 !== 2; r(x + w / 2 - 20 + i * 18, y + 18, 14, 7, on ? '#1f7a48' : '#1b2a22'); });
|
|
97
|
+
for (const by of [y + 58, y + 92]) { r(x + 10, by, w - 20, 8, '#e8ecf2'); r(x + 10, by, w - 20, 2, '#ffffff'); for (let k = 0; k < 3; k++) { const mx = x + 22 + k * (w - 44) / 3; r(mx, by - 10, 16, 10, PAL.ink); r(mx + 1, by - 9, 14, 8, '#0c1410'); r(mx + 3, by - 7, 4 + hash('lm' + k + by + ((t / 400) | 0)) % 9, 1, '#62ff7a'); } }
|
|
98
|
+
} else { // the waiting room: two phone booths with a red light, a bench
|
|
99
|
+
for (const bx of [x + 10, x + w - 34]) { r(bx, y + 8, 24, 44, '#3a2a4a'); r(bx + 2, y + 12, 20, 38, '#8fc7ff55'); r(bx + 8, y + 6, 8, 3, ((t / 400) | 0) % 2 ? '#ff4d57' : '#5a0f16'); }
|
|
100
|
+
r(x + w * .25, y + 88, w * .5, 6, '#6b4a33'); r(x + w * .25, y + 94, 3, 8, '#4a3324'); r(x + w * .75 - 3, y + 94, 3, 8, '#4a3324');
|
|
101
|
+
}
|
|
102
|
+
const label = plain(L[P.name]), lw = label.length * 4 + 8; r(x + 6, y + h - 12, lw, 9, '#00000088'); pixText(x + 10, y + h - 10, label, '#f4ecd8');
|
|
103
|
+
// inside, you see who is doing this right now (the agents themselves stay at their desks)
|
|
104
|
+
const here = playerOn && me.ready && me.x + 8 > x && me.x + 8 < x + w && me.y + 12 > y && me.y + 12 < y + h;
|
|
105
|
+
const who = (data ? data.people : []).filter(p => !p.leaving && PLACE_STATES[P.name].includes(p.state));
|
|
106
|
+
if (!here) { if (who.length) { const n = String(who.length); r(x + w - 18, y + h - 13, 14, 11, PAL.ink); r(x + w - 17, y + h - 12, 12, 9, P.name === 'waiting' ? '#e43b44' : '#1b1622'); pixText(x + w - 14 - (n.length - 1) * 2, y + h - 10, n, '#ffffff'); } continue; }
|
|
107
|
+
const cols = Math.max(1, Math.floor((w - 20) / 34)), seats = [];
|
|
108
|
+
for (let i = 0; i < who.length && seats.length < cols * 2; i++) seats.push({ x: x + 14 + (i % cols) * 34, y: y + (i < cols ? 44 : 78) - 4 });
|
|
109
|
+
who.slice(0, seats.length).forEach((p, i) => {
|
|
110
|
+
const st = seats[i], lk = look(p.id);
|
|
111
|
+
movers.push({ y: st.y, draw: () => { Art.drawFront(st.x, st.y, lk, t, { legs: Art.LEGS_SIT, legsKey: 'sit' }); const nm = plain(p.name).slice(0, 8), nw = nm.length * 4 + 4; r(st.x + 8 - nw / 2, st.y + 26, nw, 7, '#000000aa'); pixText(st.x + 10 - nw / 2, st.y + 27, nm, '#f4ecd8'); } });
|
|
112
|
+
placeHits.push({ x: st.x, y: st.y, id: p.id, name: p.name, need: P.name === 'waiting' });
|
|
113
|
+
});
|
|
114
|
+
if (!who.length) { const e = plain(L['no_' + P.name]), ew = e.length * 4 + 8; r(x + w / 2 - ew / 2, y + h / 2 - 5, ew, 10, '#00000088'); pixText(x + w / 2 - ew / 2 + 4, y + h / 2 - 3, e, '#f4ecd8'); }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// a click on someone you see in one of these rooms opens them
|
|
118
|
+
overlay.addEventListener('click', e => {
|
|
119
|
+
if (trophyView || !placeHits.length) return;
|
|
120
|
+
const rc = overlay.getBoundingClientRect(), x = (e.clientX - rc.left) / S, y = (e.clientY - rc.top) / S;
|
|
121
|
+
const h = placeHits.find(h => x >= h.x && x <= h.x + 16 && y >= h.y - 2 && y <= h.y + 26); if (!h) return;
|
|
122
|
+
e.stopImmediatePropagation(); h.need && typeof openTalk === 'function' ? openTalk(h.id) : showPerson(h.id);
|
|
123
|
+
}, true);
|
package/public/js/player.js
CHANGED
|
@@ -11,6 +11,7 @@ const PLAYER_SPEED = 70;
|
|
|
11
11
|
function solidRooms() {
|
|
12
12
|
const out = rooms.map(R => ({ x: R.x, y: R.y, w: R.w, h: R.h, doors: [{ x: R.doorX + 9, y: R.y + R.h, side: 'bottom' }], gap: 9 }));
|
|
13
13
|
if (game) out.push({ x: game.x, y: game.y, w: game.w, h: game.h, doors: game.doors, gap: 9 });
|
|
14
|
+
for (const P of places) out.push({ x: P.x, y: P.y, w: P.w, h: P.h, doors: P.doors, gap: 9 });
|
|
14
15
|
if (napBox) out.push({ x: RX - 4, y: wing.nap, w: RW + 8, h: 80, doors: [{ x: RX - 4, y: wing.nap + 37, side: 'left' }], gap: 9 });
|
|
15
16
|
return out;
|
|
16
17
|
}
|
|
@@ -97,6 +98,7 @@ function nearThing() {
|
|
|
97
98
|
}
|
|
98
99
|
for (const g of gameHits) if (near(g.x + g.w / 2, g.y + g.h, 24)) return { label: g.title, run: () => { const m = /data-tab="(\w+)"/.exec(g.attr); if (m) openReport(m[1]); else if (/trophyroom/.test(g.attr)) setTrophyView(true); else if (/arcade/.test(g.attr)) openArcade(); } };
|
|
99
100
|
if (typeof officeTips === 'function') { const o = officeTips(); for (const [b, tab, label] of [[boardBox, 'tasks', o.tasks], [costBox, 'usage', o.costs], [noticesBox, 'notices', o.notices], [historyBox, 'feed', o.history], [releaseBox, 'version', o.releases]]) if (b && near(b.x + b.w / 2, b.y + b.h + 14, 26)) return { label: label.split('\n')[0], run: () => openReport(tab) }; }
|
|
101
|
+
for (const h of (typeof placeHits !== 'undefined' ? placeHits : [])) if (near(h.x + 8, h.y + 12, 22)) return { label: T.walk.talk(h.name), run: () => (h.need ? openTalk(h.id) : showPerson(h.id)) };
|
|
100
102
|
if (radioBox && near(radioBox.x + 7, radioBox.y + 20, 26)) return { label: radioOn ? T.radio.on : T.radio.off, run: () => setRadio(!radioOn) };
|
|
101
103
|
if (catBox && near(cat.x + 6, cat.y + 6, 18)) return { label: T.walk.pet, run: petCat };
|
|
102
104
|
if (elevatorBox && near(elevatorBox.x + 14, elevatorBox.y + 30, 22)) return { label: T.elevator.title, run: openElevator };
|
package/public/js/rooms.js
CHANGED
|
@@ -264,7 +264,7 @@ function drawTrophyRoom(t) {
|
|
|
264
264
|
r(cx - 50, Ht - 34, 100, 15, PAL.ink); r(cx - 49, Ht - 33, 98, 13, '#d9a441'); r(cx - 49, Ht - 33, 98, 1, '#ffd84d'); pixText(cx - 23, Ht - 29, 'HALL OF FAME', '#3b2418');
|
|
265
265
|
const got = st.reduce((x, a) => x + a.tier, 0), all = st.reduce((x, a) => x + a.max, 0);
|
|
266
266
|
pixText(cx - 70, Ht - 12, `${got}/${all}`, '#8a6420'); { const l2 = `${legends.length} LEGEND`; pixText(cx + 70 - l2.length * 4, Ht - 12, l2, '#a3283a'); }
|
|
267
|
-
trophyHits = hits;
|
|
267
|
+
trophyHits = hits; if (typeof drawRoomWalker === 'function') drawRoomWalker(t);
|
|
268
268
|
}
|
|
269
269
|
// clicking a trophy: a card with the trophy large, its tier and how far the next one is
|
|
270
270
|
const trophyCard = document.createElement('div');
|
package/public/js/scene.js
CHANGED
|
@@ -448,6 +448,7 @@ function drawScene(t, dt) {
|
|
|
448
448
|
movers.push({ y: cat.y, draw: () => drawMascot(cat.x, cat.y, cat.mode, t, cat.flip) });
|
|
449
449
|
movers.push(...extraWalkers(t));
|
|
450
450
|
drawPets(t, movers);
|
|
451
|
+
if (typeof drawPlaceRooms === 'function') drawPlaceRooms(t, movers); // the library, the lab and the waiting room (who you see depends on where you stand)
|
|
451
452
|
drawPlayer(t, movers);
|
|
452
453
|
movers.push({ y: robot.y, draw: () => drawRobot(t) });
|
|
453
454
|
movers.sort((a, b) => a.y - b.y).forEach(m => m.draw());
|
package/public/js/tasks.js
CHANGED
|
@@ -622,11 +622,43 @@ function drawReleaseBadge(x, y, t) { // the rocket poster gets a star when a new
|
|
|
622
622
|
// the same panels in the ☰ menu
|
|
623
623
|
{
|
|
624
624
|
const menu = document.getElementById('tools-menu');
|
|
625
|
-
|
|
625
|
+
// each panel gets a 16 px pixel icon in the same style as the menu's other buttons (rectangles x, y, w, h)
|
|
626
|
+
const R = rects => rects.map(([x, y, w, h]) => `M${x} ${y}h${w}v${h}h-${w}z`).join('');
|
|
627
|
+
const ICON = {
|
|
628
|
+
tasks: R([[5, 1, 6, 3], [3, 3, 10, 1], [3, 3, 1, 12], [12, 3, 1, 12], [3, 14, 10, 1], [5, 7, 6, 1], [5, 10, 6, 1]]),
|
|
629
|
+
usage: R([[7, 1, 2, 14], [4, 3, 8, 2], [4, 5, 2, 2], [4, 7, 8, 2], [10, 9, 2, 2], [4, 11, 8, 2]]),
|
|
630
|
+
notices: 'M7 1h2v1h2v1h1v5h1v2h1v2H2v-2h1V8h1V3h1V2h2zM6 13h4v1H9v1H7v-1H6z',
|
|
631
|
+
feed: R([[2, 3, 2, 2], [6, 3, 8, 2], [2, 7, 2, 2], [6, 7, 8, 2], [2, 11, 2, 2], [6, 11, 8, 2]]),
|
|
632
|
+
repos: R([[1, 2, 14, 3], [2, 5, 1, 9], [13, 5, 1, 9], [2, 13, 12, 1], [6, 7, 4, 1]]),
|
|
633
|
+
archive: R([[3, 1, 10, 1], [3, 1, 1, 14], [12, 1, 1, 14], [3, 14, 10, 1], [3, 7, 10, 1], [7, 4, 2, 1], [7, 10, 2, 1]]),
|
|
634
|
+
timelapse: R([[1, 3, 14, 1], [1, 12, 14, 1], [1, 3, 1, 10], [14, 3, 1, 10], [6, 6, 2, 4], [8, 7, 2, 2]]),
|
|
635
|
+
version: R([[7, 1, 2, 2], [6, 3, 4, 7], [4, 8, 2, 3], [10, 8, 2, 3], [7, 11, 2, 3]]),
|
|
636
|
+
broadcast: R([[2, 6, 2, 4], [4, 5, 2, 6], [6, 4, 2, 8], [8, 2, 2, 12], [11, 7, 3, 2]]),
|
|
637
|
+
watches: R([[2, 1, 3, 2], [11, 1, 3, 2], [4, 3, 8, 1], [3, 4, 1, 8], [12, 4, 1, 8], [4, 12, 8, 1], [7, 6, 1, 4], [8, 9, 2, 1], [3, 13, 2, 2], [11, 13, 2, 2]]),
|
|
638
|
+
phone: R([[4, 1, 8, 1], [4, 1, 1, 14], [11, 1, 1, 14], [4, 14, 8, 1], [6, 12, 4, 1]]),
|
|
639
|
+
tv: R([[5, 1, 1, 3], [10, 1, 1, 3], [1, 4, 14, 1], [1, 12, 14, 1], [1, 4, 1, 9], [14, 4, 1, 9], [6, 13, 4, 2]]),
|
|
640
|
+
};
|
|
641
|
+
if (menu) for (const tab of ['tv', 'phone', 'broadcast', 'repos', 'tasks', 'usage', 'watches', 'archive', 'timelapse', 'feed', 'notices', 'version']) {
|
|
626
642
|
const b = document.createElement('button'); b.className = 'icon-btn menu-panel'; b.dataset.menuTab = tab;
|
|
627
|
-
b.innerHTML = `<
|
|
643
|
+
b.innerHTML = `<svg viewBox="0 0 16 16" shape-rendering="crispEdges" aria-hidden="true"><path fill="currentColor" d="${ICON[tab]}"/></svg><span class="ml"></span>`; menu.prepend(b);
|
|
628
644
|
b.addEventListener('click', () => { if (typeof setMenu === 'function') setMenu(false); if (tab === 'tv') return setTv(true); openReport(tab); });
|
|
629
645
|
}
|
|
646
|
+
// three groups instead of one long list: what you look at, what you do, how the office behaves
|
|
647
|
+
if (menu) {
|
|
648
|
+
const groups = [['see', ['[data-menu-tab=tasks]', '[data-menu-tab=usage]', '[data-menu-tab=notices]', '[data-menu-tab=feed]', '[data-menu-tab=repos]', '[data-menu-tab=archive]', '[data-menu-tab=timelapse]', '[data-menu-tab=version]']],
|
|
649
|
+
['do', ['#btn-launch', '[data-menu-tab=broadcast]', '[data-menu-tab=watches]', '[data-menu-tab=phone]', '[data-menu-tab=tv]', '#btn-search', '#btn-replay', '#btn-photo', '#btn-deco']],
|
|
650
|
+
['prefs', ['#btn-notify', '#btn-sound', '#btn-focus', '#btn-walk', '#btn-radio', '#btn-autostart', '#btn-lang', '#btn-office']]];
|
|
651
|
+
// start with the computer, right in the menu (it also stays in Personalise): a switch with the same green dot
|
|
652
|
+
const as = document.createElement('button'); as.id = 'btn-autostart'; as.className = 'icon-btn'; as.setAttribute('aria-pressed', 'false');
|
|
653
|
+
as.innerHTML = `<svg viewBox="0 0 16 16" shape-rendering="crispEdges" aria-hidden="true"><path fill="currentColor" d="${R([[7, 1, 2, 6], [4, 3, 2, 1], [10, 3, 2, 1], [3, 4, 1, 1], [12, 4, 1, 1], [2, 5, 1, 6], [13, 5, 1, 6], [3, 11, 1, 1], [12, 11, 1, 1], [4, 12, 2, 1], [10, 12, 2, 1], [6, 13, 4, 1]])}"/></svg><span class="ml"></span>`;
|
|
654
|
+
menu.appendChild(as);
|
|
655
|
+
const syncAs = () => fetch('api/autostart').then(r => r.json()).then(d => { as.setAttribute('aria-pressed', d.on ? 'true' : 'false'); as.disabled = !!d.demo; }).catch(() => {});
|
|
656
|
+
as.addEventListener('click', async () => { const on = as.getAttribute('aria-pressed') !== 'true'; as.disabled = true; const r = await api('api/autostart', { on }); as.disabled = false; as.setAttribute('aria-pressed', r.on ? 'true' : 'false'); toast(`<b>${esc(T.menuGroups.autostart)}</b>${esc(r.ok ? (r.on ? T.appx.asOn : T.appx.asOff) : '⚠')}`, r.ok ? 'ok' : 'warn'); if (typeof syncAutostart === 'function') syncAutostart(); });
|
|
657
|
+
syncAs(); document.getElementById('btn-menu').addEventListener('click', syncAs);
|
|
658
|
+
const secs = groups.map(([k, sels]) => { const sec = document.createElement('div'); sec.className = 'menu-sec'; sec.innerHTML = `<div class="menu-h" data-menu-group="${k}"></div>`; for (const q of sels) { const el = menu.querySelector(':scope > ' + q); if (el) sec.appendChild(el); } return sec; });
|
|
659
|
+
for (const el of [...menu.querySelectorAll(':scope > .icon-btn')]) secs[1].appendChild(el); // anything not listed still shows
|
|
660
|
+
secs.forEach(sec => menu.appendChild(sec));
|
|
661
|
+
}
|
|
630
662
|
}
|
|
631
663
|
// repositories at a glance: who is in each one now, what is not committed or pushed, task branches and PRs, today's activity
|
|
632
664
|
// one message to several agents: typed into the ones waiting in a terminal, sent in the background to the others
|
|
@@ -730,10 +762,11 @@ document.addEventListener('click', async e => {
|
|
|
730
762
|
async function renderPhone() {
|
|
731
763
|
const P = T.phone, body = reportEl.querySelector('.report-body');
|
|
732
764
|
const d = await api('api/phone'); if (reportTab !== 'phone' || reportEl.hidden) return;
|
|
733
|
-
|
|
765
|
+
const F = T.phoneOn;
|
|
766
|
+
if (!d || !d.on) { body.innerHTML = tabsHtml() + `<h3>📱 ${esc(P.title)}</h3><p>${esc(F.how)}</p><p><button class="btn" data-phone-on="1">📱 ${esc(F.on)}</button> <small class="note" data-phone-msg>${d && d.reason && !['off', 'starting'].includes(d.reason) ? esc(F.fail[d.reason] || d.reason) : ''}</small></p><p class="note">${esc(F.cli)} <code>npx coworking-agents --phone</code></p><p class="note">${esc(P.warn)}</p>`; return; }
|
|
734
767
|
body.innerHTML = tabsHtml() + `<h3>📱 ${esc(P.title)}</h3><p>${esc(P.scan)}</p><canvas class="qr" width="10" height="10"></canvas><p><code class="qr-url">${esc(d.url)}</code></p>${d.ipUrl && d.ipUrl !== d.url ? `<p class="note">${esc(T.phone.ipAlt)}</p><p><code class="qr-url">${esc(d.ipUrl)}</code></p>` : ''}<p class="note">${esc(P.warn)}</p>`;
|
|
735
768
|
drawQr(body.querySelector('canvas.qr'), d.qr);
|
|
736
|
-
body.insertAdjacentHTML('beforeend', `<h3>📲 ${esc(T.phone.homeTitle)}</h3><p class="note">${esc(T.phone.homeIos)}</p><p class="note">${esc(T.phone.homeAndroid)}</p><p class="note">${esc(T.phone.homeNote)}</p><p><button class="btn small ghost" data-phonerotate>🔄 ${esc(T.phone.rotate)}</button> <small class="note" data-phonerotate-msg></small></p>`);
|
|
769
|
+
body.insertAdjacentHTML('beforeend', `<h3>📲 ${esc(T.phone.homeTitle)}</h3><p class="note">${esc(T.phone.homeIos)}</p><p class="note">${esc(T.phone.homeAndroid)}</p><p class="note">${esc(T.phone.homeNote)}</p><p><button class="btn small ghost" data-phonerotate>🔄 ${esc(T.phone.rotate)}</button> <button class="btn small ghost" data-phone-on="0">⏻ ${esc(F.off)}</button> <small class="note" data-phonerotate-msg></small></p><p class="note">${esc(F.kept)}</p>`);
|
|
737
770
|
const V = T.visit; visitUntil = d.visit ? d.visit.until : 0;
|
|
738
771
|
body.insertAdjacentHTML('beforeend', `<h3>👀 ${esc(V.title)}</h3><p class="note">${esc(V.sub)}</p>${d.visit
|
|
739
772
|
? `<p class="ok">${esc(V.live(new Date(d.visit.until).toLocaleTimeString(lang === 'pt' ? 'pt-BR' : 'en', { hour: '2-digit', minute: '2-digit' })))}</p><canvas class="qr qr-visit" width="10" height="10"></canvas><p><code class="qr-url">${esc(d.visit.url)}</code></p><p><button class="btn small danger" data-visit="stop">✕ ${esc(V.stop)}</button></p>`
|
|
@@ -745,6 +778,14 @@ function drawQr(c, qr) {
|
|
|
745
778
|
c.width = c.height = (n + q * 2) * s; g.fillStyle = '#fff'; g.fillRect(0, 0, c.width, c.height); g.fillStyle = '#000';
|
|
746
779
|
qr.forEach((row, r) => [...row].forEach((x, k) => { if (x === '1') g.fillRect((k + q) * s, (r + q) * s, s, s); }));
|
|
747
780
|
}
|
|
781
|
+
// turn the phone panel on or off right here (remembered: it comes back on with the office)
|
|
782
|
+
document.addEventListener('click', async e => {
|
|
783
|
+
const b = e.target.closest('[data-phone-on]'); if (!b) return;
|
|
784
|
+
b.disabled = true;
|
|
785
|
+
const r = await api('api/phone-toggle', { on: b.dataset.phoneOn === '1' });
|
|
786
|
+
if (!r.ok) { b.disabled = false; const m = document.querySelector('[data-phone-msg]'); if (m) m.textContent = T.phoneOn.fail[r.reason] || r.reason || '?'; return; }
|
|
787
|
+
phoneOn = b.dataset.phoneOn === '1'; renderPhone();
|
|
788
|
+
});
|
|
748
789
|
document.addEventListener('click', async e => {
|
|
749
790
|
const b = e.target.closest('[data-phonerotate]'); if (!b || !confirm(T.phone.rotateSure)) return;
|
|
750
791
|
b.disabled = true; const r = await fetch('api/phone-rotate', { method: 'POST', headers: { 'X-Coworking': '1' } }).then(x => x.json()).catch(() => ({ ok: false }));
|
|
@@ -791,7 +832,7 @@ function setTv(on) {
|
|
|
791
832
|
document.addEventListener('keydown', e => { if (e.key === 'Escape' && document.body.classList.contains('tv')) setTv(false); });
|
|
792
833
|
document.addEventListener('fullscreenchange', () => { if (!document.fullscreenElement && document.body.classList.contains('tv')) setTv(false); });
|
|
793
834
|
if (new URLSearchParams(location.search).get('tv') === '1') setTimeout(() => setTv(true), 500);
|
|
794
|
-
function labelMenuPanels() { const O = T.officeObj; document.querySelectorAll('[data-menu-tab]').forEach(b => { const l = O.menu[b.dataset.menuTab]; b.querySelector('.ml').textContent = l; b.title = l; }); }
|
|
835
|
+
function labelMenuPanels() { const O = T.officeObj; document.querySelectorAll('[data-menu-tab]').forEach(b => { const l = O.menu[b.dataset.menuTab]; b.querySelector('.ml').textContent = l; b.title = l; }); document.querySelectorAll('[data-menu-group]').forEach(h => { h.textContent = T.menuGroups[h.dataset.menuGroup]; }); const as = document.querySelector('#btn-autostart .ml'); if (as) { as.textContent = T.menuGroups.autostart; as.parentNode.title = T.appx.autostart; } }
|
|
795
836
|
setInterval(labelMenuPanels, 2000); setTimeout(labelMenuPanels, 300);
|
|
796
837
|
|
|
797
838
|
// ---- the recommendation card: current, recommended and pending config, the reason, Apply; refreshed as the task moves ----
|
package/public/js/topbar.js
CHANGED
|
@@ -213,7 +213,7 @@ const menuEl = document.getElementById('tools-menu'), menuBtn = document.getElem
|
|
|
213
213
|
document.body.appendChild(menuEl); // out of the bar: its overflow would clip the menu in some browsers
|
|
214
214
|
function setMenu(open) {
|
|
215
215
|
menuEl.hidden = !open; menuBtn.setAttribute('aria-expanded', open);
|
|
216
|
-
if (open) { const b = menuBtn.getBoundingClientRect(); menuEl.style.top = (b.bottom + 8) + 'px'; menuEl.style.right = Math.max(8, innerWidth - b.right) + 'px'; }
|
|
216
|
+
if (open) { const b = menuBtn.getBoundingClientRect(); menuEl.style.top = (b.bottom + 8) + 'px'; menuEl.style.right = Math.max(8, innerWidth - b.right) + 'px'; menuEl.style.maxHeight = Math.max(160, innerHeight - b.bottom - 20) + 'px'; } // never taller than the window: it scrolls
|
|
217
217
|
}
|
|
218
218
|
menuBtn.onclick = e => { e.stopPropagation(); setMenu(menuEl.hidden); };
|
|
219
219
|
menuEl.addEventListener('click', e => { if (e.target.closest('#btn-search,#btn-replay')) setMenu(false); });
|
package/public/js/views.js
CHANGED
|
@@ -71,7 +71,7 @@ function drawTerrace(t) {
|
|
|
71
71
|
});
|
|
72
72
|
if (!relaxing.length) { r(Wt / 2 - 66, Ht - 40, 132, 14, '#00000055'); pixText(Wt / 2 - 58, Ht - 36, lang === 'pt' ? 'NINGUEM DE FOLGA' : 'NOBODY ON BREAK', '#f4ecd8'); }
|
|
73
73
|
if (!day) { ctx.fillStyle = 'rgba(10,12,40,.25)'; ctx.fillRect(0, HZ, Wt, Ht - HZ); } // evening light on the deck
|
|
74
|
-
viewHits = hits;
|
|
74
|
+
viewHits = hits; if (typeof drawRoomWalker === 'function') drawRoomWalker(t);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
// ---- the server room: two rows of glass racks, a NOC wall with live numbers, cold aisle, cable trays ----
|
|
@@ -118,7 +118,7 @@ function drawServers(t) {
|
|
|
118
118
|
r(14, 160, 8, 18, '#e43b44'); r(15, 156, 6, 4, '#2a2a30'); r(16, 164, 4, 6, '#ffffff');
|
|
119
119
|
for (let x = 0; x < Wt; x += 16) r(x, Ht - 6, 8, 6, '#ffd84d'), r(x + 8, Ht - 6, 8, 6, '#1b1622');
|
|
120
120
|
if (!people.length) pixText(Wt / 2 - 30, 200, 'NO SESSIONS', '#c0cbdc');
|
|
121
|
-
viewHits = hits;
|
|
121
|
+
viewHits = hits; if (typeof drawRoomWalker === 'function') drawRoomWalker(t);
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
// ---- the elevator panel: floors, the terrace, the server room, the trophy room ----
|
|
@@ -132,6 +132,7 @@ function openElevator() {
|
|
|
132
132
|
${btn('data-go="trophy"', E.trophy, viewName === 'trophy')}
|
|
133
133
|
${floors.map((f, i) => btn(`data-go-floor="${i}"`, floors.length > 1 ? E.floor(i + 1) : E.office, !trophyView && floor === i)).reverse().join('')}
|
|
134
134
|
${btn('data-go="servers"', E.servers, viewName === 'servers')}
|
|
135
|
+
|
|
135
136
|
</div></div></div>`;
|
|
136
137
|
liftEl.hidden = false; achBump('lifts');
|
|
137
138
|
}
|
package/public/style.css
CHANGED
|
@@ -996,3 +996,15 @@ body.tv .app-ask { display: none; }
|
|
|
996
996
|
.watch-form { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
|
|
997
997
|
.watch-form select, .watch-form input { font: 13px/1.3 system-ui, -apple-system, sans-serif; padding: 6px 8px; border: 2px solid var(--line); border-radius: 6px; background: var(--bg); color: var(--ink); }
|
|
998
998
|
.watch-form input[data-w-file] { min-width: 280px; }
|
|
999
|
+
/* the ☰ menu: two columns on a wide window, a little tighter, and it scrolls instead of running off the screen */
|
|
1000
|
+
.tools-menu:not([hidden]) { overflow-y: auto; overscroll-behavior: contain; }
|
|
1001
|
+
.tools-menu .icon-btn { height: 30px; }
|
|
1002
|
+
/* three groups side by side on a wide window, stacked on a narrow one */
|
|
1003
|
+
.tools-menu:not([hidden]) { padding: 10px; gap: 0; }
|
|
1004
|
+
.tools-menu .menu-sec { display: flex; flex-direction: column; gap: 1px; min-width: 210px; }
|
|
1005
|
+
.tools-menu .menu-sec + .menu-sec { border-top: 2px dashed #332f37; margin-top: 8px; padding-top: 8px; }
|
|
1006
|
+
.tools-menu .menu-h { font: 700 10px/1 var(--pix); letter-spacing: .08em; text-transform: uppercase; color: #6f6577; padding: 4px 10px 8px; }
|
|
1007
|
+
@media (min-width: 900px) {
|
|
1008
|
+
.tools-menu:not([hidden]) { display: grid; grid-template-columns: repeat(3, minmax(200px, 1fr)); column-gap: 10px; }
|
|
1009
|
+
.tools-menu .menu-sec + .menu-sec { border-top: 0; margin-top: 0; padding-top: 0; border-left: 2px dashed #332f37; padding-left: 10px; }
|
|
1010
|
+
}
|
package/public/tray.png
ADDED
|
Binary file
|
package/src/autostart.js
CHANGED
|
@@ -59,7 +59,8 @@ function enable(mode) {
|
|
|
59
59
|
}
|
|
60
60
|
function disable() {
|
|
61
61
|
const e = entry(command('npx'));
|
|
62
|
-
|
|
62
|
+
// stop the running one too (the tests skip this: the label is the real one, and they must never stop your office)
|
|
63
|
+
if (process.platform === 'darwin' && !process.env.COWORKING_TEST) try { execFileSync('launchctl', ['bootout', `gui/${process.getuid()}/${LABEL}`], { stdio: 'ignore' }); } catch {}
|
|
63
64
|
try { fs.unlinkSync(e.file); } catch {}
|
|
64
65
|
return { ok: true };
|
|
65
66
|
}
|
package/src/menubar.js
CHANGED
|
@@ -11,7 +11,7 @@ const { spawn, spawnSync } = require('child_process');
|
|
|
11
11
|
|
|
12
12
|
const DIR = () => path.join(os.homedir(), '.config', 'coworking-agents');
|
|
13
13
|
const FILE = () => path.join(DIR(), 'menubar.json');
|
|
14
|
-
const ICON = path.join(__dirname, '..', 'public', '
|
|
14
|
+
const ICON = path.join(__dirname, '..', 'public', 'tray.png'); // the small face reads best at menu bar size
|
|
15
15
|
let child = null, kind = null, base = '', token = '', lastBody = '', lastTip = '', seen = null, notes = [];
|
|
16
16
|
|
|
17
17
|
const has = cmd => { try { return spawnSync('which', [cmd], { stdio: 'ignore' }).status === 0; } catch { return false; } };
|
package/src/server.js
CHANGED
|
@@ -126,7 +126,7 @@ function start({ port = 4777, host = '127.0.0.1', privacy = false, interval = 10
|
|
|
126
126
|
return res.end();
|
|
127
127
|
}
|
|
128
128
|
// icons are public: browsers fetch them without cookies and cache the 401 otherwise
|
|
129
|
-
const PUBLIC_ICONS = new Set(['/favicon.ico', '/favicon.png', '/
|
|
129
|
+
const PUBLIC_ICONS = new Set(['/favicon.ico', '/favicon.png', '/apple-touch-icon.png', '/logo.svg', '/manifest.webmanifest', '/icon-192.png', '/icon-512.png']); // no data in any of them
|
|
130
130
|
if (!PUBLIC_ICONS.has(url.pathname) && !sameSecret(cookie(req, 'cw'), token)) return send(res, 401, 'text/html; charset=utf-8', LOCKED);
|
|
131
131
|
|
|
132
132
|
if (url.pathname === '/events') {
|
|
@@ -333,7 +333,7 @@ function start({ port = 4777, host = '127.0.0.1', privacy = false, interval = 10
|
|
|
333
333
|
if (url.pathname === '/api/state') return send(res, 200, 'application/json', safeSnapshot());
|
|
334
334
|
if (url.pathname === '/api/ping') return send(res, 200, 'application/json', '{"ok":true}');
|
|
335
335
|
// the phone link and its QR code, only to this machine (the phone server itself never serves this)
|
|
336
|
-
if (url.pathname === '/api/phone') return send(res, 200, 'application/json', JSON.stringify(phoneInfo ? { on: true, url: phoneInfo.url, ipUrl: phoneInfo.ipUrl, qr: require('./qr').encode(phoneInfo.url).map(r => r.map(x => (x ? 1 : 0)).join('')), visit: visitNow && visitNow.until > Date.now() ? { ...visitNow, qr: require('./qr').encode(visitNow.url).map(r => r.map(x => (x ? 1 : 0)).join('')) } : null } : { on: false, reason: phoneReason }));
|
|
336
|
+
if (url.pathname === '/api/phone') return send(res, 200, 'application/json', JSON.stringify(phoneInfo ? { on: true, url: phoneInfo.url, ipUrl: phoneInfo.ipUrl, qr: require('./qr').encode(phoneInfo.url).map(r => r.map(x => (x ? 1 : 0)).join('')), visit: visitNow && visitNow.until > Date.now() ? { ...visitNow, qr: require('./qr').encode(visitNow.url).map(r => r.map(x => (x ? 1 : 0)).join('')) } : null } : { on: false, reason: phoneReason, always: !!tasks.list().settings.phone }));
|
|
337
337
|
if (url.pathname === '/api/appinstall' && req.method === 'GET') return send(res, 200, 'application/json', JSON.stringify({ on: require('./appinstall').installed(), platform: process.platform, demo }));
|
|
338
338
|
if (url.pathname === '/api/appinstall' && req.method === 'POST') { // create or remove the app on this computer
|
|
339
339
|
const origin = req.headers.origin || '';
|
|
@@ -361,6 +361,21 @@ function start({ port = 4777, host = '127.0.0.1', privacy = false, interval = 10
|
|
|
361
361
|
req.on('end', () => { let m = {}; try { m = JSON.parse(body); } catch {} if (demo) return send(res, 200, 'application/json', '{"ok":false,"reason":"demo"}'); const on = !!m.on; tasks.op({ op: 'settings', menubar: on }); const r = on ? mb.start({ base: baseUrl, token }) : (mb.stop(), { ok: true }); if (on) mb.write(snapshot({ privacy }), mbExtra()); send(res, 200, 'application/json', JSON.stringify({ ...r, on: mb.running() })); });
|
|
362
362
|
return;
|
|
363
363
|
}
|
|
364
|
+
if (url.pathname === '/api/phone-toggle' && req.method === 'POST') { // turn the phone panel on or off now, remembered for the next start
|
|
365
|
+
const origin = req.headers.origin || '';
|
|
366
|
+
if (req.headers['x-coworking'] !== '1' || (origin && !LOCAL_ORIGIN.test(origin))) { res.writeHead(403); return res.end(); }
|
|
367
|
+
let body = '';
|
|
368
|
+
req.on('data', c => { body += c; if (body.length > 1e3) req.destroy(); });
|
|
369
|
+
req.on('end', async () => {
|
|
370
|
+
let m = {}; try { m = JSON.parse(body); } catch {}
|
|
371
|
+
if (demo) return send(res, 200, 'application/json', '{"ok":false,"reason":"demo"}');
|
|
372
|
+
const on = !!m.on; tasks.op({ op: 'settings', phone: on });
|
|
373
|
+
if (on) return send(res, 200, 'application/json', JSON.stringify(await startPhoneNow()));
|
|
374
|
+
if (phoneInfo) { phoneInfo.close(); phoneInfo = null; } phoneReason = 'off'; visitNow = null;
|
|
375
|
+
send(res, 200, 'application/json', '{"ok":true}');
|
|
376
|
+
});
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
364
379
|
if (url.pathname === '/api/phone-rotate' && req.method === 'POST') { // a new phone code: every phone has to scan the QR again
|
|
365
380
|
const origin = req.headers.origin || '';
|
|
366
381
|
if (req.headers['x-coworking'] !== '1' || (origin && !LOCAL_ORIGIN.test(origin))) { res.writeHead(403); return res.end(); }
|
|
@@ -398,15 +413,23 @@ function start({ port = 4777, host = '127.0.0.1', privacy = false, interval = 10
|
|
|
398
413
|
});
|
|
399
414
|
}
|
|
400
415
|
|
|
416
|
+
// opt-in: a separate, restricted server on the LAN address (see src/phone.js); its code is kept like the office's, and its
|
|
417
|
+
// port is always the same (not the office's + 1), so a home-screen icon keeps working whatever port the office got
|
|
418
|
+
const PHONE_PORT = Number(process.env.COWORKING_PHONE_PORT) || 4778;
|
|
419
|
+
async function startPhoneNow() {
|
|
420
|
+
if (phoneInfo) return { ok: true };
|
|
421
|
+
try {
|
|
422
|
+
const r = await require('./phone').startPhone({ port: port ? PHONE_PORT : 0, privacy, demo, snapshot, tasks, sendText, people: () => real().people, token: keepToken ? phoneToken : '', onToken: onPhoneToken, extra: phoneUsage });
|
|
423
|
+
if (r.ok) { phoneInfo = r; phoneReason = ''; const t = r.url.split('t=')[1]; if (keepToken && onPhoneToken && t !== phoneToken) { phoneToken = t; onPhoneToken(t); } } else phoneReason = r.reason;
|
|
424
|
+
return r.ok ? { ok: true } : { ok: false, reason: r.reason };
|
|
425
|
+
} catch (e) { phoneReason = e.code === 'EADDRINUSE' ? 'port' : 'error'; return { ok: false, reason: phoneReason }; }
|
|
426
|
+
}
|
|
401
427
|
return new Promise((resolve, reject) => {
|
|
402
428
|
server.once('error', reject);
|
|
403
429
|
server.listen(port, host, async () => {
|
|
404
430
|
const base = `http://${host === '0.0.0.0' ? 'localhost' : host}:${server.address().port}`; baseUrl = base;
|
|
405
431
|
if (!demo && (menubar || tasks.list().settings.menubar)) { mb.start({ base, token }); try { mb.write(snapshot({ privacy }), mbExtra()); } catch {} }
|
|
406
|
-
if (phone)
|
|
407
|
-
try { const r = await require('./phone').startPhone({ port: port ? server.address().port + 1 : 0, privacy, demo, snapshot, tasks, sendText, people: () => real().people, token: keepToken ? phoneToken : '', onToken: onPhoneToken, extra: phoneUsage }); if (r.ok) phoneInfo = r; else phoneReason = r.reason; }
|
|
408
|
-
catch (e) { phoneReason = e.code === 'EADDRINUSE' ? 'port' : 'error'; }
|
|
409
|
-
}
|
|
432
|
+
if (phone || (!demo && tasks.list().settings.phone)) await startPhoneNow(); // --phone, or turned on once in the office (remembered)
|
|
410
433
|
resolve({ server, token, base, url: `${base}/?t=${token}`, phone: phoneInfo, phoneReason, close: () => { clearInterval(timer); clearInterval(mbTimer); clearInterval(paceTimer); mb.stop(); for (const c of clients) c.destroy(); server.close(); if (phoneInfo) phoneInfo.close(); } });
|
|
411
434
|
});
|
|
412
435
|
});
|
package/src/tasks.js
CHANGED
|
@@ -202,6 +202,7 @@ function op(m = {}) {
|
|
|
202
202
|
if (m.autoReview !== undefined) settings.autoReview = !!m.autoReview;
|
|
203
203
|
if (m.monthGoal !== undefined) settings.monthGoal = Math.max(0, Math.min(1e6, Number(m.monthGoal) || 0));
|
|
204
204
|
if (m.menubar !== undefined) settings.menubar = !!m.menubar;
|
|
205
|
+
if (m.phone !== undefined) settings.phone = !!m.phone;
|
|
205
206
|
if (['app', 'browser', ''].includes(m.appChoice)) settings.appChoice = m.appChoice;
|
|
206
207
|
if (m.quiet && typeof m.quiet === 'object') settings.quiet = { on: !!m.quiet.on, from: /^([01]\d|2[0-3]):[0-5]\d$/.test(m.quiet.from) ? m.quiet.from : '22:00', to: /^([01]\d|2[0-3]):[0-5]\d$/.test(m.quiet.to) ? m.quiet.to : '08:00' };
|
|
207
208
|
if (m.dayBudget !== undefined) settings.dayBudget = Math.max(0, Math.min(1e5, Number(m.dayBudget) || 0));
|
package/public/favicon.svg
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" shape-rendering="crispEdges"><rect x="2" y="0" width="12" height="1" fill="#1b1622"/><rect x="1" y="1" width="1" height="1" fill="#1b1622"/><rect x="2" y="1" width="12" height="1" fill="#f4dfc6"/><rect x="14" y="1" width="1" height="1" fill="#1b1622"/><rect x="0" y="2" width="1" height="1" fill="#1b1622"/><rect x="1" y="2" width="2" height="1" fill="#f4dfc6"/><rect x="3" y="2" width="10" height="1" fill="#1b1622"/><rect x="13" y="2" width="2" height="1" fill="#f4dfc6"/><rect x="15" y="2" width="1" height="1" fill="#1b1622"/><rect x="0" y="3" width="1" height="1" fill="#1b1622"/><rect x="1" y="3" width="1" height="1" fill="#f4dfc6"/><rect x="2" y="3" width="1" height="1" fill="#1b1622"/><rect x="3" y="3" width="10" height="1" fill="#2b2336"/><rect x="13" y="3" width="1" height="1" fill="#1b1622"/><rect x="14" y="3" width="1" height="1" fill="#f4dfc6"/><rect x="15" y="3" width="1" height="1" fill="#1b1622"/><rect x="0" y="4" width="1" height="1" fill="#1b1622"/><rect x="1" y="4" width="1" height="1" fill="#f4dfc6"/><rect x="2" y="4" width="1" height="1" fill="#1b1622"/><rect x="3" y="4" width="3" height="1" fill="#2b2336"/><rect x="6" y="4" width="1" height="1" fill="#2ce8f5"/><rect x="7" y="4" width="2" height="1" fill="#2b2336"/><rect x="9" y="4" width="1" height="1" fill="#ff5fd2"/><rect x="10" y="4" width="3" height="1" fill="#2b2336"/><rect x="13" y="4" width="1" height="1" fill="#1b1622"/><rect x="14" y="4" width="1" height="1" fill="#f4dfc6"/><rect x="15" y="4" width="1" height="1" fill="#1b1622"/><rect x="0" y="5" width="1" height="1" fill="#1b1622"/><rect x="1" y="5" width="1" height="1" fill="#f4dfc6"/><rect x="2" y="5" width="1" height="1" fill="#1b1622"/><rect x="3" y="5" width="2" height="1" fill="#2b2336"/><rect x="5" y="5" width="1" height="1" fill="#2ce8f5"/><rect x="6" y="5" width="4" height="1" fill="#2b2336"/><rect x="10" y="5" width="1" height="1" fill="#ff5fd2"/><rect x="11" y="5" width="2" height="1" fill="#2b2336"/><rect x="13" y="5" width="1" height="1" fill="#1b1622"/><rect x="14" y="5" width="1" height="1" fill="#f4dfc6"/><rect x="15" y="5" width="1" height="1" fill="#1b1622"/><rect x="0" y="6" width="1" height="1" fill="#1b1622"/><rect x="1" y="6" width="1" height="1" fill="#f4dfc6"/><rect x="2" y="6" width="1" height="1" fill="#1b1622"/><rect x="3" y="6" width="1" height="1" fill="#2b2336"/><rect x="4" y="6" width="1" height="1" fill="#2ce8f5"/><rect x="5" y="6" width="6" height="1" fill="#2b2336"/><rect x="11" y="6" width="1" height="1" fill="#ff5fd2"/><rect x="12" y="6" width="1" height="1" fill="#2b2336"/><rect x="13" y="6" width="1" height="1" fill="#1b1622"/><rect x="14" y="6" width="1" height="1" fill="#f4dfc6"/><rect x="15" y="6" width="1" height="1" fill="#1b1622"/><rect x="0" y="7" width="1" height="1" fill="#1b1622"/><rect x="1" y="7" width="1" height="1" fill="#f4dfc6"/><rect x="2" y="7" width="1" height="1" fill="#1b1622"/><rect x="3" y="7" width="2" height="1" fill="#2b2336"/><rect x="5" y="7" width="1" height="1" fill="#2ce8f5"/><rect x="6" y="7" width="4" height="1" fill="#2b2336"/><rect x="10" y="7" width="1" height="1" fill="#ff5fd2"/><rect x="11" y="7" width="2" height="1" fill="#2b2336"/><rect x="13" y="7" width="1" height="1" fill="#1b1622"/><rect x="14" y="7" width="1" height="1" fill="#f4dfc6"/><rect x="15" y="7" width="1" height="1" fill="#1b1622"/><rect x="0" y="8" width="1" height="1" fill="#1b1622"/><rect x="1" y="8" width="1" height="1" fill="#f4dfc6"/><rect x="2" y="8" width="1" height="1" fill="#1b1622"/><rect x="3" y="8" width="3" height="1" fill="#2b2336"/><rect x="6" y="8" width="1" height="1" fill="#2ce8f5"/><rect x="7" y="8" width="2" height="1" fill="#2b2336"/><rect x="9" y="8" width="1" height="1" fill="#ff5fd2"/><rect x="10" y="8" width="3" height="1" fill="#2b2336"/><rect x="13" y="8" width="1" height="1" fill="#1b1622"/><rect x="14" y="8" width="1" height="1" fill="#f4dfc6"/><rect x="15" y="8" width="1" height="1" fill="#1b1622"/><rect x="0" y="9" width="1" height="1" fill="#1b1622"/><rect x="1" y="9" width="1" height="1" fill="#f4dfc6"/><rect x="2" y="9" width="1" height="1" fill="#1b1622"/><rect x="3" y="9" width="10" height="1" fill="#2b2336"/><rect x="13" y="9" width="1" height="1" fill="#1b1622"/><rect x="14" y="9" width="1" height="1" fill="#f4dfc6"/><rect x="15" y="9" width="1" height="1" fill="#1b1622"/><rect x="0" y="10" width="1" height="1" fill="#1b1622"/><rect x="1" y="10" width="2" height="1" fill="#f4dfc6"/><rect x="3" y="10" width="10" height="1" fill="#1b1622"/><rect x="13" y="10" width="2" height="1" fill="#f4dfc6"/><rect x="15" y="10" width="1" height="1" fill="#1b1622"/><rect x="1" y="11" width="1" height="1" fill="#1b1622"/><rect x="2" y="11" width="12" height="1" fill="#f4dfc6"/><rect x="14" y="11" width="1" height="1" fill="#1b1622"/><rect x="2" y="12" width="2" height="1" fill="#1b1622"/><rect x="4" y="12" width="2" height="1" fill="#f4dfc6"/><rect x="6" y="12" width="8" height="1" fill="#1b1622"/><rect x="3" y="13" width="1" height="1" fill="#1b1622"/><rect x="4" y="13" width="1" height="1" fill="#f4dfc6"/><rect x="5" y="13" width="1" height="1" fill="#1b1622"/><rect x="3" y="14" width="2" height="1" fill="#1b1622"/></svg>
|