@patimweb/pi-zep 1.0.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/.node-version ADDED
@@ -0,0 +1 @@
1
+ 26.5.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Patrick Weppelmann
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,179 @@
1
+ # pi-zep
2
+
3
+ A [pi](https://github.com/earendil-works/pi) extension that books project times
4
+ (**Projektzeiten**) in ZEP over plain HTTP — no browser, and no paid ZEP REST API.
5
+
6
+ ## Why
7
+
8
+ ZEP's official REST API is a paid add-on and, at least up to ZEP v7.13.88,
9
+ cannot create a Projektzeit booking at all. The web UI itself talks to a plain
10
+ PHP endpoint that can, and it needs no API key. This extension speaks that
11
+ endpoint directly.
12
+
13
+ ## How it works
14
+
15
+ The whole protocol was reverse-engineered from the web client. Three parts:
16
+
17
+ | Step | Request |
18
+ |------|---------|
19
+ | Login | `POST {root}/view/login.php` with `userid`, `password`, `login` → `302` whose `Location` carries the new `CLIENTSESSID` |
20
+ | Page / token | `GET {root}/view/index.php?CLIENTSESSID=…&menu=ProjektzeitVerwaltungMgr&action=save` → HTML containing `var requesttoken = '…'` |
21
+ | Booking | `POST {root}/view/ajax.php?CLIENTSESSID=…&pageContextId=Projektzeiten&mgr=ProjektzeitMgr&mgrId=3&action=save&JS_ENV_VAR_isFast=true&JS_ENV_VAR_locale=de&json=true&ajax=1` |
22
+
23
+ `{root}` is `<baseUrl>/<mandant>`, e.g. `https://zep-online.de/zepneoimpulse`.
24
+
25
+ The booking request needs:
26
+
27
+ * the **`x-requesttoken`** header (mandatory), plus `requesttoken` in the body,
28
+ * the **`PHPSESSID`** cookie **and** the `CLIENTSESSID` query parameter — neither alone is accepted,
29
+ * a form-urlencoded body with `datum`, `von`, `bis`, `dauer`, `projektId`, `vorgangId`, `taetigkeit`, `bemerkung`, `ort`, …
30
+
31
+ ### Three things that bite
32
+
33
+ 1. **The token rotates.** Every response — including errors — hands out the next
34
+ `requesttoken`. This client adopts it automatically. Reusing an old token is
35
+ not merely rejected: it makes the server **log the session out**
36
+ (`msg=555001` / `555003` / `555009`) and you have to log in again.
37
+ 2. **Never send an ajax call without a token.** The client fetches the page first
38
+ when it does not hold a fresh token, so it can never fall into (1).
39
+ 3. **ZEP locks the account after 5 failed password attempts.** The client
40
+ therefore caches the session in `~/.pi/zep-session.json` and only logs in
41
+ again when the cached session stops working. A failed login reports the
42
+ remaining attempts (`noch=N`).
43
+
44
+ Reads and deletes use the same envelope:
45
+
46
+ ```
47
+ GET …/ajax.php?…&mgr=ProjektzeitMgr&mgrId=0&action=setKw&kwDate=<monday> # week overview
48
+ GET …/ajax.php?…&mgr=ProjektzeitMgr&mgrId=0&action=delete&objectId=<id>&kwDate=<monday>&userId=<user>
49
+ POST …/ajax.php?…&mgrId=3&action=save&formrefresh=true&formrefreshtrigger=projektId # re-render the form for another project
50
+ ```
51
+
52
+ The last one is the UI's own mechanism for changing the project: the Vorgang
53
+ list is rendered server-side, so it only contains the Vorgänge of the project
54
+ the page was rendered for. Posting `formrefresh=true` returns the new
55
+ `<select id="vorgangId">` without saving anything.
56
+
57
+ ### Response shapes (verified against the live server)
58
+
59
+ | Outcome | `error` | `javascript` | content |
60
+ |---------|---------|--------------|---------|
61
+ | accepted | `null` | `dispatchZepEvent('zepProjektzeitMgrRefresh')` | calendar snippet |
62
+ | rejected | `"error"` | `null` | snippet containing `alert-message` with the reason |
63
+
64
+ **A rejection does not arrive in `javascript`.** It arrives as a `schnipsel`
65
+ whose target is a form message slot:
66
+
67
+ ```html
68
+ <div class="card-alert alert alert-danger">
69
+ <div class="alert-message">Projektzeit Do, 17.09.2026, 08:00 - 08:15 Speichern
70
+ fehlgeschlagen. Diese Zeit kann nicht gebucht werden, da dadurch die Plan
71
+ Stunden des Projekts/Vorgangs überschritten würden. Projektleiter wurden ggf.
72
+ per E-Mail informiert.</div>
73
+ </div>
74
+ ```
75
+
76
+ A client that only inspects `javascript` would report a useless "unspecified
77
+ error" here. `extractSnippetAlert()` reads the real text.
78
+
79
+ Other message texts observed:
80
+
81
+ * `… Tätigkeit co bei Vorgang 90-02 nicht erlaubt.` — the activity is not allowed for that Vorgang (90-02 only accepts `co_nb`).
82
+ * `… Plan Stunden des Projekts/Vorgangs überschritten…` — the Vorgang's planned hours are used up.
83
+
84
+ ### Week table quirks
85
+
86
+ * The `setKw` snippet carries **no `target`** — it has to be recognised by content (`pz_kwnav`, `id="objectId…"`, `class="tag_…"`), never by target.
87
+ * The save response ships a *calendar* snippet targeting `managerdiv_ProjektzeitMgr`, which must not be mistaken for the week table.
88
+ * The ISO date of a day row is in `id="day_YYYY-MM-DD"` (fallback: the `openMahlzeiten(this, 'YYYY-MM-DD')` onclick, then the weekday + `DD.MM.` label).
89
+ * A day row's label is split across `<div>Do</div><div>17.09.</div>`, so plain `innerText` yields `Do17.09.` while tag-stripping yields `Do 17.09.` — parse the latter.
90
+ * Booking rows have 10 cells (`action, von, bis, Dauer, action, Projekt, Vorgang, Tät., fakturierbar, Bemerkung`); the `class="luecke"` spacer rows in between have 1 and must be skipped; the sum row has 3.
91
+
92
+ ## Tools
93
+
94
+ | Tool | Purpose |
95
+ |------|---------|
96
+ | `zep_setup` | Store `userid`, `password`, `mandant` (default `zepneoimpulse`) |
97
+ | `zep_login` | Log in with the stored credentials, cache the session (`force` to ignore the cache) |
98
+ | `zep_status` | Show profiles and verify the live session (`probe` loads the booking form) |
99
+ | `zep_profile` | List / switch / delete profiles, `clear-session` |
100
+ | `zep_projects` | List projects and, per project, its Vorgänge and Tätigkeiten |
101
+ | `zep_week` | Week overview: bookings with row id, times, duration, sum, under-booked days |
102
+ | `zep_book` | Create bookings — one block or an `entries` array; `dryRun` supported |
103
+ | `zep_delete` | Delete a booking by row id |
104
+
105
+ ## Usage
106
+
107
+ ```
108
+ zep_setup name=work userid=patrick.weppelmann password=… mandant=zepneoimpulse
109
+ zep_status probe=true
110
+ zep_projects projekt=Julabo
111
+ zep_week
112
+ zep_book datum=2026-09-17 von=08:00 bis=12:00 projekt=413 vorgang=90-01 taetigkeit=co bemerkung="3447 Ladeanzeige nach Pick"
113
+ zep_book datum=2026-09-17 entries=[{von:"08:00",bis:"12:00",bemerkung:"…"},{von:"13:00",bis:"17:00",bemerkung:"…"}]
114
+ ```
115
+
116
+ `projekt` and `vorgang` accept either an id (`413`, `3284`) or part of the label
117
+ (`Julabo`, `90-01`). A full example, `dryRun` first:
118
+
119
+ ```
120
+ zep_book projekt=Julabo vorgang=90-01 datum=2026-09-17 von=08:00 bis=12:00 dryRun=true
121
+ ```
122
+
123
+ ## Important behaviour
124
+
125
+ * **ZEP rejects rather than overwrites.** A booking that would exceed the
126
+ Vorgang's planned hours comes back as *"Diese Zeit kann nicht gebucht werden,
127
+ da dadurch die Plan Stunden des Projekts/Vorgangs überschritten würden.
128
+ Projektleiter wurden ggf. per E-Mail informiert."* This extension reports that
129
+ text verbatim and never silently retries on another Vorgang.
130
+ * **`fakturierbar` is derived server-side** from project + Vorgang + Tätigkeit.
131
+ The field is `disabled` in the UI and is deliberately not sent.
132
+ * **`jpeg`-free, dependency-light:** the only runtime dependency is `typebox`.
133
+
134
+ ## Verified live
135
+
136
+ The full write cycle was exercised against the real system on 2026-09-17:
137
+ form re-render for another project, a booking created and confirmed in the week
138
+ table, then deleted and confirmed gone. A rejected booking (plan hours
139
+ exhausted) and a rejected Tätigkeit were reproduced as well.
140
+
141
+ What is *not* covered by that test run: a successful password login, because
142
+ that requires the account password. The endpoint, the field names and the
143
+ failure contract (`302 → login.php?…&action=login&msg=1&noch=N`) were verified
144
+ with throwaway credentials; everything after the redirect was verified with a
145
+ real session.
146
+
147
+ ## Files
148
+
149
+ ```
150
+ index.ts extension entry point (tool registration)
151
+ src/types.ts plain data shapes
152
+ src/config.ts profile + session persistence (~/.pi, mode 0600)
153
+ src/clients/zep-client.ts HTTP session handling, booking, week, delete
154
+ src/clients/zep-parse.ts tolerant parsers for ZEP's HTML / ajax JSON
155
+ src/formatting/ domain data -> text
156
+ src/tools/ one tool per file
157
+ tests/ parser + wire-contract tests (stubbed fetch)
158
+ ```
159
+
160
+ ## Development
161
+
162
+ ```bash
163
+ npm install
164
+ npm test # node --test
165
+ npx tsc --noEmit
166
+ ```
167
+
168
+ The wire contract is pinned by `tests/zep-client.test.ts`, which asserts the
169
+ exact query parameters, headers and body fields against the live system's
170
+ observed behaviour.
171
+
172
+ ## Configuration files
173
+
174
+ | File | Content |
175
+ |------|---------|
176
+ | `~/.pi/zep-config.json` | profiles with `userid` / `password` / `mandant` |
177
+ | `~/.pi/zep-session.json` | cached `CLIENTSESSID`, cookies and current `requesttoken` |
178
+
179
+ Both are written with mode `0600`.
package/index.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * pi ZEP Extension
3
+ *
4
+ * A ZEP time-tracking client that talks to the zep-online.de web UI's own
5
+ * PHP endpoint instead of the paid ZEP REST API.
6
+ *
7
+ * Tools:
8
+ * - zep_setup: store userid/password/Mandant
9
+ * - zep_login: log in (password, or adopt a browser tab session)
10
+ * - zep_status: show config, log in, prove the session works
11
+ * - zep_profile: list/switch/delete profiles, clear cached sessions
12
+ * - zep_projects: list projects and their Vorgänge/Tätigkeiten
13
+ * - zep_week: read the Projektzeiten week overview
14
+ * - zep_book: create bookings (one or several blocks)
15
+ * - zep_delete: delete a booking
16
+ *
17
+ * Design:
18
+ * - types.ts plain data shapes
19
+ * - config.ts profile + session persistence (~/.pi)
20
+ * - clients/zep-client.ts HTTP session handling and the booking call
21
+ * - clients/zep-parse.ts tolerant parsers for ZEP's HTML/JSON
22
+ * - formatting/formatters.ts domain data -> text
23
+ * - tools/*.ts one tool per file
24
+ */
25
+
26
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
27
+
28
+ import { loadConfig } from "./src/config.ts";
29
+ import { ZepSetupTool } from "./src/tools/zep-setup.ts";
30
+ import { ZepLoginTool } from "./src/tools/zep-login.ts";
31
+ import { ZepStatusTool } from "./src/tools/zep-status.ts";
32
+ import { ZepProfileTool } from "./src/tools/zep-profile.ts";
33
+ import { ZepProjectsTool } from "./src/tools/zep-projects.ts";
34
+ import { ZepWeekTool } from "./src/tools/zep-week.ts";
35
+ import { ZepBookTool } from "./src/tools/zep-book.ts";
36
+ import { ZepDeleteTool } from "./src/tools/zep-delete.ts";
37
+
38
+ export default function (pi: ExtensionAPI) {
39
+ loadConfig();
40
+
41
+ pi.registerTool(ZepSetupTool);
42
+ pi.registerTool(ZepLoginTool);
43
+ pi.registerTool(ZepStatusTool);
44
+ pi.registerTool(ZepProfileTool);
45
+ pi.registerTool(ZepProjectsTool);
46
+ pi.registerTool(ZepWeekTool);
47
+ pi.registerTool(ZepBookTool);
48
+ pi.registerTool(ZepDeleteTool);
49
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@patimweb/pi-zep",
3
+ "version": "1.0.0",
4
+ "description": "ZEP time-tracking for pi coding agent — book project times (Projektzeiten) on zep-online.de over plain HTTP, without the paid ZEP API",
5
+ "keywords": [
6
+ "pi-package",
7
+ "zep",
8
+ "zep-online",
9
+ "time-tracking",
10
+ "zeiterfassung",
11
+ "projektzeiten",
12
+ "pi-extension"
13
+ ],
14
+ "author": "Patrick Weppelmann",
15
+ "license": "MIT",
16
+ "type": "module",
17
+ "engines": {
18
+ "node": ">=26.0.0"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/Smotherer007/pi-zep.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/Smotherer007/pi-zep/issues"
26
+ },
27
+ "scripts": {
28
+ "build": "echo 'No build step defined'",
29
+ "test": "node --test 'tests/**/*.test.ts'",
30
+ "test:watch": "node --test --watch 'tests/**/*.test.ts'",
31
+ "typecheck": "tsc --noEmit",
32
+ "live-check": "node scripts/live-check.ts"
33
+ },
34
+ "dependencies": {
35
+ "typebox": "^1.2.8"
36
+ },
37
+ "devDependencies": {
38
+ "@earendil-works/pi-coding-agent": "0.85.1",
39
+ "@semantic-release/git": "^10.0.0",
40
+ "@types/node": "^26.5.1",
41
+ "semantic-release": "^25.0.0",
42
+ "typescript": "7.0.2"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "pi": {
48
+ "extensions": [
49
+ "./index.ts"
50
+ ],
51
+ "image": "https://raw.githubusercontent.com/Smotherer007/pi-zep/main/screenshot.png"
52
+ }
53
+ }