Initial commit: multi-bot CollabCentral
Extends the single-Novi codebase into a multi-bot mass-messenger where each bot has its own token, avatar, label, and per-user authorization. - Secrets moved out of config.json: per-bot tokens in gitignored config/botTokens.json (with enabled flag), service-account OAuth in gitignored config/token.json (rewritten by refresh cron), integration and Google keys in .env. - Single Webex integration handles OAuth for all bots via a per-app redirect URI derived from OAUTH_CALLBACK_URL_TEMPLATE. - New requireBot middleware and getBotConfig helper reject requests for unknown or disabled bots at the /CollabCentral/:app boundary. - New /info endpoint plus dynamic frontend loading (sendMessage, monitorJobs) so pages self-describe per bot, including bot avatar fetched from Webex /people/me at startup. - Job draft state keyed by cookieId + appName so each bot has its own building queue; job list/detail endpoints filter by appName so users only see jobs from bots they are authorized on. - New jobDetail page for a readable per-job view; completed jobs are retained for 30 days by the cleanup cron. - Completion adaptive cards use per-bot avatar and label. - Miscellaneous fixes: off-by-two in the send loop, removed three dead send/process variants, added defensive init for jobs.* on load, dropped the deprecated crypto npm shim, and cleaned up stray logger labels and typos. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
commit
e604c7e9c9
27 changed files with 4454 additions and 0 deletions
14
.dockerignore
Normal file
14
.dockerignore
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
node_modules
|
||||
logs
|
||||
uploads
|
||||
|
||||
# Config is mounted at runtime (-v ./config:/usr/src/app/config). Never bake it
|
||||
# into the image, especially secrets like .env / botTokens.json / token.json.
|
||||
.env
|
||||
config
|
||||
|
||||
# Misc
|
||||
testData.json
|
||||
.DS_Store
|
||||
*.log
|
||||
.git
|
||||
26
.env.example
Normal file
26
.env.example
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# --- Server ----------------------------------------------------------------
|
||||
SERVER_PORT=1450
|
||||
|
||||
# --- OAuth callback URL template ------------------------------------------
|
||||
# The `:app` placeholder is replaced per-request with the bot's appName.
|
||||
# IMPORTANT: every bot's resolved URL must be registered as an allowed
|
||||
# redirect URI in the Webex integration portal
|
||||
# (developer.webex.com -> My Apps -> <integration> -> Redirect URIs).
|
||||
#
|
||||
# Example: with the template below and bots `novi` and `techupdates`, register:
|
||||
# https://bot.example.com/CollabCentral/novi/oauth
|
||||
# https://bot.example.com/CollabCentral/techupdates/oauth
|
||||
#
|
||||
# When you add a new bot, register its redirect URI before users try to log in.
|
||||
OAUTH_CALLBACK_URL_TEMPLATE=https://bot.example.com/CollabCentral/:app/oauth
|
||||
|
||||
# --- Webex integration (single integration used by all bots' OAuth) -------
|
||||
WEBEX_INTEGRATION_CLIENT_ID=your-integration-client-id
|
||||
WEBEX_INTEGRATION_CLIENT_SECRET=your-integration-client-secret
|
||||
|
||||
# --- Webex service account (used for group/people lookups) ----------------
|
||||
WEBEX_SERVICE_ACCOUNT_CLIENT_ID=your-service-account-client-id
|
||||
WEBEX_SERVICE_ACCOUNT_CLIENT_SECRET=your-service-account-client-secret
|
||||
|
||||
# --- Google Translate -----------------------------------------------------
|
||||
GOOGLE_TRANSLATE_API_KEY=your-google-translate-api-key
|
||||
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
node_modules/
|
||||
|
||||
# Secrets
|
||||
.env
|
||||
config/botTokens.json
|
||||
config/token.json
|
||||
|
||||
# Runtime data
|
||||
config/jobs.json
|
||||
config/jobs copy.json
|
||||
config/userPrefs.json
|
||||
uploads/*
|
||||
!uploads/.gitkeep
|
||||
|
||||
# Misc
|
||||
testData.json
|
||||
.DS_Store
|
||||
*.log
|
||||
logs/
|
||||
19
Dockerfile
Normal file
19
Dockerfile
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
FROM node:20
|
||||
# Create app directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Install app dependencies
|
||||
# A wildcard is used to ensure both package.json AND package-lock.json are copied
|
||||
# where available (npm@5+)
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install
|
||||
# If you are building your code for production
|
||||
# RUN npm ci --only=production
|
||||
|
||||
# Bundle app source
|
||||
COPY . .
|
||||
|
||||
EXPOSE 1450
|
||||
|
||||
CMD [ "node", "index.js" ]
|
||||
10
config/botTokens.example.json
Normal file
10
config/botTokens.example.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"novi": {
|
||||
"token": "REPLACE-WITH-NOVI-BOT-TOKEN",
|
||||
"enabled": true
|
||||
},
|
||||
"techupdates": {
|
||||
"token": "REPLACE-WITH-TECHUPDATES-BOT-TOKEN",
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
265
config/config.json
Normal file
265
config/config.json
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
{
|
||||
"server": {
|
||||
"name": "CollabCentral",
|
||||
"port": "1450"
|
||||
},
|
||||
"languages": [],
|
||||
"webex": {
|
||||
"bot": {
|
||||
"novi": {
|
||||
"label": "Novi Communicator",
|
||||
"authorized": {
|
||||
"Y2lzY29zcGFyazovL3VzL1BFT1BMRS80NzAyNDlhNC1kNjFjLTQzNmMtYTE1My1kOGUzZTExMmI4MDU": {
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1BFT1BMRS80NzAyNDlhNC1kNjFjLTQzNmMtYTE1My1kOGUzZTExMmI4MDU",
|
||||
"displayName": "Joe McQueen",
|
||||
"email": "mcqueenj@ae.com",
|
||||
"avatar": "https://avatar-prod-us-east-2.webexcontent.com/Avtr~V1~db9cdc31-8f8e-40b4-916c-8d9e9c92e70d/V1~667e3673b56f4ddc39139c73da9140dd631eb821a756524ead12e37e3a61afc7~9c5b6e767cff46e3af35a003cea38f25~1600",
|
||||
"groups": [
|
||||
{
|
||||
"name": "Group: Audio Visual",
|
||||
"alias": "AV Team",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZTQ3NmY3MjktZWViYi00MDI3LWFlMTctNzc1YWI1ZDgzYmNhOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: South Side Campus",
|
||||
"alias": "SSW Campus",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvYjAxOWM5MzgtNTZhNi00NTQxLTk0NjgtYjFmOWEwM2I5NTNlOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: IT Warrendale",
|
||||
"alias": "Warrendale",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvMjkwN2RlZGUtZjJmNi00OTdjLTkwMjItZTRmOGIzNzM3MTFhOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: New York Design Office Personnel",
|
||||
"alias": "NY Design Center",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvNjUzOGQzYmYtY2UzOS00Y2ZlLWFmZWUtNGViYTM0OGQ3Y2FjOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: 34th Street Office Personnel",
|
||||
"alias": "34th Street",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZGVmOTA0MDMtMTBlZS00YzRlLWIzYWItYzNjZDlkODhmMTQ1OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: San Francisco Personnel",
|
||||
"alias": "San Francisco Office",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvNjJjYTcxYTktYWYzYy00YjQ4LWIxZjUtMjc1NDEwNGYwMWJlOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: Ottawa Campus",
|
||||
"alias": "Ottawa Campus",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZWIyYWI1ZDgtYzk2Ny00MmUwLTg0OWQtZGE5OTYzMDdkNDhhOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: Hazleton Campus",
|
||||
"alias": "Hazleton Campus",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvMDc3NTI1NGEtZTIxMS00ZTQ1LTgxNjktNjYxYTAzOGYxNTE3OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: Canada Distribution Center",
|
||||
"alias": "Canada DC",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZmUwMDk1MjUtMzEyZC00YzMxLTgwMzItNWEyODIwODRiYTVjOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: Mexico Office Personnel",
|
||||
"alias": "Mexico Office",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvNjhmYTJiZGUtNjI2My00NWEwLTljMDMtNjMyMGI0NmVlODE0OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: Hong Kong Office",
|
||||
"alias": "Hong Kong Office",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvMjdlM2QzZGEtZjI4Ny00NDg3LWI3NzUtM2JmZTA3MzNhNzZhOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: Hazleton Mgmt",
|
||||
"alias": "Hazleton Mgmt",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvYTEzODI3OTgtOWY5Zi00YmY1LTlkMmQtODNhZTk2OGY0MjU3OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: Ottawa Campus Mgmt",
|
||||
"alias": "Ottawa Campus Mgmt",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZDYwNzFhYmMtYzNjZC00ZDdkLWFiYTktOGY0NDRiZTViODQ3OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: MDC DC Mgmt",
|
||||
"alias": "Canada DC Mgmt",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvOTk2ZWRmNzYtNjdlYS00MjdkLWEzNGEtMTBjNjJhYmRkMDM5OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Y2lzY29zcGFyazovL3VzL1BFT1BMRS84MWUzMzExMi1jYWZlLTQ0NWYtOGE5OS01MDI3Mzg4ZDQwNGM": {
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1BFT1BMRS84MWUzMzExMi1jYWZlLTQ0NWYtOGE5OS01MDI3Mzg4ZDQwNGM",
|
||||
"displayName": "Megan Reinecker",
|
||||
"email": "reineckerm@ae.com",
|
||||
"avatar": "https://avatar-prod-us-east-2.webexcontent.com/Avtr~V1~db9cdc31-8f8e-40b4-916c-8d9e9c92e70d/V1~81e33112-cafe-445f-8a99-5027388d404c~c5234572b50e49ff884c45b3ea0e3fea~1600",
|
||||
"groups": [
|
||||
{
|
||||
"name": "Group: South Side Campus",
|
||||
"alias": "SSW Campus",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvYjAxOWM5MzgtNTZhNi00NTQxLTk0NjgtYjFmOWEwM2I5NTNlOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: IT Warrendale",
|
||||
"alias": "Warrendale",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvMjkwN2RlZGUtZjJmNi00OTdjLTkwMjItZTRmOGIzNzM3MTFhOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: New York Design Office Personnel",
|
||||
"alias": "NY Design Center",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvNjUzOGQzYmYtY2UzOS00Y2ZlLWFmZWUtNGViYTM0OGQ3Y2FjOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: 34th Street Office Personnel",
|
||||
"alias": "34th Street",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZGVmOTA0MDMtMTBlZS00YzRlLWIzYWItYzNjZDlkODhmMTQ1OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: San Francisco Personnel",
|
||||
"alias": "San Francisco Office",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvNjJjYTcxYTktYWYzYy00YjQ4LWIxZjUtMjc1NDEwNGYwMWJlOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: Ottawa Campus",
|
||||
"alias": "Ottawa Campus",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZWIyYWI1ZDgtYzk2Ny00MmUwLTg0OWQtZGE5OTYzMDdkNDhhOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: Hazleton Campus",
|
||||
"alias": "Hazleton Campus",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvMDc3NTI1NGEtZTIxMS00ZTQ1LTgxNjktNjYxYTAzOGYxNTE3OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: Canada Distribution Center",
|
||||
"alias": "Canada DC",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZmUwMDk1MjUtMzEyZC00YzMxLTgwMzItNWEyODIwODRiYTVjOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: Mexico Office Personnel",
|
||||
"alias": "Mexico Office",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvNjhmYTJiZGUtNjI2My00NWEwLTljMDMtNjMyMGI0NmVlODE0OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: Hong Kong Office",
|
||||
"alias": "Hong Kong Office",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvMjdlM2QzZGEtZjI4Ny00NDg3LWI3NzUtM2JmZTA3MzNhNzZhOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: Hazleton Mgmt",
|
||||
"alias": "Hazleton Mgmt",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvYTEzODI3OTgtOWY5Zi00YmY1LTlkMmQtODNhZTk2OGY0MjU3OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: Ottawa Campus Mgmt",
|
||||
"alias": "Ottawa Campus Mgmt",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZDYwNzFhYmMtYzNjZC00ZDdkLWFiYTktOGY0NDRiZTViODQ3OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: MDC DC Mgmt",
|
||||
"alias": "Canada DC Mgmt",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvOTk2ZWRmNzYtNjdlYS00MjdkLWEzNGEtMTBjNjJhYmRkMDM5OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Y2lzY29zcGFyazovL3VzL1BFT1BMRS80NmZkYWEzYy03YTNkLTRjY2QtYWU4Ny0xYzIzNmIyNGE0Zjg": {
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1BFT1BMRS80NmZkYWEzYy03YTNkLTRjY2QtYWU4Ny0xYzIzNmIyNGE0Zjg",
|
||||
"displayName": "Jayna Catalina",
|
||||
"email": "catalinaj@ae.com",
|
||||
"avatar": "https://avatar-prod-us-east-2.webexcontent.com/Avtr~V1~db9cdc31-8f8e-40b4-916c-8d9e9c92e70d/V1~cf803b9485e536eece6e2f2e541f8ab8691867a0765656a0ebb28ba4b74d7bca~8afff234e4f045ad9fa92af6f8d082c0~1600",
|
||||
"groups": [
|
||||
{
|
||||
"name": "Group: South Side Campus",
|
||||
"alias": "SSW Campus",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvYjAxOWM5MzgtNTZhNi00NTQxLTk0NjgtYjFmOWEwM2I5NTNlOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: IT Warrendale",
|
||||
"alias": "Warrendale",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvMjkwN2RlZGUtZjJmNi00OTdjLTkwMjItZTRmOGIzNzM3MTFhOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: New York Design Office Personnel",
|
||||
"alias": "NY Design Center",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvNjUzOGQzYmYtY2UzOS00Y2ZlLWFmZWUtNGViYTM0OGQ3Y2FjOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: 34th Street Office Personnel",
|
||||
"alias": "34th Street",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZGVmOTA0MDMtMTBlZS00YzRlLWIzYWItYzNjZDlkODhmMTQ1OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: San Francisco Personnel",
|
||||
"alias": "San Francisco Office",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvNjJjYTcxYTktYWYzYy00YjQ4LWIxZjUtMjc1NDEwNGYwMWJlOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: Ottawa Campus",
|
||||
"alias": "Ottawa Campus",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZWIyYWI1ZDgtYzk2Ny00MmUwLTg0OWQtZGE5OTYzMDdkNDhhOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: Hazleton Campus",
|
||||
"alias": "Hazleton Campus",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvMDc3NTI1NGEtZTIxMS00ZTQ1LTgxNjktNjYxYTAzOGYxNTE3OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: Canada Distribution Center",
|
||||
"alias": "Canada DC",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZmUwMDk1MjUtMzEyZC00YzMxLTgwMzItNWEyODIwODRiYTVjOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: Mexico Office Personnel",
|
||||
"alias": "Mexico Office",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvNjhmYTJiZGUtNjI2My00NWEwLTljMDMtNjMyMGI0NmVlODE0OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: Hong Kong Office",
|
||||
"alias": "Hong Kong Office",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvMjdlM2QzZGEtZjI4Ny00NDg3LWI3NzUtM2JmZTA3MzNhNzZhOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: Hazleton Mgmt",
|
||||
"alias": "Hazleton Mgmt",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvYTEzODI3OTgtOWY5Zi00YmY1LTlkMmQtODNhZTk2OGY0MjU3OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: Ottawa Campus Mgmt",
|
||||
"alias": "Ottawa Campus Mgmt",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZDYwNzFhYmMtYzNjZC00ZDdkLWFiYTktOGY0NDRiZTViODQ3OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
},
|
||||
{
|
||||
"name": "Group: MDC DC Mgmt",
|
||||
"alias": "Canada DC Mgmt",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvOTk2ZWRmNzYtNjdlYS00MjdkLWEzNGEtMTBjNjJhYmRkMDM5OmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"techupdates": {
|
||||
"label": "Technology Updates",
|
||||
"authorized": {
|
||||
"Y2lzY29zcGFyazovL3VzL1BFT1BMRS80NzAyNDlhNC1kNjFjLTQzNmMtYTE1My1kOGUzZTExMmI4MDU": {
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1BFT1BMRS80NzAyNDlhNC1kNjFjLTQzNmMtYTE1My1kOGUzZTExMmI4MDU",
|
||||
"displayName": "Joe McQueen",
|
||||
"email": "mcqueenj@ae.com",
|
||||
"avatar": "https://avatar-prod-us-east-2.webexcontent.com/Avtr~V1~db9cdc31-8f8e-40b4-916c-8d9e9c92e70d/V1~667e3673b56f4ddc39139c73da9140dd631eb821a756524ead12e37e3a61afc7~9c5b6e767cff46e3af35a003cea38f25~1600",
|
||||
"groups": [
|
||||
{
|
||||
"name": "Group: Audio Visual",
|
||||
"alias": "AV Team",
|
||||
"id": "Y2lzY29zcGFyazovL3VzL1NDSU1fR1JPVVAvZTQ3NmY3MjktZWViYi00MDI3LWFlMTctNzc1YWI1ZDgzYmNhOmRiOWNkYzMxLThmOGUtNDBiNC05MTZjLThkOWU5YzkyZTcwZA"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"integration": {
|
||||
"integrationId": "Y2lzY29zcGFyazovL3VzL0FQUExJQ0FUSU9OL0MzZTQ5NDUwZGQ0ZGMxNmY0OWQxNjEyYTVjNGRiZWNhZjY2M2I1ODdmZWEyOGYzYjMwY2QwOGQzNThkNTNjMDQ1",
|
||||
"integrationName": "CollabCentral"
|
||||
},
|
||||
"serviceAccount": {
|
||||
"appName": "CollabCentral"
|
||||
}
|
||||
}
|
||||
}
|
||||
18
config/languages.json
Normal file
18
config/languages.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[
|
||||
{
|
||||
"name": "spanish",
|
||||
"key": "es"
|
||||
},
|
||||
{
|
||||
"name": "french",
|
||||
"key": "fr"
|
||||
},
|
||||
{
|
||||
"name": "japanese",
|
||||
"key": "ja"
|
||||
},
|
||||
{
|
||||
"name": "chinese",
|
||||
"key": "zh"
|
||||
}
|
||||
]
|
||||
BIN
html/DirectoryConnector.zip
Normal file
BIN
html/DirectoryConnector.zip
Normal file
Binary file not shown.
BIN
html/aeoPropsBot.png
Normal file
BIN
html/aeoPropsBot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
57
html/auth.js
Normal file
57
html/auth.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
//console.log(window.location.pathname);
|
||||
var pathArray = window.location.pathname.split('/');
|
||||
//console.log(pathArray);
|
||||
//console.log(pathArray);
|
||||
var appName = pathArray[2];
|
||||
//console.log("appName=" + appName);
|
||||
/*
|
||||
if (appName == "novi") {
|
||||
setCookie("appLabel", "Novi Communicator");
|
||||
} else if (appName == "techupdates") {
|
||||
setCookie("appLabel", "Technology Updates");
|
||||
} else {
|
||||
setCookie("appLabel", "Unknown");
|
||||
}
|
||||
setCookie("appName",appName,1);
|
||||
*/
|
||||
|
||||
if (getCookie("id")) {
|
||||
console.log("Found ID Cookie: " + getCookie("id"));
|
||||
window.location.href = "/CollabCentral/" + appName + "/sendMessage.html";
|
||||
} else {
|
||||
console.log("No ID Cookie found.");
|
||||
|
||||
var randomNumber = Math.random().toString();
|
||||
randomNumber = randomNumber.substring(2, randomNumber.length);
|
||||
fetch('/CollabCentral/' + appName + '/authUrl')
|
||||
.then(res => res.text())
|
||||
.then((res) => {
|
||||
window.location.href = res + randomNumber;
|
||||
});
|
||||
}
|
||||
|
||||
function getCookie(cname) {
|
||||
let name = cname + "=";
|
||||
let decodedCookie = decodeURIComponent(document.cookie);
|
||||
let ca = decodedCookie.split(';');
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) == ' ') {
|
||||
c = c.substring(1);
|
||||
}
|
||||
if (c.indexOf(name) == 0) {
|
||||
return c.substring(name.length, c.length);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function setCookie(name, value, days) {
|
||||
var expires = "";
|
||||
if (days) {
|
||||
var date = new Date();
|
||||
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||
expires = "; expires=" + date.toUTCString();
|
||||
}
|
||||
document.cookie = name + "=" + (value || "") + expires + "; path=/";
|
||||
}
|
||||
5
html/index.html
Normal file
5
html/index.html
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<html>
|
||||
<body>
|
||||
<script src="auth.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
165
html/jobDetail.html
Normal file
165
html/jobDetail.html
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<title>CollabCentral</title>
|
||||
<link id="appName" rel="icon" type="image/x-icon" style="border-radius: 50%;" href="favicon.ico">
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
background-color: #FFFFFF;
|
||||
color: #2F3941;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: block;
|
||||
background-color: tan;
|
||||
width: 90%;
|
||||
height: auto;
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#appInfo {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: block;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1100px;
|
||||
margin: 1em auto;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
h2.jobTitle {
|
||||
margin: 0.5em 0 0.2em;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #6c757d;
|
||||
margin: 0 0 1em;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
gap: 0.45em 1.5em;
|
||||
margin: 1em 0 1.5em;
|
||||
padding: 1em 1.2em;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.summary-grid dt {
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.summary-grid dd { margin: 0; }
|
||||
|
||||
.section-title {
|
||||
font-size: 1.05em;
|
||||
font-weight: 700;
|
||||
margin: 1.5em 0 0.4em;
|
||||
color: #2F3941;
|
||||
}
|
||||
|
||||
.message-preview {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 1em 1.2em;
|
||||
background: #fafafa;
|
||||
margin: 0.4em 0 1em;
|
||||
}
|
||||
|
||||
.message-preview img { max-width: 100%; height: auto; }
|
||||
|
||||
.pill {
|
||||
display: inline-block;
|
||||
padding: 0.15em 0.7em;
|
||||
border-radius: 999px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pill.ok { background: #d4edda; color: #155724; }
|
||||
.pill.err { background: #f8d7da; color: #721c24; }
|
||||
.pill.pending { background: #fff3cd; color: #856404; }
|
||||
|
||||
td { white-space: pre-wrap !important; word-wrap: break-word; }
|
||||
|
||||
.hidden { display: none; }
|
||||
|
||||
.panel-empty {
|
||||
padding: 2em;
|
||||
max-width: 600px;
|
||||
margin: 2em auto;
|
||||
text-align: center;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
<script src="https://code.jquery.com/jquery-1.11.2.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.css" />
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="header">
|
||||
<div id="appInfo">
|
||||
<img id="appIcon" style="border-radius: 50%; padding: 10pt;">
|
||||
<div id="appLabel" style="font-size: 40pt;margin: auto;"></div>
|
||||
</div>
|
||||
<div style="text-align: center;"><a id="jobLink" href=''>Back to Jobs</a></div>
|
||||
<div id="name" style="text-align: right;"></div>
|
||||
</div>
|
||||
|
||||
<div id="unauthorizedPanel" class="panel-empty hidden">
|
||||
<h2>You don't have access to this bot</h2>
|
||||
<p>Your account isn't on the authorized list. Contact the bot's administrator.</p>
|
||||
</div>
|
||||
|
||||
<div id="notFoundPanel" class="panel-empty hidden">
|
||||
<h2>Job not found</h2>
|
||||
<p>The job you're looking for doesn't exist, has been purged, or belongs to a different bot.</p>
|
||||
</div>
|
||||
|
||||
<div id="detailPanel" class="container hidden">
|
||||
<h2 class="jobTitle" id="jobTitle"></h2>
|
||||
<p class="subtitle" id="subtitle"></p>
|
||||
|
||||
<dl class="summary-grid" id="summaryGrid"></dl>
|
||||
|
||||
<div class="section-title">Message</div>
|
||||
<div class="message-preview" id="messagePreview"></div>
|
||||
|
||||
<div class="section-title">Recipients (<span id="recipientCount">0</span>)</div>
|
||||
<table id="recipients" style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Status</th>
|
||||
<th>Send Time</th>
|
||||
<th>Error</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script src="jobDetail.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
179
html/jobDetail.js
Normal file
179
html/jobDetail.js
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
var pathArray = window.location.pathname.split('/');
|
||||
var appName = pathArray[2];
|
||||
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
var jobId = urlParams.get('jobId');
|
||||
|
||||
if (!getCookie("id")) {
|
||||
var randomNumber = Math.random().toString().substring(2);
|
||||
fetch('/CollabCentral/' + appName + '/authUrl')
|
||||
.then(res => res.text())
|
||||
.then(res => { window.location.href = res + randomNumber; });
|
||||
} else {
|
||||
bootstrap();
|
||||
}
|
||||
|
||||
function bootstrap() {
|
||||
fetch('/CollabCentral/' + appName + '/info')
|
||||
.then(res => res.json())
|
||||
.then(info => {
|
||||
document.title = info.label + " — Job #" + (jobId || "?");
|
||||
document.getElementById("appLabel").innerHTML = info.label;
|
||||
document.getElementById("name").innerHTML = getCookie("displayName") || "";
|
||||
document.getElementById("appName").href = info.faviconUrl;
|
||||
document.getElementById("jobLink").href = "/CollabCentral/" + appName + "/monitorJobs.html";
|
||||
|
||||
var appIcon = document.getElementById("appIcon");
|
||||
appIcon.src = info.iconUrl;
|
||||
appIcon.style.maxHeight = "150px";
|
||||
appIcon.style.maxWidth = "150px";
|
||||
|
||||
if (!info.authorized) {
|
||||
document.getElementById("unauthorizedPanel").classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
loadJobDetail();
|
||||
})
|
||||
.catch(err => console.error("Failed to load /info:", err));
|
||||
}
|
||||
|
||||
function loadJobDetail() {
|
||||
if (!jobId) {
|
||||
document.getElementById("notFoundPanel").classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
fetch('/CollabCentral/' + appName + '/jobs/detail/' + encodeURIComponent(jobId))
|
||||
.then(res => {
|
||||
if (res.status === 404) {
|
||||
document.getElementById("notFoundPanel").classList.remove("hidden");
|
||||
return null;
|
||||
}
|
||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||
return res.json();
|
||||
})
|
||||
.then(job => {
|
||||
if (!job) return;
|
||||
renderJob(job);
|
||||
document.getElementById("detailPanel").classList.remove("hidden");
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Failed to load job detail:", err);
|
||||
document.getElementById("notFoundPanel").classList.remove("hidden");
|
||||
});
|
||||
}
|
||||
|
||||
function renderJob(job) {
|
||||
document.getElementById("jobTitle").textContent = "Job #" + (job.jobId || "?");
|
||||
var subtitle = "Sent by " + (job.senderDisplayName || "unknown");
|
||||
if (job.startTime) subtitle += " · started " + formatDate(job.startTime);
|
||||
document.getElementById("subtitle").textContent = subtitle;
|
||||
|
||||
var dl = document.getElementById("summaryGrid");
|
||||
dl.innerHTML = "";
|
||||
var stats = job.stats || {};
|
||||
var rows = [
|
||||
["Job ID", job.jobId],
|
||||
["Submitted by", job.senderDisplayName],
|
||||
["Scheduled for", job.scheduledFor ? formatDate(job.scheduledFor) : "Sent immediately"],
|
||||
["Started", formatDate(job.startTime)],
|
||||
["Completed", job.endTime ? formatDate(job.endTime) : "In progress"],
|
||||
["Total duration", stats.totalTime],
|
||||
["Webex time", stats.webexTime],
|
||||
["Average per message", stats.averageTime],
|
||||
["Recipients", (job.memberList || []).length],
|
||||
["Delivered", formatSuccess(stats)]
|
||||
];
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var key = rows[i][0], val = rows[i][1];
|
||||
if (val === undefined || val === null || val === "") continue;
|
||||
var dt = document.createElement("dt"); dt.textContent = key;
|
||||
var dd = document.createElement("dd"); dd.textContent = val;
|
||||
dl.appendChild(dt); dl.appendChild(dd);
|
||||
}
|
||||
|
||||
var preview = document.getElementById("messagePreview");
|
||||
var msg = (job.message && job.message.english) || job.message || {};
|
||||
if (msg.html) {
|
||||
preview.innerHTML = msg.html;
|
||||
} else if (msg.markdown) {
|
||||
preview.textContent = msg.markdown;
|
||||
} else if (msg.text) {
|
||||
preview.textContent = msg.text;
|
||||
} else if (typeof msg.raw === "string") {
|
||||
preview.textContent = msg.raw;
|
||||
} else {
|
||||
preview.innerHTML = "<em>No message content available.</em>";
|
||||
}
|
||||
|
||||
var members = job.memberList || [];
|
||||
document.getElementById("recipientCount").textContent = members.length;
|
||||
|
||||
$('#recipients').dataTable({
|
||||
data: members.map(formatRecipientRow),
|
||||
columns: [
|
||||
{ data: 'displayName' },
|
||||
{ data: 'email' },
|
||||
{ data: 'status' },
|
||||
{ data: 'time' },
|
||||
{ data: 'error' }
|
||||
],
|
||||
pageLength: 50,
|
||||
lengthMenu: [25, 50, 100, 250],
|
||||
order: [[2, 'asc']],
|
||||
searching: true,
|
||||
info: true,
|
||||
paging: true
|
||||
});
|
||||
}
|
||||
|
||||
function formatRecipientRow(m) {
|
||||
var result = m.results || null;
|
||||
var status;
|
||||
var email = "—";
|
||||
var err = "";
|
||||
|
||||
if (!result) {
|
||||
status = '<span class="pill pending">Pending</span>';
|
||||
} else if (result.id && !result.message && !result.errors) {
|
||||
status = '<span class="pill ok">Delivered</span>';
|
||||
email = result.toPersonEmail || email;
|
||||
} else {
|
||||
status = '<span class="pill err">Failed</span>';
|
||||
email = result.toPersonEmail || email;
|
||||
if (result.message) err = result.message;
|
||||
else if (result.errors && result.errors.length) {
|
||||
err = result.errors.map(function (e) { return e.description || e.code || JSON.stringify(e); }).join("; ");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
displayName: m.displayName || "—",
|
||||
email: email,
|
||||
status: status,
|
||||
time: m.msgTime ? (m.msgTime / 1000).toFixed(2) + " s" : "—",
|
||||
error: err
|
||||
};
|
||||
}
|
||||
|
||||
function formatSuccess(stats) {
|
||||
if (!stats || stats.succeededMsgs === undefined) return null;
|
||||
var pct = (typeof stats.succeededMsgPct === "number") ? stats.succeededMsgPct.toFixed(1) : stats.succeededMsgPct;
|
||||
return stats.succeededMsgs + " / " + stats.totalMsgs + " (" + pct + "%)";
|
||||
}
|
||||
|
||||
function formatDate(d) {
|
||||
if (!d) return null;
|
||||
try { return new Date(d).toLocaleString(); } catch (e) { return d; }
|
||||
}
|
||||
|
||||
function getCookie(cname) {
|
||||
let name = cname + "=";
|
||||
let decodedCookie = decodeURIComponent(document.cookie);
|
||||
let ca = decodedCookie.split(';');
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) == ' ') c = c.substring(1);
|
||||
if (c.indexOf(name) == 0) return c.substring(name.length, c.length);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
13
html/js/virtual-select.min.css
vendored
Normal file
13
html/js/virtual-select.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
5
html/js/virtual-select.min.js
vendored
Normal file
5
html/js/virtual-select.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
126
html/monitorJobs.html
Normal file
126
html/monitorJobs.html
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<title>CollabCentral</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #FFFFFF;
|
||||
color: #2F3941;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
padding: 10px;
|
||||
|
||||
}
|
||||
|
||||
#table {
|
||||
width: 80%;
|
||||
}
|
||||
td {
|
||||
white-space: pre-wrap !important;
|
||||
word-wrap:break-word;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: block;
|
||||
background-color: tan;
|
||||
width: 80%;
|
||||
height: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: block;
|
||||
text-align: right;
|
||||
align-content: right;
|
||||
}
|
||||
|
||||
#appInfo {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
align-content: center;
|
||||
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<script src="https://code.jquery.com/jquery-1.11.2.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.css" />
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<link id="appName" rel="icon" type="image/x-icon" style="border-radius: 50%;" href="favicon.ico">
|
||||
|
||||
<div class="header">
|
||||
<div id="appInfo">
|
||||
<img id="appIcon" style="border-radius: 50%; padding: 10pt;">
|
||||
<div id="appLabel" style="font-size: 40pt;margin: auto;"></div>
|
||||
</div>
|
||||
<div style="text-align: center;"><a id="jobLink" href=''>Send Message</a></div>
|
||||
<div id="name" style="text-align: right;"></div>
|
||||
</div>
|
||||
|
||||
<div id="unauthorizedPanel" class="hidden" style="padding: 2em; max-width: 600px; margin: 2em auto; text-align: center; border: 1px solid #ddd; border-radius: 8px;">
|
||||
<h2>You don't have access to this bot</h2>
|
||||
<p>Your account isn't on the authorized list for this bot. If you believe this is a mistake, contact the bot's administrator.</p>
|
||||
</div>
|
||||
|
||||
<div id="jobsPanel" class="hidden">
|
||||
<h3>Running Jobs</h3>
|
||||
<div class="table"></div>
|
||||
<table id="runningJobs" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Submitter</th>
|
||||
<th>App</th>
|
||||
<th>Start Time</th>
|
||||
<th>Total Recipients</th>
|
||||
<th>Completed Recipients</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
<h3>Scheduled Jobs</h3>
|
||||
<table id="scheduledJobs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Submitter</th>
|
||||
<th>App</th>
|
||||
<th>Message</th>
|
||||
<th>Recipients</th>
|
||||
<th>Scheduled for</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
|
||||
<h3>Completed Jobs</h3>
|
||||
<table id="completedJobs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>jobId</th>
|
||||
<th>Submitter</th>
|
||||
<th>App</th>
|
||||
<th>Message</th>
|
||||
<th>Start Time</th>
|
||||
<th>End Time</th>
|
||||
<th>Total Time</th>
|
||||
<th>Webex Time</th>
|
||||
<th>Average Message Time</th>
|
||||
<th>Messages Delivered</th>
|
||||
<th>Total Messages</th>
|
||||
<th>Success Rate</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
<script src="monitorJobs.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
132
html/monitorJobs.js
Normal file
132
html/monitorJobs.js
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
var pathArray = window.location.pathname.split('/');
|
||||
var appName = pathArray[2];
|
||||
|
||||
if (getCookie("id")) {
|
||||
console.log("Found ID Cookie: " + getCookie("id"));
|
||||
|
||||
// Fetch bot metadata + authorization state, then render the page.
|
||||
fetch('/CollabCentral/' + appName + '/info')
|
||||
.then(res => res.json())
|
||||
.then(info => {
|
||||
document.title = info.label;
|
||||
document.getElementById("appLabel").innerHTML = info.label;
|
||||
document.getElementById("name").innerHTML = getCookie("displayName") || "";
|
||||
document.getElementById("appName").href = info.faviconUrl;
|
||||
document.getElementById("jobLink").href = "/CollabCentral/" + appName + "/sendMessage.html";
|
||||
|
||||
var appIcon = document.getElementById("appIcon");
|
||||
appIcon.src = info.iconUrl;
|
||||
appIcon.style.maxHeight = "150px";
|
||||
appIcon.style.maxWidth = "150px";
|
||||
|
||||
if (!info.authorized) {
|
||||
document.getElementById("unauthorizedPanel").classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("jobsPanel").classList.remove("hidden");
|
||||
initJobTables();
|
||||
})
|
||||
.catch(err => console.error("Failed to load /info:", err));
|
||||
} else {
|
||||
console.log("No ID Cookie found.");
|
||||
|
||||
var randomNumber = Math.random().toString();
|
||||
randomNumber = randomNumber.substring(2, randomNumber.length);
|
||||
fetch('/CollabCentral/' + appName + '/authUrl')
|
||||
.then(res => res.text())
|
||||
.then((res) => {
|
||||
window.location.href = res + randomNumber;
|
||||
});
|
||||
}
|
||||
|
||||
function initJobTables() {
|
||||
$('#runningJobs').dataTable({
|
||||
ajax: {
|
||||
url: '/CollabCentral/' + appName + '/jobs/list/running',
|
||||
dataSrc: ''
|
||||
},
|
||||
columns: [
|
||||
{ data: 'senderDisplayName' },
|
||||
{ data: 'appName' },
|
||||
{ data: 'startTime' },
|
||||
{ data: 'totalRecipients' },
|
||||
{
|
||||
data: 'completedRecipients',
|
||||
render: function (val, type, row) {
|
||||
if (type !== 'display' || row.jobId === undefined || row.jobId === null) return val;
|
||||
return val + ' <a href="/CollabCentral/' + appName + '/jobDetail.html?jobId=' + encodeURIComponent(row.jobId) + '" style="margin-left: 0.5em; font-size: 0.85em;">view</a>';
|
||||
}
|
||||
}
|
||||
],
|
||||
searching: false,
|
||||
info: false,
|
||||
ordering: false,
|
||||
paging: false
|
||||
});
|
||||
|
||||
$('#scheduledJobs').dataTable({
|
||||
ajax: {
|
||||
url: '/CollabCentral/' + appName + '/jobs/list/scheduled',
|
||||
dataSrc: ''
|
||||
},
|
||||
columns: [
|
||||
{ data: 'senderDisplayName' },
|
||||
{ data: 'appName' },
|
||||
{ data: 'message' },
|
||||
{ data: 'totalRecipients' },
|
||||
{ data: 'scheduledFor' }
|
||||
],
|
||||
searching: false,
|
||||
info: false,
|
||||
ordering: false,
|
||||
paging: false
|
||||
});
|
||||
|
||||
$('#completedJobs').dataTable({
|
||||
ajax: {
|
||||
url: '/CollabCentral/' + appName + '/jobs/list/completed',
|
||||
dataSrc: ''
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
data: 'jobId',
|
||||
render: function (jobId) {
|
||||
if (jobId === undefined || jobId === null) return '';
|
||||
return '<a href="/CollabCentral/' + appName + '/jobDetail.html?jobId=' + encodeURIComponent(jobId) + '">#' + jobId + '</a>';
|
||||
}
|
||||
},
|
||||
{ data: 'senderDisplayName' },
|
||||
{ data: 'appName' },
|
||||
{ data: 'message.english.html' },
|
||||
{ data: 'startTime' },
|
||||
{ data: 'endTime' },
|
||||
{ data: 'stats.totalTime' },
|
||||
{ data: 'stats.webexTime' },
|
||||
{ data: 'stats.averageTime' },
|
||||
{ data: 'stats.succeededMsgs' },
|
||||
{ data: 'stats.totalMsgs' },
|
||||
{ data: 'stats.succeededMsgPct' }
|
||||
],
|
||||
searching: false,
|
||||
info: false,
|
||||
ordering: false,
|
||||
paging: false
|
||||
});
|
||||
}
|
||||
|
||||
function getCookie(cname) {
|
||||
let name = cname + "=";
|
||||
let decodedCookie = decodeURIComponent(document.cookie);
|
||||
let ca = decodedCookie.split(';');
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) == ' ') {
|
||||
c = c.substring(1);
|
||||
}
|
||||
if (c.indexOf(name) == 0) {
|
||||
return c.substring(name.length, c.length);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
BIN
html/novi.ico
Normal file
BIN
html/novi.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
html/novi.png
Normal file
BIN
html/novi.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
308
html/sendMessage.html
Normal file
308
html/sendMessage.html
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<title>CollabCentral</title>
|
||||
<link id="appName" rel="icon" type="image/x-icon" style="border-radius: 50%;" href="favicon.ico">
|
||||
<script>
|
||||
let cookie = document.cookie;
|
||||
if (cookie) {
|
||||
} else {
|
||||
window.location.href = "index.html";
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 60%;
|
||||
padding: 12px 20px;
|
||||
margin: 8px 0;
|
||||
margin-bottom: 10px;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.form-field input {
|
||||
border: 1px solid #87929D;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.form-field label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-top: 10px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #FFFFFF;
|
||||
color: #2F3941;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
padding: 10px;
|
||||
|
||||
}
|
||||
|
||||
.header {
|
||||
display: block;
|
||||
background-color: tan;
|
||||
width: 75%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
form {
|
||||
display: block;
|
||||
margin-top: 0em;
|
||||
}
|
||||
|
||||
|
||||
#file-ip-1-preview {
|
||||
width: auto;
|
||||
height: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.groups {
|
||||
width: 1000px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#my-text-area {
|
||||
height: 250px;
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.EasyMDEContainer {
|
||||
display: block;
|
||||
width: 75%;
|
||||
border: 1px solid #87929D;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.submit {
|
||||
width: 75%;
|
||||
height: auto;
|
||||
margin: 10px;
|
||||
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
width: 450px;
|
||||
padding: 1.3rem;
|
||||
min-height: 250px;
|
||||
position: absolute;
|
||||
top: 20%;
|
||||
background-color: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 15px;
|
||||
z-index: 2;
|
||||
|
||||
}
|
||||
|
||||
.modal .flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modal input {
|
||||
padding: 0.7rem 1rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.modal p {
|
||||
font-size: 0.9rem;
|
||||
color: #777;
|
||||
margin: 0.4rem 0 0.2rem;
|
||||
}
|
||||
|
||||
.submit button {
|
||||
display: inline-block;
|
||||
padding: 0.8rem 1.4rem;
|
||||
font-weight: 700;
|
||||
background-color: black;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.8rem 1.4rem;
|
||||
font-weight: 700;
|
||||
background-color: black;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.btn-open {
|
||||
position: absolute;
|
||||
bottom: 150px;
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
transform: translate(10px, -20px);
|
||||
padding: 0.5rem 0.7rem;
|
||||
background: #eee;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(3px);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: block;
|
||||
text-align: right;
|
||||
align-content: right;
|
||||
}
|
||||
|
||||
#appInfo {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
align-content: center;
|
||||
|
||||
}
|
||||
|
||||
#multi_option {
|
||||
max-width: 100%;
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
vscomp-toggle-button {
|
||||
padding: 10px 30px 10px 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css">
|
||||
<link rel="stylesheet" href="js/virtual-select.min.css" />
|
||||
|
||||
<script src="js/virtual-select.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
|
||||
<div class="header">
|
||||
<div id="appInfo">
|
||||
<img id="appIcon" style="border-radius: 50%; padding: 10pt;">
|
||||
<div id="appLabel" style="font-size: 40pt;margin: auto;"></div>
|
||||
</div>
|
||||
<div style="text-align: center;"><a id="jobLink" href=''>Monitor Jobs</a></div>
|
||||
<div id="name" style="text-align: right;"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="unauthorizedPanel" class="hidden" style="padding: 2em; max-width: 600px; margin: 2em auto; text-align: center; border: 1px solid #ddd; border-radius: 8px;">
|
||||
<h2>You don't have access to this bot</h2>
|
||||
<p>Your account isn't on the authorized list for this bot. If you believe this is a mistake, contact the bot's administrator.</p>
|
||||
</div>
|
||||
|
||||
<div id="formPanel" class="form-input hidden">
|
||||
<form action="" id="sendMessage" enctype="multipart/form-data">
|
||||
<div class="form-field">
|
||||
|
||||
<img id="file-ip-1-preview">
|
||||
<label for="uploadImage">Upload Image</label>
|
||||
<input type="file" id="uploadImage" accept="image/*" name="uploadImage"
|
||||
onchange="showImagePreview(event);">
|
||||
</div>
|
||||
<label for="my-text-area">Message</label>
|
||||
<textarea id="my-text-area"></textarea>
|
||||
|
||||
<div class="form-field">
|
||||
<div class="groups">
|
||||
<label for="favGroups">Favorite Groups:</label>
|
||||
<select id="favGroups" multiple name="favGroups" placeholder="Favorite Groups"
|
||||
data-silent-initial-value-set="false">
|
||||
</select>
|
||||
</div>
|
||||
<div class="groups">
|
||||
<label>New Groups:</label>
|
||||
<select id="newGroups" multiple name="newGroups" placeholder="New Groups"
|
||||
data-silent-initial-value-set="false">
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="people">
|
||||
<label for="uploadCSV">Upload CSV:</label>
|
||||
<input type="file" id="uploadCSV" name="uploadCSV" accept="text/csv">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<div class="when">
|
||||
<label for="meeting-time">Schedule for:</label>
|
||||
<input type="datetime-local" id="scheduledFor" name="scheduledFor" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="submit">
|
||||
<button id="submit" type="submit">Submit</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
<section class="modal hidden">
|
||||
<div class="flex">
|
||||
<button class="btn-close" onclick="continueEditing()">X</button>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Ready to send?</h3>
|
||||
<p id="modalMessage">
|
||||
This message will be sent to XX people.<br>
|
||||
Ready to send?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button class="btn" onclick="continueEditing()">Continue Editing</button>
|
||||
<button class="btn" onclick="submit()">Submit</button>
|
||||
|
||||
</section>
|
||||
|
||||
<div class="overlay hidden"></div>
|
||||
|
||||
</div>
|
||||
<script src="sendMessage.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
222
html/sendMessage.js
Normal file
222
html/sendMessage.js
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
const listbox = document.querySelector('#list');
|
||||
const modal = document.querySelector(".modal");
|
||||
const overlay = document.querySelector(".overlay");
|
||||
const openModalBtn = document.querySelector(".btn-open");
|
||||
const closeModalBtn = document.querySelector(".btn-close");
|
||||
|
||||
var pathArray = window.location.pathname.split('/'); //Gets the path of the URL called
|
||||
var appName = pathArray[2]; //Gets the appName from the path.
|
||||
|
||||
document.getElementById('sendMessage').onsubmit = function (event) {
|
||||
|
||||
event.preventDefault() // prevent form from posting without JS
|
||||
var xhttp = new XMLHttpRequest(); // create new AJAX request
|
||||
|
||||
|
||||
xhttp.onreadystatechange = function () {
|
||||
if (this.readyState == this.DONE && this.status == 200) { // sucess from server
|
||||
result = JSON.parse(xhttp.response);
|
||||
console.log(xhttp.responseText)
|
||||
console.log(xhttp.response);
|
||||
var message = "I sent you a test message in your Webex Client for review.<br><br>";
|
||||
message += "Recipients: " + result.memberList.length + "<br><br>";
|
||||
if (document.getElementById("scheduledFor").value) {
|
||||
message += "Scheduled for " + document.getElementById("scheduledFor").value;
|
||||
} else {
|
||||
message += "Message will be sent immediately.";
|
||||
}
|
||||
|
||||
|
||||
document.getElementById("modalMessage").innerHTML = message;
|
||||
openModal();
|
||||
} else { // errors occured
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var formData = new FormData()
|
||||
formData.append('uploadImage', document.getElementById('uploadImage').files[0]) // since inputs allow multi files submission, therefore files are in array
|
||||
formData.append('message', message.value())
|
||||
formData.append('uploadCSV', document.getElementById('uploadCSV').files[0]) // since inputs allow multi files submission, therefore files are in array
|
||||
formData.append('appName', appName);
|
||||
|
||||
var selectedGroups = [];
|
||||
for (var group of document.querySelector('#favGroups').getSelectedOptions()) {
|
||||
selectedGroups.push(group.value);
|
||||
}
|
||||
for (var group of document.querySelector('#newGroups').getSelectedOptions()) {
|
||||
selectedGroups.push(group.value);
|
||||
}
|
||||
|
||||
formData.append('groups', selectedGroups);
|
||||
formData.append('scheduledFor', document.getElementById("scheduledFor").value);
|
||||
xhttp.open("POST", "/CollabCentral/" + appName + "/jobs/edit")
|
||||
console.log(xhttp);
|
||||
xhttp.send(formData)
|
||||
}
|
||||
|
||||
function showImagePreview(event) {
|
||||
if (event.target.files.length > 0) {
|
||||
var src = URL.createObjectURL(event.target.files[0]);
|
||||
var preview = document.getElementById("file-ip-1-preview");
|
||||
preview.src = src;
|
||||
preview.style.display = "block";
|
||||
preview.style.maxHeight = "350px";
|
||||
preview.style.maxWidth = "500px";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let id = getCookie("id");
|
||||
|
||||
// Fetch bot metadata + authorization state, then either render the form or
|
||||
// show the "not authorized" panel. All bot-specific labels and icons come from
|
||||
// /info so adding a new bot doesn't require any front-end changes.
|
||||
fetch('/CollabCentral/' + appName + '/info')
|
||||
.then(res => res.json())
|
||||
.then(info => {
|
||||
document.title = info.label;
|
||||
document.getElementById("appLabel").innerHTML = info.label;
|
||||
document.getElementById("name").innerHTML = getCookie("displayName") || "";
|
||||
document.getElementById("appName").href = info.faviconUrl;
|
||||
document.getElementById("jobLink").href = "/CollabCentral/" + appName + "/monitorJobs.html";
|
||||
|
||||
var appIcon = document.getElementById("appIcon");
|
||||
appIcon.src = info.iconUrl;
|
||||
appIcon.style.maxHeight = "150px";
|
||||
appIcon.style.maxWidth = "150px";
|
||||
|
||||
if (!info.authorized) {
|
||||
document.getElementById("unauthorizedPanel").classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("formPanel").classList.remove("hidden");
|
||||
loadFavoriteGroups();
|
||||
loadNewGroups();
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Failed to load /info:", err);
|
||||
});
|
||||
|
||||
function loadFavoriteGroups() {
|
||||
var x = document.getElementById("favGroups");
|
||||
fetch('/CollabCentral/' + appName + '/user/groups/list')
|
||||
.then(res => res.json())
|
||||
.then((res) => {
|
||||
for (var group of res) {
|
||||
var option = document.createElement("option");
|
||||
option.text = group.name;
|
||||
option.value = group.id;
|
||||
x.add(option);
|
||||
}
|
||||
VirtualSelect.init({ ele: '#favGroups' });
|
||||
});
|
||||
}
|
||||
|
||||
function loadNewGroups() {
|
||||
var y = document.getElementById("newGroups");
|
||||
fetch('/CollabCentral/' + appName + '/user/groups/find')
|
||||
.then(res => res.json())
|
||||
.then((res) => {
|
||||
for (var group of res) {
|
||||
var option = document.createElement("option");
|
||||
option.text = group.displayName;
|
||||
option.value = group.id;
|
||||
y.add(option);
|
||||
}
|
||||
VirtualSelect.init({ ele: '#newGroups' });
|
||||
});
|
||||
}
|
||||
|
||||
function getCookie(cname) {
|
||||
let name = cname + "=";
|
||||
let decodedCookie = decodeURIComponent(document.cookie);
|
||||
let ca = decodedCookie.split(';');
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) == ' ') {
|
||||
c = c.substring(1);
|
||||
}
|
||||
if (c.indexOf(name) == 0) {
|
||||
return c.substring(name.length, c.length);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
const message = new EasyMDE({
|
||||
element: document.getElementById('my-text-area'),
|
||||
toolbar: ["bold", "italic", "|", "heading-1", "heading-2", "heading-3", "|", "code", "unordered-list", "ordered-list", "link", "quote", "|", "preview"],
|
||||
maxHeight: "200px"
|
||||
});
|
||||
|
||||
|
||||
|
||||
function submit() {
|
||||
if (document.getElementById("scheduledFor").value) {
|
||||
fetch("/CollabCentral/" + appName + "/jobs/schedule", {
|
||||
method: "POST",
|
||||
redirect: "follow"
|
||||
})
|
||||
.then(response => {
|
||||
clearForm();
|
||||
// HTTP 301 response
|
||||
if (response.redirected) {
|
||||
window.location.href = response.url;
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.info(err + " url: " + url);
|
||||
});
|
||||
|
||||
} else {
|
||||
console.log("RunNow selected.");
|
||||
fetch("/CollabCentral/" + appName + "/jobs/runNow", {
|
||||
method: "POST",
|
||||
redirect: "follow"
|
||||
})
|
||||
.then(response => {
|
||||
clearForm();
|
||||
// HTTP 301 response
|
||||
if (response.redirected) {
|
||||
window.location.href = response.url;
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.info(err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
closeModal();
|
||||
}
|
||||
|
||||
function continueEditing() {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
const openModal = function () {
|
||||
modal.classList.remove("hidden");
|
||||
overlay.classList.remove("hidden");
|
||||
};
|
||||
|
||||
const closeModal = function () {
|
||||
modal.classList.add("hidden");
|
||||
overlay.classList.add("hidden");
|
||||
};
|
||||
|
||||
overlay.addEventListener("click", closeModal);
|
||||
|
||||
function clearForm() {
|
||||
document.getElementById('uploadImage').innerHTML = "";
|
||||
message.value("");
|
||||
document.getElementById('uploadCSV').innerHTML = "";
|
||||
document.getElementById('favGroups').selected = null;
|
||||
document.getElementById('newGroups').selected = null;
|
||||
document.getElementById('scheduledFor').innerHTML = "";
|
||||
}
|
||||
|
||||
|
||||
BIN
html/techupdates.ico
Normal file
BIN
html/techupdates.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
html/techupdates.png
Normal file
BIN
html/techupdates.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
1500
package-lock.json
generated
Normal file
1500
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
25
package.json
Normal file
25
package.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "collabcentral",
|
||||
"version": "1.0.0",
|
||||
"description": "Micro-service for sending messages to the organization.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node --env-file=.env index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Joseph B. McQueen",
|
||||
"license": "ISC",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"body-parser": "^1.20.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"express": "^4.18.2",
|
||||
"form-data": "^4.0.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-cron": "^3.0.2",
|
||||
"node-fetch": "^3.3.2",
|
||||
"p-queue": "^7.4.1",
|
||||
"sharp": "^0.32.6",
|
||||
"uuid": "^9.0.1"
|
||||
}
|
||||
}
|
||||
0
uploads/.gitkeep
Normal file
0
uploads/.gitkeep
Normal file
Loading…
Reference in a new issue