1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
|
# Bitweaver-lsces Project Context
## Stack
- PHP 8.6 / Firebird 5 / adodb / Smarty
- One repo per package (bitweaver-lsces organisation)
- webtrees used as an additional package with illuminate-firebird providing Firebird DB driver (personal fork)
- externals/ holds actively-developed third-party dependencies
- No npm, no Node.js — ever
## Philosophy
- Fix root causes, not symptoms
- Self-hosted, document everything
- Internal stays internal
- Clean diffs matter more than PSR compliance for its own sake
## Deploy Path
Code is edited and tested in `/srv/website/bitweaver5` (xdebug available).
Each bitweaver package is a self-contained directory under `bitweaver5/`, with its
own **individual git repo** under `~/Development/bitweaver-lsces/<package>/`.
Proven changes are copied to the matching package repo — normally by CC directly,
with BeyondCompare used for manual review when needed.
Deploy steps:
1. Copy changed files to `~/Development/bitweaver-lsces/<package>/`
2. Commit in that package's git repo
3. `git push` — updates GitHub (publish-only, not part of the deploy chain)
4. `/etc/webstack/scripts/server-pull-all.sh <package>` — pulls to srv9 and srv10 from the desktop's local copy
Servers do NOT pull from GitHub — they pull from the desktop's local copy.
After pulling on a server, clear the Smarty template cache and restart php-fpm.
Server configuration (nginx, PHP, Firebird) lives in `/etc/webstack/` — a separate
git repo replicated across desktop, srv9, and srv10. Never look in `/etc/nginx` or
`/etc/php*` — they are not the source of truth.
`/etc/webstack` pushes to the bare repo at `/srv/git/webstack.git` (not GitHub) — always
`git push` there before pulling on servers, otherwise servers see stale state.
> NOTE: `/srv/git/bitweaver` → nginx is not yet wired up (infrastructure thread — do not action).
Test all changes on srv9 first (including `zypper dup` before system updates).
srv10 is production — only gets changes proven on srv9.
## Scope
Focus exclusively on Bitweaver code in the current working package.
Do not roam into other packages unless explicitly asked.
**Ignore completely:**
- `webtrees/` — separate application, has its own work thread
- `vendor/` — composer-managed, do not touch
- `externals/` — third-party libs, treat as read-only unless explicitly asked
- `~/Development/` — not relevant to in-place editing
- `/etc/nginx`, `/etc/php*` — not the source of truth, see /srv/webstack
## Patterns & Conventions
- Tabs, not spaces
- Short array syntax `[]` throughout
- Double quotes preferred (interpolation available)
- adodb for all DB access — Firebird 5
- No composer autoload changes without explicit discussion
- No framework magic — keep it explicit and traceable
## PHP-CS-Fixer
Config at project root. Rules in use:
- `array_syntax` short
- `indentation_type` (tabs)
- `no_trailing_whitespace`, `no_whitespace_in_blank_line`
- `ternary_to_null_coalescing` — `isset($x) ? $x : $y` → `$x ?? $y`
- `get_class_to_class_keyword` — `get_class($x)` → `$x::class`
- `modernize_types_casting` — `intval()` → `(int)` etc (risky — verify output)
- `use_arrow_functions` — eligible closures → `fn()` (risky — verify scoping)
- `no_unused_imports`, `no_useless_else`, `no_useless_return`
- `trailing_comma_in_multiline`
- `blank_line_after_namespace`
- `no_extra_blank_lines`
**Removed (noise, no value):**
- `single_quote` — double quotes are fine, interpolation is useful
- `ordered_imports` — pure pedantry, clutters diffs
Fixer has been run across 741 files. Any future run should use `--dry-run --diff` first.
## Current Work Threads
stock and contact packages
### Active
- hauth/facebook login — keep option open; not culling
- JavaScript tidy — other areas beyond util/javascript
### Pending
- webtrees data/images separation (buried in app, needs separating like bitweaver storage)
- externals/composer halfway-house — ckeditor and util-type dependencies
- `/srv/git/bitweaver` → nginx wiring (infrastructure thread, separate from code work)
- phpsurgery theme: currently using BlueSky — needs a proper site-specific theme tidy
- contact + stock: audit `expunge` permission gating on xref item templates — some delete
actions (expunge=1/-1) push to history rather than hard-delete, so should gate on
`_update` not `_expunge`. Trace `edit_xref.php` for each `expunge` value: if it sets a
deleted flag rather than `DELETE FROM`, move to `p_contact_update` / `p_stock_update`.
Stock xref deletes currently gate on `$xrefAllowEdit` only — need `p_stock_expunge` added.
- icon set — current tango icons for stock/contact menus are placeholder; proper custom
icons needed for assemblies, components, movements, requisitions, add-person, add-business
## Bitweaver Structure Notes
### Permission / Role system
Default role_id values (ANONYMOUS_TEAM_ID = -1):
- `1` Administrators — perm_level `admin`
- `2` Editors — perm_level `editors`
- `3` Registered — perm_level `registered`
- `-1` Anonymous — perm_level `basic`
Permissions assigned in `*/admin/schema_inc.php`. Role assignments stored in
`users_role_permissions`. When writing xref role-filter queries, guard `mRoles`
with `array_keys($gBitUser->mRoles ?? []) ?: [-1]` — Firebird rejects empty `IN()`.
### Session / Auth cookie
Cookie name = `bit-user-{site_title_stripped}` (lowercase, alphanum only).
Login stores PHP `session_id()` in `users_cnxn.cookie` mapped to `user_id`.
Subsequent requests look up the cookie value in `users_cnxn` to identify the user —
this is separate from PHP's own session mechanism (though they share the same cookie name).
### Top navbar menu
Each package self-registers via `$gBitSystem->registerAppMenu()` in its `bit_setup_inc.php`.
Rendered by `kernel/templates/top_bar.tpl` — no built-in role gate at the dropdown level.
Config switches (stored in kernel_config, set via Themes > Admin > Menus):
- `menu_$pkg = 'n'` — disable dropdown for all users
- `${pkg}_menu_text` — custom dropdown label
- `${pkg}_menu_position` — sort position
For role-based visibility, override `top_bar.tpl` in `config/themes/merg/kernel/`.
### CSS load order
`BitThemes::loadStyleData()` loads CSS in this order:
1. Package CSS (around position 300) — each package's `html_head_inc.tpl`; also
`config/css/config.css` (old duplicate of themes/css/config.css — see note below)
2. `themes/css/config.css` — position 300 (default); canonical floaticon/icon/actionicon rules
3. Style CSS (`getStyleCssFile()`, position 998) — the active theme's main CSS
4. Browser CSS (`getBrowserStyleCssFile()`, position 999)
Site-specific CSS lives in `/etc/webstack/domains/{site}/themes/{site}/{site}.css` and is
the active theme file for that site (position 998). Site theme images go in
`themes/{site}/images/` within that domain dir — referenced via `{$gBitThemes->getStyleUrl()}images/`.
**`.floaticon` / `.icon` audit note** — `themes/css/config.css` defines `.floaticon { float:right }`
at position 300. Site theme CSS at position 998 **wins** over this. If a site theme has
`.icon { float:left }` (common in older themes for sprite icon layout), it breaks `.floaticon`
by causing child icons to float left and collapse the container. **Fix**: strip the bare
`.icon { float:left }` from the site CSS — do not scope it or patch it elsewhere.
`config/css/config.css` is a stale duplicate of `themes/css/config.css` (slightly older,
different padding direction); all sites should be audited to confirm it is not causing
conflicts. `themes/css/config.css` is the canonical source.
### Smarty notes
- `{tr}...{/tr}` for translation in templates; `KernelTools::tra()` in PHP
- `tra` is NOT a Smarty modifier — `"string"|tra` will throw a compiler error
- `{form}` block plugin auto-injects `<input type="hidden" name="tk">` (CSRF ticket)
- **`form-search` hides Bootstrap submit buttons** — Bootstrap 2 `.form-search` suppresses
the submit control. Use `class="minifind"` alone on floaticon filter forms; never add `form-search`.
- `{strip}` removes whitespace between HTML tags; keep `•` separators inside
valid `<li>` elements to avoid detachment
- Per-site footer scripts belong in `kernel/footer_inc.tpl`, NOT `kernel/footer.tpl`.
`footer_inc.tpl` is picked up by the `mAuxFiles` loop in `html.tpl` reliably.
`footer.tpl` as a theme override only loads if the active style matches exactly —
fragile and easy to miss.
### Module / Layout system
Modules are placed in layout areas via the `THEMES_LAYOUTS` table:
- `LAYOUT_AREA` — column position: `t` (top), `l` (left), `r` (right), `b` (bottom)
- `POS` — sort order within the column
- `MODULE_RSRC` — the Smarty template to render, e.g. `bitpackage:kernel/mod_top_banner.tpl`
- `ROLES` — role_id that can see this module (empty = all users); use `-1` for anonymous-only
The 't' column is rendered inside `<header id="bw-main-header">` via `displayLayoutColumn('t')`.
Bootstrap grid columns (`col-sm-*`) inside modules MUST be wrapped in a `clearfix` parent;
without it, subsequent modules in the same column float up and overlay earlier ones.
Managed via Admin > Themes > Layout. Template includes resolve via `bitpackage:` prefix.
Column visibility is controlled by feature flags in `kernel_config` checked in `BitThemes::loadLayout()`:
- `{display_mode}_hide_{area}_col` (e.g. `edit_hide_left_col`)
- `{package}_hide_{area}_col`
- `{package}_{display_mode}_hide_{area}_col`
The old hardcoded `display_mode != 'edit'` guard in `html.tpl` has been removed — columns now always
follow these flags. All flags off = columns show on all pages including edit pages.
### Site-specific theme overrides (/etc/webstack/domains)
Each vhosted site has its theme overrides at `/etc/webstack/domains/{site}/themes/{site}/`.
These are symlinked into `bitweaver5/config/themes/{site}` — e.g.:
```
/srv/website/bitweaver5/config/themes/merg -> /etc/webstack/domains/merg/themes/merg
```
Typical contents: `kernel/` (top_bar.tpl, top_banner.tpl, etc.), `images/`, site CSS, favicon.
Any template in this path overrides the package default via Smarty's `bitpackage:` resource lookup.
`config/themes/medw` and `config/themes/merg` are both symlinks — never edit the config/themes
path directly; edit the source in `/etc/webstack/domains/`.
### Contact package — Person vs Business model
Two distinct contact types, entered via separate pages:
- `add_person.php` — auto-injects `$00` type; name stored pipe-separated (`prefix|forename|surname|suffix`) in `liberty_xref.xkey_ext` of the `$00` record; `lc.title` = surname; redirects to `edit.php` for further detail
- `add_business.php` — no `$00`; user picks from `$02`+ subtypes (Supplier, Manufacturer etc., expandable via DB); `lc.title` = organisation name; redirects to `edit.php`
Type codes in `liberty_xref_item` (`content_type_guid='contact'`, `x_group='type'`, `sort_order=0`):
- `$00` Person — triggers name fields in edit/display; never shown as a checkbox in UI
- `$01` Organisation — deprecated, not used in new UI
- `$02`+ Business subtypes — shown as checkboxes in `add_business.php` and `edit.php`
`edit.php` detects person via `contact_types[0].content_id` → `$isPerson` flag:
- Person: name fields shown, Contact Types section hidden, Organisation hidden
- Business: org field shown, Contact Types (`$02`+) shown, name fields hidden
`display_contact.tpl` heading = "Personal Contact" / "Business Contact" from `contact_types.0.content_id`.
Name loaded from `$00` xref `xkey_ext` via SQL join in `Contact::load()` (`x00.xkey_ext AS name`).
xref item templates gate dates and edit actions on `{$xrefAllowEdit}` (pass `allow_edit=false` in view, `allow_edit=true` in edit).
#### Planned: ContactPerson / ContactBusiness subclasses
The current `$isPerson` detection via `$00` xref presence is a hack. The plan replaces it
with proper subclasses following the dual-guid xref pattern (as per stock):
- `ContactPerson extends Contact` — `mContentTypeGuid = 'contactperson'`, `mPackageGuid = 'contact'`
- `ContactBusiness extends Contact` — `mContentTypeGuid = 'contactbusiness'`, `mPackageGuid = 'contact'`
Shared schema (addresses, SCREF etc.) stays at `content_type_guid='contact'`.
Person-specific types (`$00` default, kitelf, committee member etc.) at `contactperson` level.
Business subtypes (`$02`+: Supplier, Manufacturer etc.) at `contactbusiness` level.
`$isPerson` flag disappears — the class IS the distinction.
Template resolution already works via `mContentTypeGuid` path lookup in LibertyContent.
**Not yet implemented.** Development/testing on `rainbowdigitalmedia` first.
Upgrade script `contact/admin/upgrades/5.0.3.php` will:
1. Register `contactperson` and `contactbusiness` content types
2. `UPDATE liberty_content SET content_type_guid = 'contactperson'` for records with a `$00` xref
3. Remaining `content_type_guid='contact'` records become `contactbusiness`
**SCREF** — short reference code for a contact, stored in `liberty_xref.xkey` where `item='SCREF'`.
Used as the `from` value in stock movement CSVs to identify the supplier/source contact.
`contact/includes/lookup_contact.php` provides JSON autocomplete searching by `lc.title` or SCREF `xkey`.
### Stock package — file naming convention
Entry points follow `verb_contenttype.php` pattern:
- `view_assembly.php`, `edit_assembly.php`
- `view_component.php`, `edit_component.php`
- `view_movement.php`, `edit_movement.php`
- `list_assemblies.php`, `list_components.php`, `list_movements.php`
- `add_supplier.php` — specialist add page (no generic add_assembly/add_component yet)
- `add_order.php` — draft ORDER movement from shortages list; pre-populates lines with
shortage qty, supplier autocomplete, editable qty/delete per line before creating movement
`view.php` and `edit.php` removed.
`list_stock.php` — stock levels from movement xrefs. Shortages filter (`?shortages=1`) works
on both main list (level < 0) and BOM view (remaining < 0). Shortages view has floaticon icons
for Print, CSV export (`?format=csv`, part_number + qty, skips blanks), and Create Order.
### Firebird GROUP BY strictness
Firebird requires every non-aggregate column in SELECT to appear in GROUP BY — including `lc.data`, `lc.title` etc. Correlated scalar subqueries in SELECT (e.g. `SELECT FIRST 1 ...`) are exempt. MySQL is more lenient; Firebird is not.
### Stock movement model
Movement = pure `liberty_content` record (`content_type_guid='stockmovement'`). No `stock_movement` table.
**Direction** inferred from `reference` xref group (x_group='reference', sort_order=1):
- `REQN` = outbound (to kitlocker)
- `TRANS` = inbound from another elf
- `ORDER` = inbound from supplier
**Status** = `lc.event_time` (BIGINT, Unix seconds) — `0` = placed/open, positive = received/fulfilled.
`StockMovement::markReceived()` sets it to `time()`. `isReceived()` uses `!empty()` so 0 = not received.
**Reference xref** (x_group='reference', sort_order=1), one row per movement:
- `item` = REQN/TRANS/ORDER
- `xkey` = reference number/key
- `data` = free-text "from" (fallback if no contact linked)
- `xref` = contact content_id (linked supplier/source — looked up via SCREF xkey)
- `start_date` (TIMESTAMP) = order date (from CSV col 3 or manual entry)
**Items** = `quantity` xref group (x_group='quantity', sort_order=2), `multiple=1`, one xref row per component line:
- `item` = SGL/PCK/SHT/VOL — quantity type
- `xref` = component content_id
- `xkey` = quantity value
- `xorder` = line sequence (managed explicitly, NOT via `fAddXref` — `multiple=0` on these items for other content types)
**CSV format** (one movement per file): line 1 = `from(SCREF), ref, order_date(dd/mm/yy), received_date(dd/mm/yy optional)`; lines 2+ = `component_title, quantity, [optional qty type]`. Qty type defaults to component's existing xref type if not specified.
Uploaded CSVs are saved to `STOCK_IMPORT_PATH` (`storage/stock/`) as `<origname>_move_<content_id>.csv` for audit. BOM uploads save as `<origname>_bom_<content_id>.csv`.
**`storeXref()` always needs a named variable** — it takes `&$pParamHash` by reference; passing a literal array is a fatal error.
### LibertyXref / xorder
`liberty_xref.xorder` — used for BOM grouping and sort. Must be explicitly selected
in queries; it is not auto-included in standard SELECT lists.
### LibertyXrefType — instance class
`LibertyXrefType` is an **instance class**, not a bag of statics. Construct with
`new LibertyXrefType( $contentTypeGuid, $packageGuid = null )`. In page/class code,
always access it via `LibertyContent::xrefType()` which lazily creates and caches the
instance. The five runtime query methods (`getDisplayGroups`, `getTypeMarkers`,
`getAvailableItems`, `getTemplateFormats`, `getContentTypeMarkers`) are instance methods.
Admin cross-type queries (`getXrefTypeList`, `getContentTypeGuids`, `getGroupList`)
remain static.
### Dual-guid xref schema (package-level + content-type-level)
A package with multiple content types can define xref groups/items at two levels:
- **Package-level** — groups shared across all content types in the package, keyed by the package guid
- **Content-type-level** — groups specific to one content type, keyed by the content type guid
**Stock is the reference implementation:**
- Package-level (`'stock'`): `stgrp`, `supplier`, `kitlocker` — apply to both assemblies and components
- Content-type-level (`'stockcomponent'`): `quantity`, `values`; (`'stockassembly'`): `quantity`
To support this, pass `$packageGuid` when constructing `LibertyXrefType` or `LibertyXrefInfo`
(both accept it as an optional second argument). The `mPackageGuid` property on `LibertyContent`
is set automatically by `registerContentType()` when `handler_package` differs from the content
type guid — so subclasses get it for free.
When writing xref JOIN queries that span both levels, always join item↔group on
`t.content_type_guid = s.content_type_guid` (self-consistent); apply the guid `IN()` filter
only in the WHERE clause on `s`. Putting the filter in the JOIN ON instead causes
cross-matching when two guids share an `x_group` name.
### LibertyXrefGroup display path (contact + stock)
**PHP pattern** — display and edit pages:
```php
$gContent->loadXrefInfo();
$gBitSmarty->assign( 'gXrefInfo', $gContent->mXrefInfo );
```
**Template pattern** — view and edit templates:
```smarty
{foreach $gXrefInfo->mGroups as $xrefGroup}
{include file=$gContent->getXrefListTemplate($xrefGroup->mTemplate)
xrefGroup=$xrefGroup allow_edit=false} {* true for edit pages *}
{/foreach}
```
Group templates receive `$xrefGroup` (LibertyXrefGroup object). First two lines must be:
```smarty
{assign var=xrefAllowEdit value=$allow_edit|default:false}
{assign var=isHistory value=($xrefGroup->mXGroup eq 'history')}
```
Fallback for groups with no specific template → `liberty/list_xref.tpl`.
**Linked content fields (`linked_title` / `linked_data`)** — `LibertyXrefType::loadContent()`
LEFT JOINs `liberty_content lc_linked ON lc_linked.content_id = x.xref` and exposes
`lc_linked.title AS linked_title` and `lc_linked.data AS linked_data` on every xref row.
These come from the **linked content item's** `liberty_content` row (via the `x.xref` FK),
NOT from the xref row's own `xkey`/`xkey_ext`/`data` columns (which are already available
as `$xrefInfo.xkey`, `$xrefInfo.xkey_ext`, `$xrefInfo.data` without any join).
When `x.xref > 0` these fields hold the title and description of the linked item (contact,
component, assembly, etc.). `liberty_content` has no `xkey_ext` equivalent — if further
fields from the linked item are needed, add them to the SELECT in `loadContent()` as
additional `lc_linked.*` aliases, or use a correlated subquery for linked xref data.
- **View templates**: use `$xrefInfo.linked_title` and `$xrefInfo.linked_data` directly — no
separate enrichment query needed.
- **Edit templates** (`edit_xref.php` path): `enrichXrefDisplay()` is called on the single row
before display. Override this in the content class (e.g. `StockBase::enrichXrefDisplay()`)
to set `xref_title` for the edit form. The two paths use different field names by design.
- **Extra fields** (e.g. `part_size` from a second xref): override `loadXrefInfo()` in the
content class, call `parent::loadXrefInfo()` first, then enrich the group rows. Use
`array_map( fn($r) => $r['xref'], $group->mXrefs )` — NOT `array_column()` — to extract
xref values from `LibertyXref` objects (ArrayAccess; `array_column` ignores offsetGet on
some PHP builds).
**Floaticon placement** — on list pages the floaticon div goes inside the `<div class="floaticon">`
inside the assembly view's header. On view/edit pages it goes inside the `.header` div before
the `<h1>`. Forms in the floaticon use `class="minifind"` for correct spacing (see Smarty notes).
`assembly_icons_inc.tpl` is included in every assembly view layout's header — put assembly-level
icons there rather than in `view_assembly.tpl` directly.
**Kitlocker tab visibility** — `edit_component.php` and `view_component.php` detect kitlocker
components via a KLID xref presence check and assign `$isKitlocker`. In templates, stash the
`kitlocker` and `stgrp` groups during the normal foreach and render them at the end only when
`$isKitlocker` is true:
```smarty
{assign var=klGroup value=null}{assign var=sgGroup value=null}
{foreach $gXrefInfo->mGroups as $xrefGroup}
{if $xrefGroup->mXGroup eq 'kitlocker'}{assign var=klGroup value=$xrefGroup}
{elseif $xrefGroup->mXGroup eq 'stgrp'}{assign var=sgGroup value=$xrefGroup}
{else}{include file=... xrefGroup=$xrefGroup ...}{/if}
{/foreach}
{if $isKitlocker && $klGroup}{include ...}{/if}
{if $isKitlocker && $sgGroup}{include ...}{/if}
```
**movement edit_movement.php** filters 'reference' group in template:
`{if $xrefGroup->mXGroup neq 'reference'}` — reference is rendered directly in the form.
**CSV import xorder** — `ImportContactCSV.php` explicitly sets xorder: 0 for single items,
1 for #P/#F (multiple=1) - will need to address when more than one record needed.
**`contact_types` in Contact** — separate from the display path. Populated by
`loadXrefTypeList()` which queries sort_order=0 items (the 'type' group: `$00`, `$02`+).
Used for `$isPerson` detection in `edit.php` and display templates. `loadXrefInfo()`
deliberately excludes sort_order=0, so there is no overlap.
**Raw LEFT JOINs in `Contact::load()`** — joins `liberty_xref` directly for `$00`
(person name), `#S` (service address), `#L` (location), `IMG` (gallery). `IMG`, `#S`,
`#L` have no live data.
**Pending: Contact::load() cleanup** — remove the three commented-out dead joins; replace
the active raw xref joins with the proper path: `$00` name from `loadXrefTypeList()` with
`xkey_ext` added; `#S`/`#L`/`ap` from `loadXrefInfo()` address group (postcode join
already present in `LibertyXrefGroup::loadXrefs()`); gallery association needs rethinking
separately from the xref mechanism.
View pages pass `allow_edit=false` (or omit), edit pages pass `allow_edit=true`.
### Contact delete / expunge
`edit.php` handles `expunge=1`: checks `p_contact_expunge`, calls `$gContent->expunge()`,
redirects to `list_contacts.php`. `contact_date_bar.tpl` uses
`{smartlink ... ifile="edit.php" expunge=1}`. `Contact::expunge()` deletes liberty_xref rows
then calls `LibertyContent::expunge()`.
## Infrastructure
### /etc/webstack
Single git repo replicated across desktop, srv9, and srv10. Push to `/srv/git/webstack.git`
before pulling on servers — servers pull from the desktop copy, not GitHub.
Contains: nginx vhost configs, logrotate, cron scripts, domain theme symlinks, PHP/Firebird config.
`/etc/logrotate.d/nginx` is a **hard link** to `/etc/webstack/logrotate.d/nginx` — one config,
not two competing ones. Never edit via `/etc/logrotate.d/` directly.
### Nginx log structure
Central combined log: `/var/log/nginx/access.log` — every request from all vhosts, domain name
appended at end of each line by nginx log format.
Per-domain dirs: `/var/log/nginx/{domain}/` — contain `access.log` (filtered, rebuilt nightly),
`80.access.log` (port 80, written directly by nginx), and `error.log`. All are covered by the
domain logrotate stanza (`size=+8M`, `rotate 7`). The filtered `access.log` is always small so
`notifempty` skips it; the stanza exists for `80.access.log` and `error.log`.
Logrotate on Tumbleweed is a **crontab entry**, not a cron.daily script — ordering tricks
(renaming files in cron.daily) do not affect when logrotate runs relative to nginx-stats.
Raw log rotation (parent only): `size=+16M`, `rotate 14`, `maxage 30`, `delaycompress`.
At current traffic (~7 MB/day) this gives ~30 days of history across ~14 archives.
Compressed archives use `.xz` or `.gz` depending on when they were created.
### nginx-stats / goaccess
Script: `/etc/webstack/cron.daily/nginx-stats`
Runs nightly. Builds `/tmp/nginx-combined.log` from the current `access.log` plus all
parent archives (decompressing `.xz`/`.gz` inline), then greps per domain into
`/var/log/nginx/{domain}/access.log`, runs goaccess, publishes
`/srv/website/{domain}/stats-rep.html`. Cleans up `combined.log` on exit.
Domain `access.log` files are ephemeral outputs — rebuilt from parent archives each run.
History lives entirely in the parent archive chain; no domain-level archives are created.
## Session Management
At the end of each productive session, append discoveries, decisions, and completed items to this file.
Use `/clear` to reset context when it gets bloated — this file re-orients the session.
### 2026-06-14 — Stock multi-user (kitelf) + PBLD prebuild type
**Kitelf filtering**
- `list_movements.php` / `list_stock.php`: `?user_id=X` filter; creator names in
list_movements are clickable filter links; breadcrumb shows kitelf name with link
to their filtered list; `×` clear replaced by breadcrumb `<small>` pattern
- `list_movements.php`: unified `part_content_id` replaces separate
`assembly_content_id` / `component_content_id`; type-aware breadcrumb and qty column
(assembly kit count vs component qty with PRT/SHT formatting)
- `StockMovement::getList()`: `$partId`/`$partIsAsm` collapse the two part vars;
unified `part_qty`/`part_qty_type` SELECT; `lc.user_id` added to SELECT;
PBLD added to all `IN('REQN','TRANS','ORDER')` lists and sort subqueries
- `view_movement.tpl`: updated to `<header>`/`<section>` pattern with kitelf breadcrumb
**PBLD movement type**
- New `PBLD` (Prebuild) movement type: assembly-only, BOM exploded, optional note
- `add_prebuild.php` / `add_prebuild.tpl`: creates PBLD; `add_requisition` retired
from UI (file kept); list_stock BOM "Create Requisition" → "Create Prebuild"
- `edit_movement.php/tpl` / `view_movement.php/tpl`: `$isBuild` (REQN+PBLD) and
`$isPbld` flags; PBLD shows "Build Date"/"Completed"/"In progress" labels;
assembly picker and xref tab shown for both REQN and PBLD
- `schema_inc.php`: PBLD registered in stockmovement reference xref items
- `StockMovement::getDirection()`: PBLD explicit as 'O'; unknown types return ''
**Owner change**
- `edit_content_owner_inc.tpl` included in `edit_assembly.tpl` and `edit_movement.tpl`
- `liberty_allow_change_owner` must be active + `p_liberty_edit_content_owner` permission
- Enables reassigning `user_id` (kitelf ownership) from edit pages
**Template cleanup**
- Dead files removed: `center_list_assemblies.php`, `center_list_components.php`,
`center_component_comments.php/.tpl`, `center_list_components.tpl`,
`list_assemblies2.tpl`, `list_assemblies_simple.tpl`, `list_assemblies.tpl` (old)
- Renamed: `stock_fixed_grid_inc.tpl` → `view_kitlocker.tpl`;
`list_assemblies_simple.tpl` → `list_assemblies.tpl`
- `assembly_icons_inc.tpl` and `assembly_nav.tpl` properly implemented —
floaticons and breadcrumb extracted from `stock_simple_list_inc.tpl` into includes
- `user_galleries.tpl`: inlined from `list_assemblies2.tpl`; sortby suppressed in
user view; floaticons (print, add prebuild, stock levels); panel layout with
`panel-heading` (title + KLID), `panel-body` (parsed_data), `panel-footer` (counts)
**StockAssembly::getList() enriched**
- Now provides per-row: `parsed_data` (via `parseDataHash`), `part_number` (#SUP first),
`klid`, `component_count` (BOM lines), `prebuild_count` (PBLD kit total for assembly
owner via correlated subquery — uses `mc.user_id = lc.user_id`)
## CC Limitations
For execution-order bugs and session/config state problems,
use xdebug rather than asking CC to trace — static analysis
cannot follow runtime state reliably.
|