109 Commits
Author SHA1 Message Date
Phiality 05a1372de8 Fix: Screaming Frog licence is $279/year, not $259
Their pricing page now lists $279 / £199 / €245 per user per year, with
bulk bands below that from five seats up. The old figure was also across
librecrawl.com, which has been corrected separately.
2026-09-29 18:57:38 +10:00
PhialsBasementandClaude Opus 5.5 3a93daa6d8 Fix: Zoho login failed with "An error occurred"
The authorize request sent prompt=select_account, which Zoho doesn't
support (its only valid value is "consent"). Zoho showed a generic error
page instead of the sign-in screen. Drop the parameter.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 12:34:46 +10:00
PhialsBasementandClaude Opus 5.5 d451885216 Add optional Zoho OAuth login
Setting ZOHO_OAUTH_ENABLED with a client ID and secret adds a "Login
with Zoho" button to the login page. Zoho users are matched by a new
users.zoho_id column. An existing account with the same email is linked
to Zoho. Otherwise a new verified account is created, unless
ZOHO_ALLOW_SIGNUP is off, which is the default when registration is
disabled.

Also configurable: redirect URI, data-center accounts server, scopes,
allowed email domains and the tier for new accounts. All are documented
in .env.example and passed through in docker-compose.yml.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 12:20:46 +10:00
PhialsBasementandClaude Opus 5.5 26cce27bf0 Keep link-check-only external rows out of the Overview
External links that were only HEAD-checked (Crawl External off) now get
a link_check_only flag, saved with the crawl. The Overview leaves those
rows out, since those sites were not crawled; the External tab still
lists them. Off-site images and crawled pages are not flagged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 12:20:46 +10:00
PhialsBasementandClaude Opus 5.5 ce5616c472 External tab populated without crawling external sites
With Crawl External off, external <a> targets are now HEAD-checked like
images and get a synthesized row (status, type, size), so the External
tab and Links-tab statuses fill in without fetching the other site.
HEAD refusals (403/405/501) are confirmed with a streamed GET. Image
and external checks share one helper across both crawl paths.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 09:46:19 +10:00
PhialsBasementandClaude Opus 5.5 512e1b103e Fix: images hosted on a CDN never HEAD-checked
Off-domain images were skipped unless Crawl External was on, so sites
serving every image from a CDN (Wix, Shopify, Squarespace) got no image
statuses or Images-tab rows. Embedded images are page assets: check them
wherever they're hosted. Crawl External still gates off-domain pages.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 09:36:31 +10:00
Phiality d0ac5058cc Fix: alt="" was reported as missing alt text (#95)
extract_images used img.get('alt', ''), so an image with no alt attribute
and one correctly marked decorative with alt="" both became the empty
string. The detector then tested truthiness, and correct markup counted
as a defect.

Images now carry has_alt alongside alt, and only a missing attribute is
flagged. The warning also says how many decorative images were found, so
the count is explainable rather than just smaller.

The Links tab had the same conflation, showing "(no alt text)" for both;
it now distinguishes "(decorative)" from "(no alt attribute)".

Crawls saved before has_alt existed carry no flag. Those are left
unflagged rather than guessed at, since the old data cannot tell the two
cases apart. Re-crawl for accurate numbers.

Thanks to musclemintco for the diagnosis, which was exactly right.
2026-09-08 14:05:30 +10:00
PhialsBasementandClaude Fable 5.1 82f40a083a Fix: crawl stuck in "finishing up" on big sites; exports ignoring the chosen format
Duplicate-content detection compared every page with every other page using
SequenceMatcher and emitted two issues per matching pair. On a 1,127-URL site
with ~900 templated suburb pages that meant 4+ minutes of CPU, 703,902 issues,
and a 90-second SQLite insert that locked the database for every other
request (status polls, settings) — the UI sat in "finishing up" until the
crawl was force-stopped.

- issue_detector: one issue per page (count + closest five), identical
  title/description/H1 grouped first, candidate pairs from a prefix filter on
  title tokens, word-level Dice similarity, only 200 HTML pages considered,
  and a 120s time budget so the pass can never hang a crawl.
  900 templated pages: 1.4s / 900 issues (was ~4min / 703k).
- auth_db/crawl_db: 30s busy timeout and WAL mode so reads no longer fail
  with "database is locked" while the crawler saves a batch.

Exports: "Excel (XLSX)" was offered in Settings but never implemented (it
silently produced CSV), XML only applied to the URLs file (links/issues fell
back to CSV), and the export button read the format from the server copy of
the settings, which silently lags the browser copy when the background sync
fails — e.g. while the database was locked by the bug above.

- main: XLSX (openpyxl) and XML for URLs, links and issues on both the
  streaming and legacy export endpoints; shared row helpers.
- app.js/settings.js: export with the settings the browser is showing;
  surface a failed server sync instead of swallowing it; decode base64
  binary exports.
- tests: fixture tests for one-issue-per-page duplicate detection (crawl +
  1,500-page scale check) and for every format × every data type on both
  export endpoints.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 12:46:57 +10:00
Phiality 3c710d8ac8 Fix: response time included the JS render wait (#93, #94)
#93: with JS rendering on, response_time was measured around the whole
render, which includes the js_wait_time sleep. At the 3s default every
page cleared the 3000ms threshold and got flagged "Slow Response Time".
Reported on a site scoring 97-99 on PSI with all 54 pages flagged.

render_page now reports the browser's own request timing (responseEnd,
falling back to the measured navigation) as response_time, and the full
render separately as render_time. On librecrawl.com with a 3s wait that
is 957ms vs 4287ms, and nothing is flagged. render_time is stored, shown
in the URL details modal, and added to crawled_urls via migration.

#94: "URLs Discovered" ignored the rows synthesized from image HEAD
checks, since those are never queued. The counter sat at the HTML page
count while crawled climbed past it. Image rows are now marked
discovered when their row is created.

Fixture tests cover both.
2026-08-19 09:35:06 +10:00
Phiality d6b6a65aa7 Promote Plugin Workshop in README
Added a section to promote the Plugin Workshop for sharing and discovering plugins.
2026-08-14 12:47:24 +10:00
Phiality 1d8e70d27b feat: link to the Plugin Workshop
- Workshop button in the app header
- README and the plugins README point at it for finding/sharing plugins

The workshop is back up at workshop.librecrawl.com. On the marketing site
the /workshop redirect now goes there instead of to GitHub, and the nav
links point straight at the subdomain.
2026-08-14 12:45:53 +10:00
Phiality 26019fa37c Fix: connection pool too small for concurrency + image checks
requests defaults to 10 connections per host, but crawl workers and the
image HEAD checks (5 threads of their own) draw from the same pool, so
urllib3 kept discarding live connections and reopening them:

  WARNING:urllib3.connectionpool: Connection pool is full, discarding
  connection: developer.mozilla.org

Mount an adapter sized to concurrency + IMAGE_CHECK_WORKERS + 5, and
resize it when concurrency changes in settings. An MDN crawl went from a
steady stream of these warnings to none.
2026-08-14 12:45:44 +10:00
Phiality aaf5827294 Fix: crawl data sync, redirect resolution, max_urls accounting
Incremental polling sliced server arrays by the client's row count, so
updates to already-sent rows (linked_from, link statuses, image HEAD
results) never reached the UI, and the issue cursor desynced whenever an
exclusion pattern dropped one. Replaced with an event log (seq + epoch);
clients apply appends/updates by key.

- Relative links on redirected pages resolved against the requested URL,
  minting internal 404s from the target site's links; chains now recorded
- Image HEAD checks ignored crawl_external
- Image rows counted against max_urls (25-URL react.dev crawl = 1 page)
- _save_batch_to_db cleared its queues after writing, dropping rows
  appended mid-write (400-page Wikipedia crawl lost 3,693 links)
- Fetches in flight at max_urls hit the DB uncounted
- Sitemap discovery blocked start_crawl; now backgrounded, completion
  waits on it
- start_crawl read session_id before creation, disabling persistence
- Link/issue cells used innerHTML on crawled page content
- Virtual scroller measures real row heights, rAF-throttled, spacers
  track data length

Adds tests/: fixture tests (no network) + real-site harness.
2026-08-13 20:43:41 +10:00
PhialityandClaude Fable 5 a41542cedd Fix: streaming export issue keys, file-loaded crawl fallback (#48)
Follow-up to #48:
- Issues export used nonexistent keys (issue_type/severity/description);
  now matches the real issue dicts and legacy format (type/category/issue/details)
- Crawls loaded from a saved file exist only in browser state, which the
  streaming endpoint can't see — exportData() now falls back to the legacy
  POST /api/export_data path for those instead of downloading a 404 body
- target_status None renders as 'Not crawled' instead of blank (0 stays 0)
- Export field names from the query string are restricted to identifier-like
  strings since they become XML tag names and CSV headers

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 15:54:27 +10:00
liquidpurple d5a0d9a4aa Fix: export OOM on large crawls — add streaming export endpoint (#48)
* Add streaming export endpoint to main.py

* Replace exportData() with streaming download
2026-08-13 15:52:24 +10:00
Gaurav Chaudhary 082e1ac52c feat: display meta keywords in crawl results (#78)
Add Keywords column to the overview table, URL detail view, and export options.
2026-08-13 15:47:42 +10:00
liquidpurple c87a0a4561 Fix: stop_crawl() overwrites "completed" status (#51)
Race condition: the cleanup thread calls stop_crawl() after a crawl has already been marked "completed", changing its status back to "stopped".

Added a status check before overwriting — if the crawl is already
"completed", leave it alone.
2026-08-13 15:47:36 +10:00
PhialityandClaude Fable 5 4cf632b739 feat: make image crawling opt-in, populate Images tab via HEAD checks (#86, #87)
Follow-up to #87: full image downloads are now behind a 'Crawl Images'
checkbox (default off) instead of being in the default include_extensions.
When off, the Images tab is populated from the broken-image HEAD checks
that already run (status, content-type, size from Content-Length) at no
extra request cost. Also guards the issue detector against flagging
non-HTML results for missing titles/meta descriptions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 15:35:14 +10:00
SuoweiHu 769851b9ac fix: add image link to the discovery queue (#87)
* Fix: added additional logic to add image link to the discovery queue

* Fix: added image extension to the included extensions
2026-08-13 15:26:30 +10:00
Abdul Aouwal 8f6dedb32d fix: ensure data directory exists before init_db call (#79)
sqlite3.connect() fails with 'unable to open database file' on fresh
installs where data/ directory does not exist. This happens with both
Docker (bind mount creates ./data as root, container user can't write)
and bare Python (no data/ directory at all).

Fix ensures data/ exists before any database operation:
- main.py: add os.makedirs before init_db() call
- start-librecrawl.sh: mkdir -p data at script start
- start-librecrawl.bat: mkdir data at script start

Fixes #65
2026-08-13 15:14:32 +10:00
PhialityandClaude Fable 5 d98ff55d84 Fix: disable export/save buttons while processing (#75)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 15:14:22 +10:00
PhialityandClaude Fable 5 29c5e50c3e Fix: normalize bare domain URLs to prevent false duplicate crawls (#88)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 15:14:22 +10:00
a59c0835cb security: derive Flask SECRET_KEY from env with secure random fallback (#41, #71)
app.secret_key was hardcoded to a value committed in the public repo, so
every stock deployment shared the same Flask session-signing key — anyone
could forge a signed session cookie (including tier=admin) for any instance.

Now read SECRET_KEY from the environment, falling back to a per-process
secrets.token_hex(32) random key (with a startup warning) when unset. The
fallback is intentionally ephemeral rather than a static default, so no
public key is ever used — worst case is sessions not surviving a restart,
not forgeable admin cookies. Documents SECRET_KEY in .env.example and README.

Combines the secure fallback approach from #71 with the documentation from
#41; applied directly since #71 carried whole-file line-ending churn and
#41 kept an insecure static fallback.

Co-authored-by: getuslocal <getuslocal@users.noreply.github.com>
Co-authored-by: Mobilpadde <Mobilpadde@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 14:40:54 +10:00
edd6ce06c6 feat: read LOCAL_MODE from environment variable (#40)
Wire LOCAL_MODE via os.getenv so it can be set through .env / docker-compose,
matching how DISABLE_GUEST/REGISTRATION_DISABLED already work. The
REGISTRATION_DISABLED half of #40 already landed via #53, leaving this single
line; applied directly since #40 was a fork branch that conflicted after #53.
Safe now that LOCAL_MODE defaults to false (e48141d).

Co-authored-by: Mobilpadde <Mobilpadde@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 11:33:32 +10:00
PhialityandClaude Opus 4.8 e48141d0ec security: default LOCAL_MODE to false to prevent insecure-by-default deploys
LOCAL_MODE disables all authentication and grants every visitor admin
tier. Both the documented setup path (cp .env.example .env) and the
docker-compose default shipped LOCAL_MODE=true, so a stock deployment
exposed an unauthenticated admin server. Flip both defaults to false and
add explicit warnings. This also makes it safe to wire LOCAL_MODE as an
env var (PR #40).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 11:31:55 +10:00
liquidpurple 028a7640b9 Fix: hide Resume button on completed crawls (#52)
The Resume button was shown for completed crawls, but resuming a completed crawl always fails. Only render the button for statuses
that can actually be resumed: paused, failed, running, and stopped.
2026-06-01 11:25:20 +10:00
liquidpurple 683f484d79 Fix: resume fails for stopped and failed crawls (#50)
Only "paused" and "running" statuses were allowed in the resume logic. Stopped and failed crawls returned a misleading "Cannot resume crawl with status: stopped" error despite being resumable.

Added "stopped" and "failed" to the list of resumable statuses.
2026-06-01 11:25:17 +10:00
liquidpurple 3838213864 Fix: REGISTRATION_DISABLED and DISABLE_GUEST env vars not read (#53)
Only the CLI arguments (--disable-register, --disable-guest) were checked. The environment variable path was missing, so setting
REGISTRATION_DISABLED=true or DISABLE_GUEST=true had no effect.

Added os.getenv() fallback for both flags, accepting "true", "1", or "yes".
2026-06-01 11:25:15 +10:00
Russell Heimlich 6839c5c184 feat: allow configuring external port via environment variable (#63)
* Change external port binding for librecrawl service

Adds support to specify the `EXTERNAL_PORT` to bind to. Falls back to :5000 if `EXTERNAL_PORT` is not set.

Port 5000 would conflict with Synology NAS if installed there.

* Add EXTERNAL_PORT to .env.example

Added EXTERNAL_PORT variable for application port binding.
2026-06-01 11:25:11 +10:00
pantic14 a51c8884c1 chore: add .env to .gitignore to prevent credential leaks (#67) 2026-06-01 11:25:08 +10:00
Phiality f2be7a33ec Detect broken images and collect img src as links
- collect_all_links() now also collects <img src> URLs as links with
  placement='image', so they appear in the Links tab alongside <a href>
  links.
- After collecting images per page, HEAD-check their URLs (parallel,
  cached, capped at 50/page) and store the HTTP status.
- Issue detector flags broken images (4xx/5xx/0) as Content errors.
- Removed the hard-coded 20-image cap in extract_images().

Closes #61
2026-04-25 22:06:50 +10:00
Phiality 76cbefae8a Fix recursive crawling and add No Response filter
- Remove forced max_depth=0 override when the starting URL has a path
  component. The crawler now respects the user's configured depth
  regardless of whether the URL is a domain root or a subpath like
  /blog. Closes #59

- Add No Response / DNS Not Found filter (#44): classify fetch errors
  (dns_not_found, timeout, connection_refused, ssl_error) and surface
  them via a new sidebar filter, per-error-type rows in the Status
  Codes table, issue detection, and DB persistence.
2026-04-25 21:52:46 +10:00
Phiality 75d601da0d Merge pull request #58 from obriat/docker-compose-v2
Move to docker compose V2
2026-04-18 15:25:06 +10:00
Phiality 9f59f57b3c Merge remote-tracking branch 'origin/main' into docker-compose-v2
# Conflicts:
#	docker-compose.yml
#	main.py
2026-04-18 15:24:16 +10:00
Phiality f4ea2bc3ef Add demo mode and dangerously-skip-auth flag
- Demo mode (--demo / DEMO_MODE): per-user 1.5GB memory limit with
  auto-stop and incremental memory accounting via UserMemoryTracker.
- Dangerously skip auth (--dangerously-skip-auth / DANGEROUSLY_SKIP_AUTH):
  log in as any username with no password; username only separates
  per-user sessions.
- README: minor update.
2026-04-18 15:09:32 +10:00
Olivier Briat 5d6fd8f1ab Move to docker compose V2 2026-04-17 10:39:06 +02:00
Phiality e737189e11 Update README.md 2026-04-15 12:31:21 +10:00
PhialAIIMS e49ecf2d9b Added demo mode as seperate mode for limiting memory usage per user in demo 2026-03-28 06:11:57 +11:00
Phiality 49da406a12 Merge pull request #31 from NvhNvhsi/patch-1
Update docker-compose.yml to add SMTP comments
2026-02-08 20:33:56 +11:00
Phiality 3b1cc93b82 Fix: robots.txt check blocking all URLs due to urllib User-Agent (#29)
RobotFileParser.read() uses urllib with Python's default UA header.
Many servers return 403 for that, causing RobotFileParser to set
disallow_all=True and block every URL. Now fetches robots.txt with
the crawler's requests session which has the proper User-Agent.
2026-02-08 17:54:29 +11:00
Phiality 63ce0dd1e1 Fix: visualization hierarchy flat + loaded crawl data not rendering (#28)
- Hierarchical layout now uses crawl depth data instead of BFS graph
  distance, which was flat because nav links make every page 1 hop from root
- Added depth to node data in both backend and frontend
- Loaded crawl data no longer overwritten when opening visualization tab
2026-02-08 17:38:34 +11:00
Phiality bdc059af2c Feat: add --disable-guest / DISABLE_GUEST env var (#24)
Adds a flag to disable guest login, same pattern as --disable-register.
Supports both CLI arg (--disable-guest / -dg) and .env var (DISABLE_GUEST=true).
2026-02-08 17:10:27 +11:00
Phiality abdd6b7109 Fix: users.db not persisting to mapped Docker volume (#23)
DB path was 'users.db' (resolving to /app/users.db) but the Docker
volume maps ./data:/app/data. Changed all DB paths to data/users.db
so the database is created inside the mounted volume.
2026-02-08 17:08:03 +11:00
NvhNvhsi 31f54e0bc7 Update docker-compose.yml to add SMTP comments 2026-01-04 12:59:59 +01:00
Phiality 77c3534ceb Fix: missing playwright in non-root (closes #18) 2025-11-25 17:17:53 +11:00
Phiality 336a5c2d0d Update README.md 2025-11-24 02:50:25 +00:00
Phiality effedd04fa Feat: duplication checks & DB Persistance (closes #9 and #7) 2025-11-23 23:18:49 +11:00
Phiality f184d4b26f Fix: Line Endings (Closes #15) 2025-11-23 18:56:40 +11:00
Phiality aee0cb23b2 Feat: Add registration, email service, single-url crawling etc 2025-11-23 11:36:23 +11:00
Phiality a98e810805 Update README.md 2025-11-21 13:30:56 +00:00