Imported from ggghhhjjj/janus (
AGENTS.md). Install upstream withnpx skills add ggghhhjjj/janus. Copyright stays with the author.
Agent Instructions — Hello Cordova (Angular + Browser)
Project Overview
Apache Cordova app targeting the browser platform, with Angular 21 as the UI framework.
- Angular source:
src/— always edit files here - Build output:
www/— generated bynpm run build, do not edit - Cordova output:
platforms/browser/www/— generated by Cordova, do not edit - App config:
config.xml - Dependencies:
package.json
Key Source Files
| File | Purpose |
|---|---|
src/index.html |
HTML shell — CSP header, <app-root>, cordova.js script tag |
src/app/app.ts |
Root standalone component |
src/app/cordova.service.ts |
CordovaService — wraps deviceready as an Observable |
src/app/services/i18n.service.ts |
Runtime i18n — translate(key), setLocale(), Signal-based |
src/app/services/translations.generated.ts |
All translations — single source of truth for i18n |
src/app/services/state.service.ts |
Global app state — exposes transactions$, fifoState$; computed signals: transactionNumbers, conflictTransactionIds, brokerAccountBalances |
src/app/services/database.service.ts |
IndexedDB persistence (stores: transactions, settings; indexes: ticker, date, type) |
src/app/services/fifo.service.ts |
FIFO calculation logic |
src/app/services/matching.service.ts |
Transforms FifoState → MatchingDetailsRow[], totals, and verification flag |
src/app/utils/number-utils.ts |
Shared rounding helpers — always use round2() from here, never redefine it |
src/app/models/ |
Shared TypeScript interfaces |
src/app/components/dashboard/ |
Layout-only container — orchestrates components, no data logic |
src/app/components/broker-account/ |
Component — broker account balance |
src/app/components/total-gain-loss/ |
Component — total realized gain/loss |
src/app/components/yearly-breakdown/ |
Component — year-by-year gain/loss table |
src/app/components/open-lots/ |
Component — open positions by ticker |
src/app/components/fifo-matching/ |
Component — FIFO lot matching detail table |
src/app/components/shared/tx-table/ |
CSS ownership boundary — TxTableComponent (ViewEncapsulation.None) owns all shared transaction-cell classes: .type-badge*, .num-cell*, .col-date__*, .tx-ticker__meta, .tx-row--conflict, .notes-cell. Wrap any <table class="data-table"> that needs these styles in <app-tx-table>. |
public/ |
Static assets copied verbatim into www/ |
angular.json |
Angular CLI config — output path is www/ |
hooks/before_prepare/build_angular.js |
Cordova hook — runs npm run build before prepare/build/run |
Build & Run Commands
npm install # First time or after package.json changes
npm run build # ng build → www/ → cordova build browser
npm run cordova:run # Build + serve in browser
npm run watch # ng build --watch (Angular only, no Cordova step — fast dev loop)
npm test # Vitest (watch mode auto-starts)
npm run clean # Delete www/ output directory
Requires
cordovaCLI:npm install -g cordova
How It Works
npm run build→ng buildcompilessrc/→www/- Cordova copies
www/→platforms/browser/www/and serves it - The
before_preparehook firesnpm run buildon everycordova run/build/prepare
Angular Conventions
- Standalone components — always
standalone: true, no NgModule. - Signals for reactive state:
signal(),computed(). No manual subscriptions for local state. - Component naming:
app.ts,app.html,app.css(no.component.infix). CordovaService.deviceReady$is the only entry point for Cordova plugin access.
Widget Architecture
Dashboard sections are self-sufficient widgets — each widget owns its data, presentation, and styles without receiving inputs from its host.
Rules:
- Widgets inject
StateService(or other services) directly — never use@Input()to pass data that could be fetched from a service. - Exception:
ActionMenuComponentis a reusable UI primitive that legitimately receivesActionMenuItem[]via@Input()— this is not a dashboard widget. - Use
toSignal(inject(StateService).fifoState$, { initialValue: null })as the standard pattern to subscribe to reactive state. - Widget CSS files are self-contained — include
.card,.card__title, utility classes like.text-rightetc. if used in the template. .cardbase styles are defined globally insrc/styles.css— no need to redeclare; declare only widget-specific overrides.- The
DashboardComponentis layout-only: it may contain conditional rendering (@if) but no data-derivation logic.
Template for a new widget:
@Component({
selector: 'app-my-widget',
standalone: true,
imports: [CurrencyPipe], // only what the template uses
templateUrl: './my-widget.html',
styleUrl: './my-widget.css',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MyWidgetComponent {
private readonly fifoState = toSignal(inject(StateService).fifoState$, { initialValue: null });
readonly i18n = inject(I18nService);
readonly myData = computed(() => this.fifoState()?.someField ?? defaultValue);
}
See src/app/components/total-gain-loss/ for the simplest widget example.
CSS Conventions
All CSS in this project must follow BEM (Block Element Modifier) naming.
Rules:
- Block: the component root class, e.g.
.swap-modal,.transaction-table. - Element: a child of a block, joined with
__, e.g..swap-modal__title,.swap-radio-label__input. - Modifier: a variant of a block or element, joined with
--, e.g..type-badge--buy,.modal-card--wide. - Never use nested tag selectors to style elements — e.g.
.swap-radio-label inputis wrong; use.swap-radio-label__inputinstead. - Never use ID selectors (
#foo) for styling. - Pseudo-classes and pseudo-elements are allowed directly on a BEM class — e.g.
.swap-radio-label__input:checked::beforeis correct. - Each BEM block maps to one component CSS file; blocks are not shared across components (except globals in
src/styles.cssand the transaction-cell design system insrc/app/components/shared/tx-table/tx-table.css). - Never redeclare
.type-badge*,.num-cell*,.col-date__*,.tx-ticker__meta,.tx-row--conflict, or.notes-cellin any consumer component CSS — they live intx-table.css. - Never add inline styles to elements that have a corresponding class in
tx-table.css(e.g..tx-ticker__meta,.tx-total,.tx-feeare already styled there).
CSS Shared via ViewEncapsulation.None
TxTableComponent uses ViewEncapsulation.None so its styles apply to projected content from any parent component. Angular's emulated encapsulation stamps projected DOM elements with the providing component's _ngcontent attribute — not the wrapper's — so scoped CSS rules on the wrapper cannot match them. None makes the styles global; BEM class names are the safety boundary.
Rules for this pattern:
- Use only when the same CSS classes need to apply to content projected from multiple different parent components.
- Always set
your-element-selector { display: block; }in the CSS file — HTML custom elements default todisplay: inline, which can disrupt block layout of children. - Consumer components must add
TxTableComponentto theirimportsarray and wrap the table in<app-tx-table>.
Examples:
/* ✅ Correct BEM */
.swap-radio-label { }
.swap-radio-label__input { }
.swap-radio-label__input:checked { }
.swap-radio-label__input:checked::before { }
.type-badge--buy { }
/* ❌ Wrong — nested tag selector */
.swap-radio-label input[type="radio"] { }
/* ❌ Wrong — nesting without BEM element class */
.swap-radio-label > span { }
Service Conventions
- Pure/sync services: Business-logic services (
FifoService,MatchingService) are stateless and synchronous — noasync, no RxJS, no Angular injection needed to instantiate them in tests. - Rounding: always import
round2fromsrc/app/utils/number-utils.ts. Never redefine a local rounding helper. - Responsibilities:
FifoService— canonical FIFO math, producesFifoState(single source of truth for numbers).MatchingService— presentation transform only: flattensFifoState→ rows/totals/verification. No DB or state writes.StateService— reactive bridge; callsFifoService.calculate()and exposesfifoState$to the UI.
- Testing: pure services can be unit-tested by
new FifoService()/new MatchingService()directly — noTestBed. - MatchingService verification: uses epsilon
0.01to compare detailed gain sum vsfifoState.totalRealizedGainLoss.
Tests
- Runner: Vitest (
npm test→ng test). Watch mode starts automatically. - Location: co-located spec files (
*.spec.ts) next to the source they test. - Style:
describe/it/expectfromvitest— seesrc/app/services/fifo.service.spec.tsfor reference. Transactionfactory pattern: use amakeTx(overrides)helper that spreads defaults (including requiredtime: '00:00:00.000'andcurrency: 'USD') before overrides.Transaction.seqNois optional — used for conflict ordering when multiple transactions share the samedate + time + ticker. UseStateService.swapSeqNos()to resolve conflicts.- For
MatchingServicetests composeFifoService.calculate(txs)to produce canonicalFifoState, then assertMatchingService.computeMatching(state)— seesrc/app/services/matching.service.spec.ts. - No Angular
TestBedis needed for pure service tests.
i18n Conventions
See full documentation in I18N.md. Use /add-i18n skill for step-by-step help.
- All user-facing strings must use
i18n.translate('key')— no hardcoded English or Bulgarian text in templates. - Source of truth:
src/app/services/translations.generated.ts— edit this directly to add/update keys. - Supported locales:
en(English),bg(Bulgarian, default). - Service injection:
readonly i18n = inject(I18nService);in component class body. - Template patterns:
{{ i18n.translate('myKey') }} [placeholder]="i18n.translate('myKey')" [attr.aria-label]="i18n.translate('myKey')" - Never use Angular's
i18nattribute orng extract-i18n— not compatible with this runtime approach.
Cordova Conventions
- Always use
CordovaService.deviceReady$— never add rawdevicereadylisteners. cordova.jsis injected at runtime by the platform — do not move the<script src="cordova.js">tag.- CSP is set via
<meta http-equiv="Content-Security-Policy">insrc/index.html. - Plugins:
cordova plugin add <plugin-id>, declared inconfig.xml.
Common Pitfalls
- Never edit
www/orplatforms/browser/www/— auto-generated. - No hardcoded UI strings — all text goes through
i18n.translate(). - CSP failures are silent — if an API call fails, check
src/index.htmlCSP header. cordovaobject undefined in dev — always guard withtypeof cordova !== 'undefined'.ViewEncapsulation.Nonestyles are global — classes intx-table.cssapply everywhere onceTxTableComponentis mounted. Never add generic or short class names to this file; BEM is the only safety net.- Custom elements default to
display:inline— any newViewEncapsulation.Nonewrapper component must includeselector { display: block; }in its CSS file.