Application Log
+Streaming the latest lines from logs/app.log.
diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..88974cf Binary files /dev/null and b/.DS_Store differ diff --git a/.RData b/.RData new file mode 100644 index 0000000..8cf8ef6 Binary files /dev/null and b/.RData differ diff --git a/.Rhistory b/.Rhistory new file mode 100644 index 0000000..b76d5c8 --- /dev/null +++ b/.Rhistory @@ -0,0 +1,3 @@ +install.packages('jsonlite') +q +q() diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..192b97e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.git +.gitignore +.DS_Store +node_modules +vendor +uploads/* +!uploads/**/.gitkeep +.env diff --git a/.env b/.env new file mode 100644 index 0000000..623cc8c --- /dev/null +++ b/.env @@ -0,0 +1,21 @@ +JUPYTER_EXTERNAL_URL=https://hub.niph.org.kh +JUPYTERHUB_PORT=443 +JUPYTERHUB_AUTH_STRATEGY=oauth +JUPYTERHUB_DUMMY_PASSWORD= +JUPYTERHUB_COOKIE_SECURE=true +JUPYTERHUB_HTTP_TIMEOUT=90 +JUPYTERHUB_START_TIMEOUT=90 +JUPYTERHUB_USER_PATH=hub/ +JUPYTERHUB_USERNAME_TEMPLATE=user_{person_id} +DSP_WORKSPACE_ROOT=/home/niph_dev/Documents/dsp/uploads/jupyter_workspace +DSP_OAUTH_CLIENT_ID=hub-client +DSP_OAUTH_CLIENT_SECRET=hub-client-secret-20251103 +DSP_OAUTH_REDIRECT_URIS=https://hub.niph.org.kh/hub/oauth_callback +DSP_OAUTH_AUTHORIZE_URL=https://dsp.niph.org.kh/oauth/authorize +DSP_OAUTH_TOKEN_URL=https://dsp.niph.org.kh/oauth/token +DSP_OAUTH_USERINFO_URL=https://dsp.niph.org.kh/oauth/userinfo +JUPYTERHUB_OAUTH_CALLBACK=https://hub.niph.org.kh/hub/oauth_callback +JUPYTERHUB_CULL_API_TOKEN=kCc1EzULTX2jG8jyvOTA0B6vrMk5SGmWjwDvNqlQ2wY +DSP_APP_ORIGINS="https://dsp.niph.org.kh https://hub.niph.org.kh http://localhost:8082 http://192.168.170.226:8082" +DSP_FRAME_ANCESTORS="https://dsp.niph.org.kh https://hub.niph.org.kh http://localhost:8082 http://192.168.170.226:8082" +APP_PORT=443 diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..c73412a --- /dev/null +++ b/.htaccess @@ -0,0 +1,8 @@ +RewriteEngine On +RewriteBase / + +# Serve existing PHP files when the extension is omitted +RewriteCond %{REQUEST_FILENAME} !-d +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME}.php -f +RewriteRule ^(.+)$ $1.php [L] diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..12b37bf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,43 @@ +# Dockerfile for DSP PHP application with R support +FROM php:8.2-apache + +# Install system dependencies and PHP extensions +RUN apt-get update && apt-get install -y \ + libpng-dev \ + libonig-dev \ + libzip-dev \ + zip \ + unzip \ + git \ + default-mysql-client \ + r-base \ + && docker-php-ext-install pdo_mysql \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Install required R packages +RUN Rscript -e "install.packages('jsonlite', repos='https://cloud.r-project.org')" + +# Enable Apache modules commonly used by PHP apps +RUN a2enmod rewrite + +# Set working directory +WORKDIR /var/www/html + +# Copy project files into the container (can be overridden by bind mount in docker-compose) +COPY . /var/www/html + +# Ensure upload directories are writable by the web server +RUN chown -R www-data:www-data /var/www/html/uploads + +# Increase default PHP upload limits +COPY docker/custom.ini /usr/local/etc/php/conf.d/uploads.ini + +# Copy entrypoint script +COPY docker/app-entrypoint.sh /usr/local/bin/app-entrypoint.sh +RUN chmod +x /usr/local/bin/app-entrypoint.sh + +# Expose Apache port +EXPOSE 80 + +ENTRYPOINT ["app-entrypoint.sh"] diff --git a/admin/app_log.php b/admin/app_log.php new file mode 100644 index 0000000..4ac1f7a --- /dev/null +++ b/admin/app_log.php @@ -0,0 +1,119 @@ + null, + 'modified' => null, +]; + +if ($canReadLog) { + $logMeta['size'] = filesize($logPath); + $logMeta['modified'] = filemtime($logPath); + + if (isset($_GET['download'])) { + header('Content-Type: text/plain'); + header('Content-Disposition: attachment; filename="app.log"'); + header('Content-Length: ' . $logMeta['size']); + readfile($logPath); + exit; + } + + try { + $logEntries = tail_file($logPath, $lineLimit); + } catch (RuntimeException $e) { + $canReadLog = false; + $logError = $e->getMessage(); + } +} else { + $logError = 'The application log file is missing or unreadable.'; +} + +function tail_file(string $path, int $lines): array +{ + $file = new SplFileObject($path, 'r'); + $file->seek(PHP_INT_MAX); + $lastLine = $file->key(); + $startLine = max($lastLine - $lines + 1, 0); + $file->seek($startLine); + + $buffer = []; + while (!$file->eof()) { + $buffer[] = rtrim($file->current(), "\r\n"); + $file->next(); + } + + return $buffer; +} + +$lastUpdated = $logMeta['modified'] ? date('Y-m-d H:i:s', $logMeta['modified']) . ' UTC' : 'Unknown'; +$logSizeHuman = $logMeta['size'] !== null ? number_format($logMeta['size'] / 1024, 1) . ' KB' : 'Unknown'; + +?> + + + +
+Streaming the latest lines from logs/app.log.
| ID | +Title | +Description | +Reg. Date | +Mod. Date | +Actions | +
|---|---|---|---|---|---|
| + | + | 100 ? '...' : ''); ?> | ++ | + | + + + + + | +
| No About Us entries found. | +|||||
| ID | +Title | +Description | +Photo | +Status | +Reg. Date | +Mod. Date | +Actions | +
|---|---|---|---|---|---|---|---|
| + | + | 100 ? '...' : ''); ?> | +
+
+ |
+ + | + | + | + + + + + | +
| No announcements found. | +|||||||
| ID | +English Name | +Khmer Name | +Actions | +
|---|---|---|---|
| + | + | + | + + + + + | +
| No data types found. | +|||
| ID | +English Title | +Details | +Actions | +
|---|---|---|---|
| + | + | + | + + + + + | +
| No categories found. | +|||
| ID | +Name | +Message | +Status | +Submitted On | +Actions | +|
|---|---|---|---|---|---|---|
| + | + | + | 100 ? '...' : ''); ?> | ++ | + | + + Respond + + + | +
| No feedback messages found. | +||||||
| ID | +Question | +Answer | +Reg. Date | +Mod. Date | +Actions | +
|---|---|---|---|---|---|
| + | + | 100 ? '...' : ''); ?> | ++ | + | + + + + + | +
| No FAQ entries found. | +|||||
| Req. ID | +Data Source | +Requester | +Data Owner | +Permission Type | +Request Notes | +Proof | +Status | +Requested On | +Actions | +
|---|---|---|---|---|---|---|---|---|---|
| + | + | + | + | + | —'; ?> | ++ + + + View + + + N/A + + | ++ + | ++ | + + | +
| No permission requests found. | +|||||||||
| ID | +Title | +Description | +Photo | +Reg. Date | +Actions | +
|---|---|---|---|---|---|
| + | + | 100 ? '...' : ''); ?> | +
+
+ |
+ + | + + + + + | +
| No slides found. | +|||||
| No. | +Username | +Full Name | +Phone | +Current Role | +R/Jupyter | +Actions | +|
|---|---|---|---|---|---|---|---|
| + | + | + | + | + | + + + + + | ++ + Enabled + + Disabled + + | +
+
+
+
+
+ |
+
| No users match the current filters. | +|||||||
+ Approved data sources have been synced to
+ = htmlspecialchars($workspaceRelativeDir ?: 'datasources') ?>
+ inside the Jupyter environment. Only files you are approved to use are available.
+
| # | +Data Source | +Data Type | +Category | +Filename | +
|---|---|---|---|---|
| = $idx + 1 ?> | += htmlspecialchars($syncedItem['title']) ?> | += htmlspecialchars($syncedItem['data_type'] ?? 'N/A') ?> | += htmlspecialchars($syncedItem['category'] ?? 'N/A') ?> | += htmlspecialchars(basename($syncedItem['relative_path'])) ?> |
+
+ Use the embedded Jupyter workspace to manage R notebooks, explore uploaded datasets, and + collaborate with Data Owners. This view runs with your admin permissions. +
++ Prefer the full window? Open Jupyter in a new tab. +
+ ++ Once access is enabled, refresh this page to launch the JupyterLab workspace. +
+ ++ 100) { + $shortDescription .= '...'; + } + echo htmlspecialchars($shortDescription); + ?> +
+We couldn't find any data sources matching your criteria. Try adjusting your search term or selecting a different category.
+150 ? '...' : ''); ?>
+= $my_data_sources_count ?>
+= $pending_permissions_count ?>
+= $data_accesses_last_30_days ?>
+| ID | +Data Source | +Requested By | +Permission Type | +Requested Date | +Notes | +Proof | +Actions | +
|---|---|---|---|---|---|---|---|
| = htmlspecialchars($req['pkdspsdsp_id']) ?> | += htmlspecialchars($req['dspsds_title_en']) ?> | += htmlspecialchars($req['isp_firstname_en'] . ' ' . $req['isp_lastname_en']) ?> | += htmlspecialchars($req['dspsdsp_permission']) ?> | += date('Y-m-d H:i', strtotime($req['dspsdsp_reg_datetime'])) ?> | ++ —'; + ?> + | ++ + + + View + + + N/A + + | ++ + + | +
| ID | +Data Source | +Requested By | +Permission Type | +Status | +Requested Date | +Notes | +Proof | +Actions | +
|---|---|---|---|---|---|---|---|---|
| = htmlspecialchars($req['pkdspsdsp_id']) ?> | += htmlspecialchars($req['dspsds_title_en']) ?> | += htmlspecialchars($req['isp_firstname_en'] . ' ' . $req['isp_lastname_en']) ?> | += htmlspecialchars($req['dspsdsp_permission']) ?> | ++ + = htmlspecialchars($req['dspsdsp_status']) ?> + + | += date('Y-m-d H:i', strtotime($req['dspsdsp_reg_datetime'])) ?> | ++ —'; + ?> + | ++ + + + View + + + N/A + + | ++ + + + No action + + | +
| Data Source | +Type | +Download Date | +File | +
|---|---|---|---|
| = htmlspecialchars($item['dspsds_title_en']) ?> | += htmlspecialchars($item['dspstds_name_en']) ?> | += date('Y-m-d H:i', strtotime($item['dspsdspused_datetime'])) ?> | ++ + + + Download Again + + + N/A (API or no direct file) + + | +
| Data Source | +Requested For | +Date Submitted | +Status | +
|---|---|---|---|
| = htmlspecialchars($request['ds_title']) ?> | += htmlspecialchars($request['dspspr_permission_type']) ?> | += htmlspecialchars(date('M d, Y', strtotime($request['dspspr_request_date']))) ?> | ++ + = htmlspecialchars($request['dspspr_status']) ?> + | +
+ Only Approved data sources are copied into
+ = htmlspecialchars($workspaceRelativeDir ?: 'datasources') ?>
+ inside Jupyter. Use these files when collaborating with Data Owners.
+
| # | +Data Source | +Data Type | +Category | +Filename | +
|---|---|---|---|---|
| = $idx + 1 ?> | += htmlspecialchars($syncedItem['title']) ?> | += htmlspecialchars($syncedItem['data_type'] ?? 'N/A') ?> | += htmlspecialchars($syncedItem['category'] ?? 'N/A') ?> | += htmlspecialchars(basename($syncedItem['relative_path'])) ?> |
+
+ Launch the embedded JupyterLab environment to build notebooks, explore shared datasets, and collaborate with Data Owners. + Use the Files panel to open existing work or create a new R Notebook from the launcher. +
++ Prefer a dedicated window? Open Jupyter in a new tab. +
+ ++ Once access is enabled, refresh this page to launch the JupyterLab environment. +
+ += $my_data_sources_count ?>
+= $pending_permissions_count ?>
+= $data_accesses_last_30_days ?>
+| ID | +Data Source | +Requested By | +Permission Type | +Requested Date | +Notes | +Proof | +Actions | +
|---|---|---|---|---|---|---|---|
| = htmlspecialchars($req['pkdspsdsp_id']) ?> | += htmlspecialchars($req['dspsds_title_en']) ?> | += htmlspecialchars($req['isp_firstname_en'] . ' ' . $req['isp_lastname_en']) ?> | += htmlspecialchars($req['dspsdsp_permission']) ?> | += date('Y-m-d H:i', strtotime($req['dspsdsp_reg_datetime'])) ?> | ++ —'; + ?> + | ++ + + + View + + + N/A + + | ++ + + | +
| ID | +Data Source | +Requested By | +Permission Type | +Status | +Requested Date | +Notes | +Proof | +Actions | +
|---|---|---|---|---|---|---|---|---|
| = htmlspecialchars($req['pkdspsdsp_id']) ?> | += htmlspecialchars($req['dspsds_title_en']) ?> | += htmlspecialchars($req['isp_firstname_en'] . ' ' . $req['isp_lastname_en']) ?> | += htmlspecialchars($req['dspsdsp_permission']) ?> | ++ + = htmlspecialchars($req['dspsdsp_status']) ?> + + | += date('Y-m-d H:i', strtotime($req['dspsdsp_reg_datetime'])) ?> | ++ —'; + ?> + | ++ + + + View + + + N/A + + | ++ + + + No action + + | +
= $total_downloads ?>
+1500
+
+ Approved data sources you manage or have access to are synced to
+ = htmlspecialchars($workspaceRelativeDir ?: 'datasources') ?> inside Jupyter.
+ Only datasets with Approved permissions appear here.
+
| # | +Data Source | +Data Type | +Category | +Filename | +
|---|---|---|---|---|
| = $idx + 1 ?> | += htmlspecialchars($syncedItem['title']) ?> | += htmlspecialchars($syncedItem['data_type'] ?? 'N/A') ?> | += htmlspecialchars($syncedItem['category'] ?? 'N/A') ?> | += htmlspecialchars(basename($syncedItem['relative_path'])) ?> |
+
+ Launch the embedded Jupyter environment below to build, run, and share your R notebooks. Use the + toolbar menu inside Jupyter to create new R notebooks and access saved work under Files. +
++ Need more room? Open Jupyter in a new tab. +
+ ++ After your access is approved, refresh this page to launch the embedded notebook environment. +
+ +150 ? '...' : ''); ?>
+= $approved_datasources_count ?>
+= $pending_requests_count ?>
+= $my_downloads_count ?>
+| Data Source | +Type | +Download Date | +File | +
|---|---|---|---|
| = htmlspecialchars($item['dspsds_title_en']) ?> | += htmlspecialchars($item['dspstds_name_en']) ?> | += date('Y-m-d H:i', strtotime($item['dspsdspused_datetime'])) ?> | ++ + + + Download Again + + + N/A (API or no direct file) + + | +
| Data Source | +Requested For | +Date Submitted | +Proof | +Status | +
|---|---|---|---|---|
| = htmlspecialchars($request['ds_title']) ?> | += htmlspecialchars($request['dspspr_permission_type']) ?> | += htmlspecialchars(date('M d, Y', strtotime($request['dspspr_request_date']))) ?> | ++ + + View + + + N/A + + | ++ + = htmlspecialchars($request['dspspr_status']) ?> + | +
+ Approved datasets are available inside Jupyter at
+ = htmlspecialchars($workspaceRelativeDir ?: 'datasources') ?>.
+ Only data sources you have Approved access to will appear.
+
| # | +Data Source | +Data Type | +Category | +Filename | +
|---|---|---|---|---|
| = $idx + 1 ?> | += htmlspecialchars($syncedItem['title']) ?> | += htmlspecialchars($syncedItem['data_type'] ?? 'N/A') ?> | += htmlspecialchars($syncedItem['category'] ?? 'N/A') ?> | += htmlspecialchars(basename($syncedItem['relative_path'])) ?> |
+
+ Use the embedded JupyterLab session to explore approved datasets, run R notebooks, or collaborate with Data Owners and DAC Staff. +
++ Need more space? Open Jupyter in a new tab. +
+ ++ After your access is approved, revisit this page to launch the notebook workspace. +
+ ++ Configuration guidance (defaults, overrides, and security notes) now lives on the + Install & Configuration + page under R in JupyterHub Service. +
++ Use the snapshot below to confirm how this environment is currently resolving the notebook endpoint. +
+= htmlspecialchars($resolvedBaseUrl) ?>= htmlspecialchars($resolvedPort) ?>= htmlspecialchars($resolvedToken) ?>= htmlspecialchars($workspaceBaseMessage) ?>= htmlspecialchars($variable) ?>: = htmlspecialchars($value) ?>No overrides detected; defaults from the Docker stack are in effect.
+ +