From 3d44415051fbe82334a611710ab0509c402faf62 Mon Sep 17 00:00:00 2001 From: Fax Date: Sat, 28 Mar 2026 22:24:34 +1300 Subject: [PATCH 1/3] divider --- .claude/commands/changelog.md | 26 ++ .devcontainer/Dockerfile | 102 ++++++++ .devcontainer/devcontainer.json | 56 +++++ .devcontainer/init-firewall.sh | 112 +++++++++ CLAUDE.md | 7 - changelog/v0.6.0.md | 6 + src/app/components/welcome-modal/changelog.ts | 6 + .../document/document-viewer-styles.ts | 235 ++++++++++++++++++ .../document/document-viewer.component.css | 29 +++ .../document/document-viewer.component.html | 87 +++++-- .../document/document-viewer.component.ts | 162 ++++++++++-- 11 files changed, 780 insertions(+), 48 deletions(-) create mode 100644 .claude/commands/changelog.md create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/init-firewall.sh create mode 100644 src/app/features/document/document-viewer-styles.ts diff --git a/.claude/commands/changelog.md b/.claude/commands/changelog.md new file mode 100644 index 0000000..1eccce6 --- /dev/null +++ b/.claude/commands/changelog.md @@ -0,0 +1,26 @@ +Run `git diff HEAD` and `git diff --cached` to see all uncommitted changes (staged and unstaged). Analyze the diffs to understand what was changed, then update the changelog accordingly. + +## Where to add entries + +There are two files that must stay in sync: + +1. **In-app changelog:** `src/app/components/welcome-modal/changelog.ts` — add lines to the current version's `items` array in the appropriate section (`New Features`, `Improvements`, or `Bug Fixes`) +2. **Markdown changelog:** `changelog/v{version}.md` — add matching lines under the same section heading + +## When to add to the existing version vs create a new one + +- **Add to the current version** when the changes are part of ongoing work for that release — bug fixes, tweaks, new features that haven't been shipped yet. Check `APP_VERSION` in `changelog.ts` to see the current version. +- **Create a new version** only when the user explicitly asks to bump the version, or when changes are being made after a release has already been shipped/tagged. To create a new version: + 1. Bump `APP_VERSION` in `changelog.ts` + 2. Add a new entry at the top of the `CHANGELOG` array + 3. Create a new `changelog/v{version}.md` file + +## Entry format + +- Keep entries concise — one line per change +- Use action verbs: "Added ...", "Fixed ...", "Improved ...", etc. +- Categorize correctly: + - **New Features**: entirely new functionality + - **Improvements**: enhancements to existing features, UI polish, performance + - **Bug Fixes**: corrections to broken behavior +- Don't duplicate entries that already exist in the changelog diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..1289eeb --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,102 @@ +FROM node:20 + +ARG TZ +ENV TZ="$TZ" + +ARG CLAUDE_CODE_VERSION=latest + +# Install basic development tools and iptables/ipset +RUN apt-get update && apt-get install -y --no-install-recommends \ + less \ + git \ + procps \ + sudo \ + fzf \ + zsh \ + man-db \ + unzip \ + gnupg2 \ + iptables \ + ipset \ + iproute2 \ + dnsutils \ + aggregate \ + jq \ + nano \ + vim \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Ensure default node user has access to /usr/local/share +RUN mkdir -p /usr/local/share/npm-global && \ + chown -R node:node /usr/local/share + +ARG USERNAME=node + +# Persist bash history. +RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \ + && mkdir /commandhistory \ + && touch /commandhistory/.bash_history \ + && chown -R $USERNAME /commandhistory + +# Set `DEVCONTAINER` environment variable to help with orientation +ENV DEVCONTAINER=true + +# Create workspace and config directories and set permissions +RUN mkdir -p /workspace /home/node/.claude && \ + chown -R node:node /workspace /home/node/.claude + +WORKDIR /workspace + +ARG GIT_DELTA_VERSION=0.18.2 +RUN ARCH=$(dpkg --print-architecture) && \ + wget "https://github.com/dandavison/delta/releases/download/${GIT_DELTA_VERSION}/git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \ + sudo dpkg -i "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \ + rm "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" + +# Set up non-root user +USER node + +# Install global packages +ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global +ENV PATH=$PATH:/usr/local/share/npm-global/bin + +# Set the default shell to zsh rather than sh +ENV SHELL=/bin/zsh + +# Set the default editor and visual +ENV EDITOR=nano +ENV VISUAL=nano + +# Default powerline10k theme +ARG ZSH_IN_DOCKER_VERSION=1.2.0 +RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh)" -- \ + -p git \ + -p fzf \ + -a "source /usr/share/doc/fzf/examples/key-bindings.zsh" \ + -a "source /usr/share/doc/fzf/examples/completion.zsh" \ + -a "export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \ + -x + +# Install mise (runtime version manager) +RUN curl https://mise.run | sh && \ + echo 'eval "$(~/.local/bin/mise activate zsh)"' >> ~/.zshrc && \ + echo 'eval "$(~/.local/bin/mise activate bash)"' >> ~/.bashrc +ENV PATH="/home/node/.local/share/mise/shims:/home/node/.local/bin:$PATH" + +# Disable git push (block pushing over both HTTPS and SSH) +RUN git config --global --add safe.directory /workspace && \ + printf '#!/bin/sh\necho "git push is disabled in this devcontainer." >&2\nexit 1\n' > /home/node/.local/bin/git-push-blocked && \ + chmod +x /home/node/.local/bin/git-push-blocked && \ + git config --global alias.push '!git-push-blocked' + +# Install Claude +RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} + + +# Copy and set up firewall script +COPY init-firewall.sh /usr/local/bin/ +USER root +RUN chmod +x /usr/local/bin/init-firewall.sh && \ + echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/node-firewall && \ + chmod 0440 /etc/sudoers.d/node-firewall +USER node diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..8ad4988 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,56 @@ +{ + "name": "Claude Code Sandbox", + "build": { + "dockerfile": "Dockerfile", + "args": { + "TZ": "${localEnv:TZ:America/Los_Angeles}", + "CLAUDE_CODE_VERSION": "latest", + "GIT_DELTA_VERSION": "0.18.2", + "ZSH_IN_DOCKER_VERSION": "1.2.0" + } + }, + "runArgs": [ + "--cap-add=NET_ADMIN", + "--cap-add=NET_RAW" + ], + "customizations": { + "vscode": { + "extensions": [ + "anthropic.claude-code", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "eamodio.gitlens" + ], + "settings": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "terminal.integrated.defaultProfile.linux": "zsh", + "terminal.integrated.profiles.linux": { + "bash": { + "path": "bash", + "icon": "terminal-bash" + }, + "zsh": { + "path": "zsh" + } + } + } + } + }, + "remoteUser": "node", + "mounts": [ + "source=claude-code-bashhistory-${devcontainerId},target=/commandhistory,type=volume", + "source=claude-code-config-${devcontainerId},target=/home/node/.claude,type=volume" + ], + "containerEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + "CLAUDE_CONFIG_DIR": "/home/node/.claude", + "POWERLEVEL9K_DISABLE_GITSTATUS": "true" + }, + "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated", + "workspaceFolder": "/workspace", + "postStartCommand": "echo 'Dev container ready.'" +} diff --git a/.devcontainer/init-firewall.sh b/.devcontainer/init-firewall.sh new file mode 100644 index 0000000..6623521 --- /dev/null +++ b/.devcontainer/init-firewall.sh @@ -0,0 +1,112 @@ +#!/bin/bash +set -euo pipefail # Exit on error, undefined vars, and pipeline failures +IFS=$'\n\t' # Stricter word splitting + +# 1. Extract Docker DNS info BEFORE any flushing +DOCKER_DNS_RULES=$(iptables-save -t nat | grep "127\.0\.0\.11" || true) + +# Flush existing rules and delete existing ipsets +iptables -F +iptables -X +iptables -t nat -F +iptables -t nat -X +iptables -t mangle -F +iptables -t mangle -X +ipset destroy allowed-domains 2>/dev/null || true + +# 2. Selectively restore ONLY internal Docker DNS resolution +if [ -n "$DOCKER_DNS_RULES" ]; then + echo "Restoring Docker DNS rules..." + iptables -t nat -N DOCKER_OUTPUT 2>/dev/null || true + iptables -t nat -N DOCKER_POSTROUTING 2>/dev/null || true + echo "$DOCKER_DNS_RULES" | xargs -L 1 iptables -t nat +else + echo "No Docker DNS rules to restore" +fi + +# First allow DNS and localhost before any restrictions +# Allow outbound DNS +iptables -A OUTPUT -p udp --dport 53 -j ACCEPT +# Allow inbound DNS responses +iptables -A INPUT -p udp --sport 53 -j ACCEPT +# Block outbound SSH (prevents git push over SSH) +iptables -A OUTPUT -p tcp --dport 22 -j REJECT +# Allow localhost +iptables -A INPUT -i lo -j ACCEPT +iptables -A OUTPUT -o lo -j ACCEPT + +# Create ipset with CIDR support +ipset create allowed-domains hash:net + +# Resolve and add allowed domains +for domain in \ + "registry.npmjs.org" \ + "api.anthropic.com" \ + "sentry.io" \ + "statsig.anthropic.com" \ + "statsig.com" \ + "marketplace.visualstudio.com" \ + "vscode.blob.core.windows.net" \ + "update.code.visualstudio.com"; do + echo "Resolving $domain..." + ips=$(dig +noall +answer A "$domain" | awk '$4 == "A" {print $5}') + if [ -z "$ips" ]; then + echo "ERROR: Failed to resolve $domain" + exit 1 + fi + + while read -r ip; do + if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then + echo "ERROR: Invalid IP from DNS for $domain: $ip" + exit 1 + fi + echo "Adding $ip for $domain" + ipset add allowed-domains "$ip" + done < <(echo "$ips") +done + +# Get host IP from default route +HOST_IP=$(ip route | grep default | cut -d" " -f3) +if [ -z "$HOST_IP" ]; then + echo "ERROR: Failed to detect host IP" + exit 1 +fi + +HOST_NETWORK=$(echo "$HOST_IP" | sed "s/\.[0-9]*$/.0\/24/") +echo "Host network detected as: $HOST_NETWORK" + +# Set up remaining iptables rules +iptables -A INPUT -s "$HOST_NETWORK" -j ACCEPT +iptables -A OUTPUT -d "$HOST_NETWORK" -j ACCEPT + +# Set default policies to DROP first +iptables -P INPUT DROP +iptables -P FORWARD DROP +iptables -P OUTPUT DROP + +# First allow established connections for already approved traffic +iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT +iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT + +# Then allow only specific outbound traffic to allowed domains +iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT + +# Explicitly REJECT all other outbound traffic for immediate feedback +iptables -A OUTPUT -j REJECT --reject-with icmp-admin-prohibited + +echo "Firewall configuration complete" +echo "Verifying firewall rules..." +if curl --connect-timeout 5 https://example.com >/dev/null 2>&1; then + echo "ERROR: Firewall verification failed - was able to reach https://example.com" + exit 1 +else + echo "Firewall verification passed - unable to reach https://example.com as expected" +fi + +# Verify Anthropic API access +if ! curl --connect-timeout 5 https://api.anthropic.com >/dev/null 2>&1; then + echo "ERROR: Firewall verification failed - unable to reach https://api.anthropic.com" + exit 1 +else + echo "Firewall verification passed - able to reach https://api.anthropic.com as expected" +fi diff --git a/CLAUDE.md b/CLAUDE.md index fc6d152..7bb9b1c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,12 +1,5 @@ # Project Instructions for Claude -## Changelog Policy -After every code change, update the changelog: -- **In-app changelog:** `src/app/components/welcome-modal/changelog.ts` — add a line to the current version's items array -- **Markdown changelog:** `changelog/v{version}.md` — add a matching line -- If the change warrants a new version, bump `APP_VERSION` in `changelog.ts`, create a new `changelog/v{version}.md` file, and add a new entry to the `CHANGELOG` array -- Keep entries concise (one line per change) - ## Testing - Run tests with `npm test` after changes - When creating or significantly modifying a component/service, add or update tests to maintain coverage diff --git a/changelog/v0.6.0.md b/changelog/v0.6.0.md index 7dd7ea7..39534ec 100644 --- a/changelog/v0.6.0.md +++ b/changelog/v0.6.0.md @@ -16,3 +16,9 @@ - Middle-click table rows to open document in a new tab - Default columns for new users: Ticker, Form, Company, Tags, Description, Date Filed, Size - Table cell text vertically centered in rows +- Document viewer settings dropdown with Experimental Viewer and Page View toggles, persisted via Strings API +- Page View mode centers documents in a max-width container with responsive margins for readability on wide screens +- Fixed browser back button navigating within iframe instead of returning to filings +- Fixed document viewer page mode content collapsing to a short height instead of filling the viewport +- Fixed white bars appearing on sides of document in experimental viewer with page view mode +- Moved viewer settings (Styled/Page toggles) from dropdown menu to inline toggle buttons in the top bar diff --git a/src/app/components/welcome-modal/changelog.ts b/src/app/components/welcome-modal/changelog.ts index 4385102..26d53fd 100644 --- a/src/app/components/welcome-modal/changelog.ts +++ b/src/app/components/welcome-modal/changelog.ts @@ -30,6 +30,12 @@ export const CHANGELOG: ChangelogEntry[] = [ 'Middle-click table rows to open document in a new tab', 'Default columns for new users: Ticker, Form, Company, Tags, Description, Date Filed, Size', 'Table cell text vertically centered in rows', + 'Document viewer settings dropdown with Experimental Viewer and Page View toggles, persisted via Strings API', + 'Page View mode centers documents in a max-width container with responsive margins for readability on wide screens', + 'Fixed browser back button navigating within iframe instead of returning to filings', + 'Fixed document viewer page mode content collapsing to a short height instead of filling the viewport', + 'Fixed white bars appearing on sides of document in experimental viewer with page view mode', + 'Moved viewer settings (Styled/Page toggles) from dropdown menu to inline toggle buttons in the top bar', ], }, ], diff --git a/src/app/features/document/document-viewer-styles.ts b/src/app/features/document/document-viewer-styles.ts new file mode 100644 index 0000000..195943b --- /dev/null +++ b/src/app/features/document/document-viewer-styles.ts @@ -0,0 +1,235 @@ +/** + * CSS and scripts injected into fetched document HTML before rendering in iframe. + * Provides a CSS reset, dark-theme styling matching the application, and + * a postMessage script for height reporting and text selection. + */ + +export const DOCUMENT_STYLES = ` +/* ===== CSS Reset ===== */ +*, *::before, *::after { + box-sizing: border-box; +} + +html { + -webkit-text-size-adjust: 100%; + -moz-text-size-adjust: 100%; + text-size-adjust: 100%; +} + +/* ===== Base ===== */ +body { + margin: 0; + padding: 32px 40px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif !important; + font-size: 14px !important; + line-height: 1.65 !important; + color: #d4d4d4 !important; + background-color: #1e1e1e !important; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + word-wrap: break-word; + overflow-wrap: break-word; +} + +/* ===== Override SEC Inline Styles ===== */ +span, p, div, td, th, tr, li, a, b, i, u, em, strong, font, +span[style], p[style], div[style], td[style], th[style], font[style] { + font-family: inherit !important; + color: inherit !important; + background-color: transparent !important; +} + +font { + font-size: inherit !important; +} + +/* ===== Typography ===== */ +h1, h2, h3, h4, h5, h6 { + color: #e0e0e0 !important; + font-weight: 600; + font-family: inherit !important; + margin: 1.25em 0 0.5em; + line-height: 1.3; +} + +h1 { font-size: 1.5em; } +h2 { font-size: 1.3em; } +h3 { font-size: 1.15em; } +h4 { font-size: 1.05em; } + +p { + margin: 0.6em 0; +} + +b, strong { + color: #ffffff !important; + font-weight: 600; +} + +i, em { + color: #cccccc !important; +} + +a { + color: #3794ff !important; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +/* ===== Tables ===== */ +table { + width: 100%; + border-collapse: collapse; + margin: 12px 0; + background: #1e1e1e !important; + border: 1px solid #3c3c3c; +} + +th { + background: #252526 !important; + color: #cccccc !important; + font-weight: 600; + text-align: left; + padding: 8px 12px; + border: 1px solid #3c3c3c; + font-size: 13px; +} + +td { + padding: 6px 12px; + border: 1px solid #2d2d2d; + font-size: 13px; + vertical-align: top; + color: #cccccc !important; +} + +tr:nth-child(even) { background: #252526 !important; } +tr:nth-child(odd) { background: #1e1e1e !important; } +tr:hover { background: #2a2d2e !important; } + +/* Numeric value cells (SEC XBRL classes) */ +td.nump, td.num { + text-align: right; + font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace !important; + color: #4ec9b0 !important; +} + +td.numn { + text-align: right; + font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace !important; + color: #f14c4c !important; +} + +/* Bold/header rows */ +tr.rh td { + color: #ffffff !important; + font-weight: 600; +} + +/* ===== Lists ===== */ +ul, ol { + margin: 0.6em 0; + padding-left: 1.5em; +} + +li { + margin: 0.2em 0; +} + +/* ===== Images ===== */ +img { + max-width: 100%; + height: auto; +} + +/* ===== Horizontal Rules ===== */ +hr { + border: none; + border-top: 1px solid #3c3c3c; + margin: 1.25em 0; +} + +/* ===== Blockquotes ===== */ +blockquote { + border-left: 3px solid #3c3c3c; + padding-left: 16px; + margin: 1em 0; + color: #858585 !important; +} + +/* ===== Preformatted / Code ===== */ +pre, code { + font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace !important; + font-size: 13px; + background: #252526 !important; + border-radius: 3px; + color: #d4d4d4 !important; +} + +pre { + padding: 12px; + overflow-x: auto; + border: 1px solid #3c3c3c; + margin: 1em 0; +} + +code { + padding: 2px 4px; +} + +/* ===== Selection ===== */ +::selection { + background: #264f78; + color: #ffffff; +} + +/* ===== Scrollbar ===== */ +::-webkit-scrollbar { width: 8px; height: 8px; } +::-webkit-scrollbar-thumb { background: #424242; border-radius: 4px; } +::-webkit-scrollbar-track { background: #1e1e1e; } +`; + +export const DOCUMENT_POSTMESSAGE_SCRIPT = ` + +`; diff --git a/src/app/features/document/document-viewer.component.css b/src/app/features/document/document-viewer.component.css index 5f028e6..d7bb800 100644 --- a/src/app/features/document/document-viewer.component.css +++ b/src/app/features/document/document-viewer.component.css @@ -18,6 +18,35 @@ main { position: relative; } +/* ============================================ + Page View Mode + ============================================ + Centers the document in a constrained-width + "page" with responsive margins. + ============================================ */ + +.page-view-container { + background: #161616; +} + +.page-view-content { + max-width: 1100px; + margin-left: auto; + margin-right: auto; + min-height: 100%; + background: white; + box-shadow: 0 0 40px rgba(0, 0, 0, 0.5); + /* Responsive padding: shrinks as the window narrows, with a minimum */ + padding-left: clamp(16px, 4vw, 64px); + padding-right: clamp(16px, 4vw, 64px); +} + +.page-view-content-dark { + background: #1e1e1e; + padding-left: 0; + padding-right: 0; +} + /* ============================================ XBRL Report Viewer Styles ============================================ diff --git a/src/app/features/document/document-viewer.component.html b/src/app/features/document/document-viewer.component.html index 2bd39a2..d659246 100644 --- a/src/app/features/document/document-viewer.component.html +++ b/src/app/features/document/document-viewer.component.html @@ -1,25 +1,47 @@
-
-
- + + + + | + @if (filing()) { + {{ filing()!.companyConformedName }} + + {{ filing()!.formType }} + + } @else { + Loading... + } +
+ + + | - @if (filing()) { - {{ filing()!.companyConformedName }} - - {{ filing()!.formType }} - - } @else { - Loading... - } -
-